commit ec226e83488ffa8c1d2cdbe47e5a7d24c7266fc7 Author: patchzyy <64382339+patchzyy@users.noreply.github.com> Date: Sun Aug 23 17:10:50 2026 +0200 init diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..903e5cd --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.github/ISSUE_TEMPLATE/1-crash-report.yml b/.github/ISSUE_TEMPLATE/1-crash-report.yml new file mode 100644 index 0000000..4ebf2bd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-crash-report.yml @@ -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 diff --git a/.github/ISSUE_TEMPLATE/2-bug-report.yml b/.github/ISSUE_TEMPLATE/2-bug-report.yml new file mode 100644 index 0000000..00608f0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-bug-report.yml @@ -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 diff --git a/.github/ISSUE_TEMPLATE/3-install-problem.yml b/.github/ISSUE_TEMPLATE/3-install-problem.yml new file mode 100644 index 0000000..72f6828 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3-install-problem.yml @@ -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" diff --git a/.github/ISSUE_TEMPLATE/4-performance.yml b/.github/ISSUE_TEMPLATE/4-performance.yml new file mode 100644 index 0000000..670fd1d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/4-performance.yml @@ -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 diff --git a/.github/ISSUE_TEMPLATE/5-feature-request.yml b/.github/ISSUE_TEMPLATE/5-feature-request.yml new file mode 100644 index 0000000..0532e0c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/5-feature-request.yml @@ -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. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..db8a4da --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -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. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..ad2796d --- /dev/null +++ b/.github/workflows/build.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1c69c0a --- /dev/null +++ b/.gitignore @@ -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 + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..610cbe3 --- /dev/null +++ b/CONTRIBUTING.md @@ -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. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..61d1860 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ +GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. + + + Copyright (C) + + 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. \ No newline at end of file diff --git a/Launcher/.gitignore b/Launcher/.gitignore new file mode 100644 index 0000000..c9a6cc1 --- /dev/null +++ b/Launcher/.gitignore @@ -0,0 +1,4 @@ +/artifacts/ +/dist/ +**/bin/ +**/obj/ diff --git a/Launcher/Build-Installer.ps1 b/Launcher/Build-Installer.ps1 new file mode 100644 index 0000000..7792401 --- /dev/null +++ b/Launcher/Build-Installer.ps1 @@ -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)) diff --git a/Launcher/LocalBuild.ps1 b/Launcher/LocalBuild.ps1 new file mode 100644 index 0000000..4ecb371 --- /dev/null +++ b/Launcher/LocalBuild.ps1 @@ -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 } +} diff --git a/Launcher/NativeBuildFlags.ps1 b/Launcher/NativeBuildFlags.ps1 new file mode 100644 index 0000000..f7adb3e --- /dev/null +++ b/Launcher/NativeBuildFlags.ps1 @@ -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 +} diff --git a/Launcher/Prepare-NativePrebuilt.ps1 b/Launcher/Prepare-NativePrebuilt.ps1 new file mode 100644 index 0000000..10a36b6 --- /dev/null +++ b/Launcher/Prepare-NativePrebuilt.ps1 @@ -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 " 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 { "`$<`$:$_>" }) + +# --------------------------------------------------------------------------- +# 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('# $ 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) diff --git a/Launcher/Prepare-PortableTools.ps1 b/Launcher/Prepare-PortableTools.ps1 new file mode 100644 index 0000000..30dda2a --- /dev/null +++ b/Launcher/Prepare-PortableTools.ps1 @@ -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" diff --git a/Launcher/Prepare-Release.ps1 b/Launcher/Prepare-Release.ps1 new file mode 100644 index 0000000..8e51414 --- /dev/null +++ b/Launcher/Prepare-Release.ps1 @@ -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.' diff --git a/Launcher/Test-NativeDependencies.ps1 b/Launcher/Test-NativeDependencies.ps1 new file mode 100644 index 0000000..a8bbe8f --- /dev/null +++ b/Launcher/Test-NativeDependencies.ps1 @@ -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." diff --git a/Launcher/Test-PayloadBoundary.ps1 b/Launcher/Test-PayloadBoundary.ps1 new file mode 100644 index 0000000..79bdc3f --- /dev/null +++ b/Launcher/Test-PayloadBoundary.ps1 @@ -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." diff --git a/Launcher/Test-PinnedFacts.ps1 b/Launcher/Test-PinnedFacts.ps1 new file mode 100644 index 0000000..4d97c9c --- /dev/null +++ b/Launcher/Test-PinnedFacts.ps1 @@ -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.' diff --git a/Launcher/WiiCompiled.Setup/CancellationSignal.cs b/Launcher/WiiCompiled.Setup/CancellationSignal.cs new file mode 100644 index 0000000..dd4848b --- /dev/null +++ b/Launcher/WiiCompiled.Setup/CancellationSignal.cs @@ -0,0 +1,75 @@ +namespace WiiCompiled.Setup; + +/// +/// Bridges a frontend-owned, named Windows event into the cancellation token used by setup. +/// +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(); + } +} diff --git a/Launcher/WiiCompiled.Setup/CommandLine.cs b/Launcher/WiiCompiled.Setup/CommandLine.cs new file mode 100644 index 0000000..ef573d6 --- /dev/null +++ b/Launcher/WiiCompiled.Setup/CommandLine.cs @@ -0,0 +1,191 @@ +namespace WiiCompiled.Setup; + +internal enum AppMode +{ + Help, + SilentInstall, + VerifyInputs, + Uninstall, + SilentUninstall, + UninstallWorker, + SelfTest, + LaunchBase, + LaunchRetro, + CheckProducts, + RepairProducts, + Version, + EmitPayloadIdentities +} + +/// +/// 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. +/// +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 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]; + } +} diff --git a/Launcher/WiiCompiled.Setup/CompileInputsFingerprint.cs b/Launcher/WiiCompiled.Setup/CompileInputsFingerprint.cs new file mode 100644 index 0000000..8b8dc0a --- /dev/null +++ b/Launcher/WiiCompiled.Setup/CompileInputsFingerprint.cs @@ -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"; + + /// 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. + 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(); + } + + /// + /// 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 + /// + 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 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}"); +} diff --git a/Launcher/WiiCompiled.Setup/ConsoleCommands.cs b/Launcher/WiiCompiled.Setup/ConsoleCommands.cs new file mode 100644 index 0000000..499b591 --- /dev/null +++ b/Launcher/WiiCompiled.Setup/ConsoleCommands.cs @@ -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 --install-dir [--retro-dir ] [--portable]"); + Console.Out.WriteLine(" --verify-inputs --game [--retro-dir ]"); + Console.Out.WriteLine(" --check-products [--install-dir ] [--retro-dir ] [--progress-json]"); + Console.Out.WriteLine(" --repair-products --install-dir --retro-dir " + + "(--download-retro-wfc-payload | --skip-retro-wfc-payload) [--progress-json]"); + Console.Out.WriteLine(" --launch-retro | --launch-base"); + Console.Out.WriteLine(" --uninstall --install-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 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."); + } + } + + /// 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. + 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."); + } + } + + /// + /// 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. + /// + public static async Task 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."); + } + } + + /// + /// The products 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. + /// + 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))); + } + + /// + /// Inspects both products against the canonical Retro Rewind installation: the caller's explicit + /// folder when it supplied one, otherwise the recorded retro_rewind_root. + /// + 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); + } + + /// + /// 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. + /// + 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)); + } + + /// + /// Emits the stable products record. The frontend branches on status only, never + /// on the detail text, and an unrecognized status fails closed on its side. + /// + 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 } + }); +} diff --git a/Launcher/WiiCompiled.Setup/FileSystemUtilities.cs b/Launcher/WiiCompiled.Setup/FileSystemUtilities.cs new file mode 100644 index 0000000..1b662f9 --- /dev/null +++ b/Launcher/WiiCompiled.Setup/FileSystemUtilities.cs @@ -0,0 +1,233 @@ +namespace WiiCompiled.Setup; + +/// +/// 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. +/// +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); + } + } + + /// + /// 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. + /// + public static IReadOnlyList 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(); + var pending = new Stack(); + 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; + } + + /// Removes exactly this directory, clearing attributes that would refuse deletion. + 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); + } + + /// + /// One comparable spelling of a path. Normalization happens before the trailing separator is + /// dropped, because Path.GetFullPath("C:") is the current directory on that drive rather + /// than the drive root. + /// + public static string NormalizePath(string path) => + Path.TrimEndingDirectorySeparator(Path.GetFullPath(path)); + + /// Whether two paths name the same location, after normalization. + public static bool PathsEqual(string left, string right) => + NormalizePath(left).Equals(NormalizePath(right), StringComparison.OrdinalIgnoreCase); + + /// Whether is or inside it, using a + /// normalized prefix check so C:\Games2 doesn't match inside C:\Games. + 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); + } + + /// Whether two paths are equal or one contains the other, after normalization. + public static bool PathsOverlap(string left, string right) => + PathContains(left, right) || PathContains(right, left); + + /// Location rules shared by install directories and portable roots (both are trees setup may + /// replace and uninstall may delete). only names what's being rejected. + 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."); + } + } + + /// + /// 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. + /// + private const int MaximumLocationLength = 180; + + /// Well-known folders that must never be an installation or portable root. + 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 + ]; + + /// + /// 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. + /// + 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)); + + /// + /// 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. + /// + 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('\\', '/'); +} diff --git a/Launcher/WiiCompiled.Setup/GameLaunchService.cs b/Launcher/WiiCompiled.Setup/GameLaunchService.cs new file mode 100644 index 0000000..58d10d0 --- /dev/null +++ b/Launcher/WiiCompiled.Setup/GameLaunchService.cs @@ -0,0 +1,64 @@ +using System.Diagnostics; + +namespace WiiCompiled.Setup; + +internal static class GameLaunchService +{ + public static Task 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); + } + + /// 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. + 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; + } + +} diff --git a/Launcher/WiiCompiled.Setup/InputValidation.cs b/Launcher/WiiCompiled.Setup/InputValidation.cs new file mode 100644 index 0000000..854bd37 --- /dev/null +++ b/Launcher/WiiCompiled.Setup/InputValidation.cs @@ -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 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"); + + /// 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. + 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 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(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 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 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 +{ + /// Runs a redirected child process to completion. sets up a + /// working directory or scrubbed environment; is off for callers that only + /// forward output live, so a build's output isn't buffered in memory for nobody to read. + public static async Task RunAsync(string executable, IReadOnlyList arguments, + Action? output, CancellationToken cancellationToken, + Action? configure = null, bool capture = true, + Action? 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(); + var stderr = new List(); + 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? 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(); + } +} diff --git a/Launcher/WiiCompiled.Setup/InstallOperationLock.cs b/Launcher/WiiCompiled.Setup/InstallOperationLock.cs new file mode 100644 index 0000000..44f1c78 --- /dev/null +++ b/Launcher/WiiCompiled.Setup/InstallOperationLock.cs @@ -0,0 +1,86 @@ +using System.Security.Cryptography; +using System.Text; + +namespace WiiCompiled.Setup; + +/// +/// 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. +/// +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(); +} + +/// Deterministic paths shared by the operation lock and its recovery journal. +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}"); +} diff --git a/Launcher/WiiCompiled.Setup/InstallProgress.cs b/Launcher/WiiCompiled.Setup/InstallProgress.cs new file mode 100644 index 0000000..ac1e1fb --- /dev/null +++ b/Launcher/WiiCompiled.Setup/InstallProgress.cs @@ -0,0 +1,235 @@ +using System.Text.Json; + +namespace WiiCompiled.Setup; + +/// +/// Stable stage identifiers reported by --progress-json. 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. +/// +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"; +} + +/// +/// 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. +/// +internal interface IInstallReporter +{ + void Progress(string stage, string message, int percent); + void Diagnostic(string line); +} + +/// Reports to a caller-supplied sink; used by console and maintenance commands. +internal sealed class DelegatingInstallReporter : IInstallReporter +{ + private readonly Action _sink; + + public DelegatingInstallReporter(Action sink) => _sink = sink; + + public void Progress(string stage, string message, int percent) => _sink(message); + public void Diagnostic(string line) => _sink(line); +} + +/// +/// The --progress-json protocol: one JSON object per line on stdout, nothing else on stdout, +/// diagnostics on stderr. The terminal result line is written exactly once. +/// +internal sealed class NdjsonInstallReporter : IInstallReporter +{ + private static readonly JsonSerializerOptions Options = new() { WriteIndented = false }; + private readonly Action? _log; + private readonly object _gate = new(); + private int _lastPercent; + private bool _finished; + + public NdjsonInstallReporter(Action? 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); + } + + /// + /// 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. + /// + public void EnsureFinished(string errorIfUnfinished) => Failure(errorIfUnfinished); + + private static void WriteLine(object value) + { + Console.Out.WriteLine(JsonSerializer.Serialize(value, Options)); + Console.Out.Flush(); + } +} + +/// Plain-text console reporting for a silent install without --progress-json. +internal sealed class ConsoleInstallReporter : IInstallReporter +{ + private readonly Action? _log; + + public ConsoleInstallReporter(Action? 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); + } +} + +/// Build step identifiers from the bundled script's MKWCBUILD:STEP:<id> 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. +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"; +} + +/// +/// 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 MKWCBUILD: prefix, so the slice can +/// advance on real events instead of on a timer. +/// +internal sealed class BuildProgressWindow +{ + private const string Marker = "MKWCBUILD:"; + private const string StepMarker = "STEP:"; + + /// The fraction the compile step reaches; beyond it, compiler output is a heartbeat. + 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)); +} diff --git a/Launcher/WiiCompiled.Setup/InstallScratchSpace.cs b/Launcher/WiiCompiled.Setup/InstallScratchSpace.cs new file mode 100644 index 0000000..49aa053 --- /dev/null +++ b/Launcher/WiiCompiled.Setup/InstallScratchSpace.cs @@ -0,0 +1,81 @@ +namespace WiiCompiled.Setup; + +/// +/// 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. +/// +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; } + + /// Creates staging beside the installation, suitable even when the install is absent. + public static InstallScratchSpace CreateSibling(string installDirectory, IInstallReporter? reporter = null) => + Create(InstallOperationPaths.GetParent(InstallOperationPaths.NormalizeRoot(installDirectory)), + installDirectory, reporter); + + /// Creates build/update output inside an existing installation. + 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); + } + + /// Deletes scratch left by an interrupted operation. Call only while locked. + 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)}-*"; +} diff --git a/Launcher/WiiCompiled.Setup/InstallTransaction.cs b/Launcher/WiiCompiled.Setup/InstallTransaction.cs new file mode 100644 index 0000000..230050f --- /dev/null +++ b/Launcher/WiiCompiled.Setup/InstallTransaction.cs @@ -0,0 +1,369 @@ +namespace WiiCompiled.Setup; + +internal enum InstallTransactionEntryKind +{ + Directory, + File +} + +/// An exact prepared-path to live-path replacement made by an install transaction. +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); +} + +/// +/// 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. +/// +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); + } + + /// Moves every old destination aside and publishes every prepared entry. + 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; + } + } + + /// + /// 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 and before the first config write. + /// + public void RecordRuntimeConfigurationMutation(RuntimeConfigSnapshot snapshot) + { + EnsureActive(TransactionState.Published); + _journal.RuntimeConfigExisted = snapshot.Existed; + _journal.RuntimeConfigContents = snapshot.Contents; + _journal.RuntimeConfigMutationPlanned = true; + WriteJournal(); + } + + /// + /// 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. + /// + public void Commit() + { + EnsureActive(TransactionState.Published); + _journal.State = TransactionState.Committed; + WriteJournal(); + _finished = true; + TryCleanCommitted(_journalPath, _journal, _reporter); + } + + public void Dispose() + { + if (_finished) return; + RollBack(); + } + + /// Recovers the one deterministic journal for an installation. Call only while locked. + 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(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 NormalizeEntries(string root, string workRoot, + IReadOnlyList entries) + { + var normalized = new List(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 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 + } +} diff --git a/Launcher/WiiCompiled.Setup/Installation.cs b/Launcher/WiiCompiled.Setup/Installation.cs new file mode 100644 index 0000000..0b621ff --- /dev/null +++ b/Launcher/WiiCompiled.Setup/Installation.cs @@ -0,0 +1,591 @@ +namespace WiiCompiled.Setup; + +/// Provenance written by the bundled build script next to every product it produces. +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"; +} + +/// +/// The typed product status of the --check-products contract. These are the only nine answers +/// the backend gives; see docs/WHEELWIZARD_CONTRACT.md. A frontend branches on the status alone. +/// +internal enum ProductStatus +{ + Absent, + + /// The binary matches the installed toolkit and the canonical compile inputs. + Current, + + /// The toolkit that produced the binary is no longer the installed toolkit. + ToolkitChanged, + + /// The canonical Code.pul is not the one this binary statically embeds. + CodePulChanged, + + /// Another translator-consumed Retro Rewind input changed. + CompileInputsChanged, + + /// Only the cached Retro-WFC payload needs repair; no compilation. + PayloadChanged, + + /// + /// The canonical Retro Rewind installation is missing or structurally invalid, so the product + /// can neither be trusted nor rebuilt until the frontend supplies it. + /// + InputsMissing, + + /// + /// The product exists, but the installation cannot prove its provenance or provide the required + /// toolkit. A caller must repair the installation before launching it. + /// + Blocked, + + /// The binary or the support files it needs are missing or unusable. + Broken +} + +/// +/// 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. +/// +internal sealed record ProductState(ProductStatus Status, string Detail) +{ + /// The stable machine-readable status string of the command-line contract. + 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" + }; + + /// Whether a caller must do something before this product may be launched. + public bool ActionRequired => Status is not ProductStatus.Current and not ProductStatus.Absent; +} + +/// +/// 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. +/// +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); + + /// The portable root this installation lives in, or null for an ordinary installation. + public string? PortableRootDirectory => PortableRoot.TryFind(Root); + + /// + /// The runtime configuration this installation reads: <portable root>\UserData\Config.toml + /// when portable, otherwise the shared per-user file. + /// + public string ConfigPath => RuntimeConfiguration.ResolveConfigPath(Root); + + /// The install directory copy of setup that acts as the CLI launcher. + 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); + + /// + /// 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. + /// + public bool IsPresent => Directory.Exists(Root) && + (Exists || File.Exists(BaseExecutable) || File.Exists(RetroExecutable)); + /// + /// 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. + /// + 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(InstallStatePath); + + internal ToolkitState? ReadToolkitState() => JsonState.TryRead(ToolkitStatePath); + + /// + /// 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. + /// + 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 ""; + } + + /// The recorded package identity of the installed Toolkit directory, under the same + /// fail-closed rules as . + 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 + }); + + /// + /// Reads the complete product provenance. A product without the current fingerprint schema is + /// intentionally untrusted rather than guessed from an older, partial provenance document. + /// + public ProductFingerprint? ReadProductFingerprint(string productDirectory, string toolkitFingerprint) + { + var recorded = JsonState.TryRead( + Path.Combine(productDirectory, ProductFingerprint.FileName)); + return recorded is { SchemaVersion: 1 } ? recorded : null; + } + + /// + /// The canonical Retro Rewind install to compare against: explicit --retro-dir if given, else the + /// recorded retro_rewind_root. Reads only compile inputs, so it stays cheap for every status check and launch. + /// + 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; + } + } + + /// The canonical Retro Rewind root the runtime reads its assets from, if one is set. + 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); + } + + /// + /// Everything about the base product except its copied support files. Repair inspects those + /// separately because they are republished from the installed workspace without recompiling. + /// + 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); + } + + /// + /// 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. + /// + 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, ""); + } + + /// + /// Compares the workspace payload cache with the identity this product embeds. + /// carries an observation the operation already paid for: + /// true/false skip the hash, null means nothing is known yet. + /// + 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( + 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; + } + + /// + /// 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. + /// + 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; + } + } + + /// The Retro-WFC payload choice this installation was built with. + 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; + } + } +} diff --git a/Launcher/WiiCompiled.Setup/InstalledLayout.cs b/Launcher/WiiCompiled.Setup/InstalledLayout.cs new file mode 100644 index 0000000..73d8efb --- /dev/null +++ b/Launcher/WiiCompiled.Setup/InstalledLayout.cs @@ -0,0 +1,54 @@ +namespace WiiCompiled.Setup; + +/// +/// 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. +/// +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"; + + /// Payload entry names use forward slashes; the archive is written by tar. + 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); + + /// + /// The pinned offline dependency sources shipped under BuildWorkspace\Dependencies. 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. + /// + public static readonly string[] DependencyNames = + [ + "abseil-cpp", "cppwinrt", "dawn_prebuilt", "fmt", "freetype", "imgui", "native_prebuilt", + "png", "SDL", "sqlite3", "tracy", "xxhash", "zlib", "zstd" + ]; +} + +/// +/// Source-owned files copied verbatim beside every product. One inventory drives identity +/// (), verification, and publication. Renaming +/// an identity key below requires bumping the fingerprint's version salt. +/// +internal static class ProductRuntimeAssets +{ + /// The bootstrap tree, copied from runtime/assets/wii into every product. + public const string SourceBootstrapDirectoryName = "wii"; + public const string ProductBootstrapDirectoryName = "wii_bootstrap"; + + /// Single files, given as their path below runtime/assets and their product name. + 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]); +} diff --git a/Launcher/WiiCompiled.Setup/InstallerEngine.cs b/Launcher/WiiCompiled.Setup/InstallerEngine.cs new file mode 100644 index 0000000..cfdb9d4 --- /dev/null +++ b/Launcher/WiiCompiled.Setup/InstallerEngine.cs @@ -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(); + 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); + } + + /// + /// Whether this operation must replace the installed Toolkit. The shipped translator and translation + /// project are one unit, so samePackageContent alone isn't enough: sameToolkit tracks the compile + /// identity separately, since gating on package identity alone let the two disagree after the v0.2.5 healing salt bump. + /// + internal static bool MustRefreshToolkit(bool sameToolkit, bool samePackageContent, + bool dolphinToolPresent) => + !sameToolkit || !samePackageContent || !dolphinToolPresent; + + private static void AddComponent(List entries, string staging, + string installDirectory, string name) => + entries.Add(InstallTransactionEntry.Directory(Path.Combine(staging, name), + Path.Combine(installDirectory, name))); + + /// + /// 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. + /// + 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(); + + 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 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)); + } + + /// + /// Publishes the caller's component entries together with the release metadata every install and + /// update advances, then applies the runtime configuration this installation owns. + /// + private void Publish(string staging, string installDirectory, + string? canonicalRetroRoot, InstallState state, + List 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; + } + + /// + /// 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 retro_rewind_root. + /// + 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 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"); + } +} diff --git a/Launcher/WiiCompiled.Setup/JsonState.cs b/Launcher/WiiCompiled.Setup/JsonState.cs new file mode 100644 index 0000000..5b51fc6 --- /dev/null +++ b/Launcher/WiiCompiled.Setup/JsonState.cs @@ -0,0 +1,27 @@ +using System.Text.Json; + +namespace WiiCompiled.Setup; + +/// Reads and atomically writes the small JSON state documents kept inside an installation. +internal static class JsonState +{ + private static readonly JsonSerializerOptions ReadOptions = new() { PropertyNameCaseInsensitive = true }; + private static readonly JsonSerializerOptions WriteOptions = new() { WriteIndented = true }; + + public static T? TryRead(string path) where T : class + { + try + { + return File.Exists(path) ? JsonSerializer.Deserialize(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(string path, T value) => + FileSystemUtilities.WriteAtomic(path, JsonSerializer.Serialize(value, WriteOptions)); +} diff --git a/Launcher/WiiCompiled.Setup/LocalBuildService.cs b/Launcher/WiiCompiled.Setup/LocalBuildService.cs new file mode 100644 index 0000000..439c594 --- /dev/null +++ b/Launcher/WiiCompiled.Setup/LocalBuildService.cs @@ -0,0 +1,197 @@ +using System.Diagnostics; + +namespace WiiCompiled.Setup; + +/// +/// 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. +/// +internal enum BuildProfile { Base, RetroRewind, Both } + +internal sealed class LocalBuildService +{ + private readonly IInstallReporter _reporter; + + public LocalBuildService(IInstallReporter reporter) => _reporter = reporter; + + /// + /// Runs the bundled translate-and-compile script against , which + /// holds a complete Toolkit and BuildWorkspace pair - either an installation being + /// staged or an existing installation being repaired in place. + /// + 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 + { + "-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."); + } + } + + /// + /// Records what this product was built from: ties it to the + /// translator/workspace, to Retro Rewind's embedded Code.pul, so a + /// later launch or update can tell whether the binary is still correct. + /// + 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( + 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); + } +} diff --git a/Launcher/WiiCompiled.Setup/Models.cs b/Launcher/WiiCompiled.Setup/Models.cs new file mode 100644 index 0000000..0ba2fae --- /dev/null +++ b/Launcher/WiiCompiled.Setup/Models.cs @@ -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; } + + /// + /// The canonical Retro Rewind installation Wheel Wizard owns. Its resolved path is recorded as + /// retro_rewind_root; only its compile inputs are ever snapshotted. + /// + public string? RetroDirectoryPath { get; init; } + + public RetroWfcPayloadMode RetroWfcPayloadMode { get; init; } + public required string InstallDirectory { get; init; } + + /// + /// Establish a portable installation: the parent of becomes the + /// portable root, gets the portable.txt marker and a UserData directory, and all + /// runtime user state and [paths] settings stay inside it. + /// + 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; } = ""; + + /// + /// Content identities computed once at release-build time (Build-Installer.ps1 --emit-payload-identities) + /// so install doesn't re-hash gigabytes per user; compilation alone re-derives the toolkit identity from disk. + /// + public string ToolkitFingerprint { get; set; } = ""; + public string ToolkitPackageFingerprint { get; set; } = ""; + public string RuntimeAssetsFingerprint { get; set; } = ""; + + /// + /// Cache-reuse subsets of . Empty in a pre-0.2.0 manifest, + /// which every consumer treats as "changed" and therefore as a full rebuild - never as reuse. + /// + public string TranslationFingerprint { get; set; } = ""; + public string NativeToolchainFingerprint { get; set; } = ""; +} + +/// +/// One validated, content-identified download in operation-owned scratch space. Callers use this +/// exact directory for both the update decision and any resulting build. +/// +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; + + /// + /// The setup version that produced this installation. This is the field WheelWizard compares + /// against the latest published v* release tag; see docs/WHEELWIZARD_CONTRACT.md. + /// + public string SetupVersion { get; set; } = ProductInfo.Version; + + public string ProductVersion { get; set; } = ProductInfo.Version; + + /// The directory this state describes, so a frontend can confirm what it found. + 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; } + + /// + /// The RetroRewind6 directory this installation was pointed at. The runtime's actual copy is + /// [paths] retro_rewind_root; this mirror lets uninstall drop only the setting it owns. + /// + public string RetroRewindRoot { get; set; } = ""; +} + +/// +/// Written next to every locally produced product (Base, RetroRewind): the exact inputs +/// it was built from, so launch/update can decide without re-running anything whether it's still correct. +/// +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; } = ""; + /// + /// 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. + /// + public string ExecutableSha256 { get; set; } = ""; + public string CodePulSha256 { get; set; } = ""; + /// + /// 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. + /// + 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"; +} + +/// +/// 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 in O(1). +/// +internal sealed class ToolkitState +{ + [JsonRequired] + public int SchemaVersion { get; set; } = 2; + public string ToolkitFingerprint { get; set; } = ""; + public string ToolkitPackageFingerprint { get; set; } = ""; + /// Identity of the runtime assets copied verbatim beside each product. + public string RuntimeAssetsFingerprint { get; set; } = ""; + /// Cache-reuse subsets of the compile identity; empty on a pre-0.2.0 installation. + 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"; +} diff --git a/Launcher/WiiCompiled.Setup/PayloadArchive.cs b/Launcher/WiiCompiled.Setup/PayloadArchive.cs new file mode 100644 index 0000000..3fb9055 --- /dev/null +++ b/Launcher/WiiCompiled.Setup/PayloadArchive.cs @@ -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(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(); + } +} diff --git a/Launcher/WiiCompiled.Setup/PortableRoot.cs b/Launcher/WiiCompiled.Setup/PortableRoot.cs new file mode 100644 index 0000000..eab3dcd --- /dev/null +++ b/Launcher/WiiCompiled.Setup/PortableRoot.cs @@ -0,0 +1,143 @@ +namespace WiiCompiled.Setup; + +/// +/// A portable installation is a self-contained directory tree the user can move or carry on removable media: +/// +/// <root>\portable.txt marker; its contents are irrelevant +/// <root>\Install\ the installation directory +/// <root>\UserData\ runtime user state (Config.toml, NAND, Cache, Logs) +/// +/// The runtime finds the same root independently (runtime/include/runtime_config.h, +/// PortableRootDirectory); this class must keep the same marker name, layout, and depth bound. +/// +internal static class PortableRoot +{ + public const string MarkerFileName = "portable.txt"; + public const string UserDataDirectoryName = "UserData"; + public const string InstallDirectoryName = "Install"; + + /// + /// Parents searched above the start directory. Kept small (installs sit 2 levels below root, + /// <root>\Install\Base\game.exe) so an unrelated marker far up a drive can't capture it. + /// + public const int MaximumSearchDepth = 4; + + /// + /// The portable root belongs to, or null otherwise. Callers pass the + /// installation directory (not the running executable's location), since a downloaded setup runs from Downloads. + /// + 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); + + /// Whether is or lives inside it. + public static bool Contains(string root, string candidate) => + FileSystemUtilities.PathContains(root, candidate); + + /// + /// 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. + /// + 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; + } + + /// + /// 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 . + /// + public static void EnsureUsableRoot(string root) => + FileSystemUtilities.EnsureUsableLocation(root, "A portable installation root"); + + private static string Normalize(string path) => FileSystemUtilities.NormalizePath(path); +} + +/// +/// A portable root can be moved or renamed between operations. Every installed-host operation that +/// reads install-state.json passes through here first so exactly one place decides what a +/// moved installation means, and so a non-portable installation is never touched. +/// +internal static class PortableInstallHealing +{ + /// + /// 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. + /// + 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; + } +} diff --git a/Launcher/WiiCompiled.Setup/ProductRepairService.cs b/Launcher/WiiCompiled.Setup/ProductRepairService.cs new file mode 100644 index 0000000..57769d1 --- /dev/null +++ b/Launcher/WiiCompiled.Setup/ProductRepairService.cs @@ -0,0 +1,646 @@ +namespace WiiCompiled.Setup; + +/// +/// Reconciles installed products against the canonical Retro Rewind install Wheel Wizard owns: the +/// toolkit compiles, the source only contributes inputs and its path (retro_rewind_root). +/// +internal sealed class ProductRepairService +{ + internal enum RetroWfcPayloadAction { None, PublishCacheOnly, RebuildRetro } + + /// + /// 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. + /// + internal sealed record ReconcileOptions + { + /// Operation scratch the caller owns, or null to own one for this call. + public string? ScratchRoot { get; init; } + + /// The release's authoritative clean-disc inputs, supplied together or not at all. + public string? ExpectedDolSha256 { get; init; } + public string? ExpectedRelSha256 { get; init; } + + /// + /// The frontend's own canonical RetroRewind6 directory, recorded as + /// retro_rewind_root. Required by every Retro Rewind operation: the compile-input + /// snapshot lives in operation scratch, so it can never be the durable value. + /// + public string? CanonicalRetroRewindRoot { get; init; } + } + + internal sealed record ReconciliationResult( + RetroRewindCompileInputs? CompileInputs, + bool PublicationCommitted, + /// + /// 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. + /// + bool? CachedRetroWfcPayloadMatches); + + private readonly Installation _installation; + private readonly IInstallReporter _reporter; + + public ProductRepairService(Installation installation, IInstallReporter reporter) + { + _installation = installation; + _reporter = reporter; + } + + /// + /// Repairs the base product and any copy-only drift the installation can fix from its own + /// authoritative workspace. The caller must hold . + /// + public Task RepairBaseAsync(ReconcileOptions options, + CancellationToken cancellationToken = default) => + ReconcileAsync(null, RetroWfcPayloadMode.NotApplicable, options, cancellationToken); + + /// + /// Reconciles Retro Rewind against one immutable compile-input snapshot of the canonical + /// installation, recompiling only what those inputs actually change. + /// + public Task 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 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('.')})."); + } + } + + /// The statuses that can only be resolved by producing the product again. + 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; } + } + + /// Everything one reconciliation decided to publish as a single transaction. + 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(); + 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 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")); + } +} diff --git a/Launcher/WiiCompiled.Setup/Program.cs b/Launcher/WiiCompiled.Setup/Program.cs new file mode 100644 index 0000000..7863336 --- /dev/null +++ b/Launcher/WiiCompiled.Setup/Program.cs @@ -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"; + + /// + /// 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. + /// + 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"); +} diff --git a/Launcher/WiiCompiled.Setup/RetroRewindSource.cs b/Launcher/WiiCompiled.Setup/RetroRewindSource.cs new file mode 100644 index 0000000..75ea9b8 --- /dev/null +++ b/Launcher/WiiCompiled.Setup/RetroRewindSource.cs @@ -0,0 +1,59 @@ +namespace WiiCompiled.Setup; + +/// +/// Resolves the one canonical Retro Rewind install. Wheel Wizard owns and passes it as +/// --retro-dir; the backend only resolves, reads, and records it, never packages or copies it. +/// +internal static class RetroRewindSource +{ + /// + /// Resolves the RetroRewind6 folder from a selection that may be the folder itself or a + /// parent containing exactly one RetroRewind6/Binaries/Code.pul. + /// + 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 { 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.") + }; + } + + /// + /// 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. + /// + 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; + } +} diff --git a/Launcher/WiiCompiled.Setup/RuntimeAssetPublication.cs b/Launcher/WiiCompiled.Setup/RuntimeAssetPublication.cs new file mode 100644 index 0000000..beb4186 --- /dev/null +++ b/Launcher/WiiCompiled.Setup/RuntimeAssetPublication.cs @@ -0,0 +1,46 @@ +namespace WiiCompiled.Setup; + +/// +/// 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. +/// +internal static class RuntimeAssetPublication +{ + public static void AddEntries(List entries, string sourceAssets, + string preparedRoot, IEnumerable<(string Name, string Destination)> products, + string expectedFingerprint, string operationDescription, Action 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))); + } + } +} diff --git a/Launcher/WiiCompiled.Setup/RuntimeConfiguration.cs b/Launcher/WiiCompiled.Setup/RuntimeConfiguration.cs new file mode 100644 index 0000000..cc7a546 --- /dev/null +++ b/Launcher/WiiCompiled.Setup/RuntimeConfiguration.cs @@ -0,0 +1,296 @@ +using System.Globalization; +using System.Text; + +namespace WiiCompiled.Setup; + +internal sealed record RuntimeConfigSnapshot(bool Existed, byte[] Contents); + +/// +/// Reads and writes the runtime's Config.toml. Every entry point takes the file it operates on +/// since its location depends on the installation (portable UserData vs. per-user app data); +/// callers obtain it once via . +/// +internal static class RuntimeConfiguration +{ + public const string ConfigFileName = "Config.toml"; + + /// + /// Ordinal, matching the runtime parser/writer (runtime_config.h) exactly. Case-insensitive + /// matching here would let setup rewrite a [Paths]/Dvd_Root spelling the runtime ignores. + /// + private const StringComparison KeyComparison = StringComparison.Ordinal; + + /// The per-user configuration an ordinary (non-portable) installation shares. + public static string ApplicationDataConfigPath => Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "WiiCompiled", ConfigFileName); + + /// + /// The configuration file that governs . This mirrors + /// RuntimeConfigFile::ResolveConfigPath in the runtime: portable roots win, everything + /// else is per-user application data. + /// + 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); + + /// + /// 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. + /// + internal static void SetRetroRewindRoot(string configPath, string retroRewindRoot) => + SetPath(configPath, "retro_rewind_root", retroRewindRoot); + + /// The canonical Retro Rewind root, or null when no installation has recorded one. + 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); + + /// The raw stored text of a [paths] key, exactly as the file holds it. + internal static string? GetPath(string configPath, string key) => + TryUnquoteToml(GetRawValue(configPath, "paths", key) ?? "", out var value) ? value : null; + + /// + /// A [paths] value as the runtime will actually use it. The runtime resolves every relative + /// [paths] value against the directory holding Config.toml (never the working + /// directory), so the host must resolve it the same way before comparing or reading it. + /// + 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); + } + + /// + /// 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. + /// + 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); + } + + /// The raw TOML literal stored for a key, or null when the section or key is absent. + 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; + } + + /// + /// 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. + /// + private static void SetSectionValue(List 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 ReadLinesOrDefault(string configPath) => + File.Exists(configPath) ? File.ReadAllLines(configPath).ToList() : DefaultConfigLines(); + + private static void WriteLines(string configPath, List lines) => + FileSystemUtilities.WriteAtomic(configPath, + Encoding.UTF8.GetBytes(string.Join(Environment.NewLine, lines) + Environment.NewLine)); + + private static List DefaultConfigLines() => + [ + "# WiiCompiled user configuration", + "", + "[paths]" + ]; + + private static bool IsSection(string value) => + value.Length >= 2 && value[0] == '[' && value[^1] == ']'; + + /// + /// Line-for-line port of RemoveComment in runtime_config.h, including its backslash + /// escaping inside basic strings: without it a Windows path ending in \\" reads as + /// re-opening the quote, and the two implementations disagree about which # starts a comment. + /// + 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; + } + +} diff --git a/Launcher/WiiCompiled.Setup/SelfTests.cs b/Launcher/WiiCompiled.Setup/SelfTests.cs new file mode 100644 index 0000000..cb54093 --- /dev/null +++ b/Launcher/WiiCompiled.Setup/SelfTests.cs @@ -0,0 +1,1494 @@ +using System.Buffers.Binary; +using System.Security.Cryptography; + +namespace WiiCompiled.Setup; + +internal static class SelfTests +{ + public static int Run() + { + var failures = new List(); + Test("ISO extension", () => TestAcceptedExtension(".iso"), failures); + Test("GCM extension", () => TestAcceptedExtension(".gcm"), failures); + Test("GCZ extension", () => TestAcceptedExtension(".gcz"), failures); + Test("CISO extension", () => TestAcceptedExtension(".ciso"), failures); + Test("WBFS extension", () => TestAcceptedExtension(".wbfs"), failures); + Test("WIA extension", () => TestAcceptedExtension(".wia"), failures); + Test("RVZ extension", () => TestAcceptedExtension(".RVZ"), failures); + Test("Unsupported extension", () => + { + var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".nfs"); + File.WriteAllBytes(path, [0]); + try + { + try { InputValidation.ValidateExtension(path); throw new Exception("NFS was accepted."); } + catch (InvalidDataException) { } + } + finally { File.Delete(path); } + }, failures); + Test("Disc compatibility", () => + InputValidation.EnsureCompatibleDisc(new DiscHeader { GameId = "RMCP01" }, + new PayloadManifest { ExpectedGameId = "RMCP01" }), failures); + Test("Disc rejection", () => + { + try + { + InputValidation.EnsureCompatibleDisc(new DiscHeader { GameId = "RMCJ01" }, + new PayloadManifest { ExpectedGameId = "RMCP01" }); + throw new Exception("Wrong region was accepted."); + } + catch (InvalidDataException) { } + }, failures); + Test("Payload timestamp normalization", TestPayloadTimestampNormalization, failures); + Test("Workspace timestamp reconciliation", TestWorkspaceTimestampReconciliation, failures); + Test("Toolkit accompanies the workspace", TestToolkitRefreshDecision, failures); + Test("Retro-WFC payload snapshot reconciliation", TestRetroWfcPayloadSnapshotReconciliation, failures); + Test("Retro-WFC transient retry policy", TestRetroWfcTransientRetryPolicy, failures); + Test("Shared runtime configuration", () => TestPathSettingRoundTrip("dvd_root", + RuntimeConfiguration.SetDvdRoot, RuntimeConfiguration.RemoveDvdRootIfOwned), failures); + Test("Canonical Retro Rewind configuration", () => TestPathSettingRoundTrip("retro_rewind_root", + RuntimeConfiguration.SetRetroRewindRoot, RuntimeConfiguration.RemoveRetroRewindRootIfOwned), + failures); + Test("Runtime configuration key matching", TestRuntimeConfigurationKeyMatching, failures); + Test("Install location validation", TestInstallLocationValidation, failures); + Test("Command line contract", TestCommandLineContract, failures); + Test("Portable root discovery", TestPortableRootDiscovery, failures); + Test("Portable install validation failure leaves no marker", + TestPortableValidationFailureLeavesNoMarker, failures); + Test("Portable configuration path resolution", TestPortableConfigPathResolution, failures); + Test("Portable relative path settings", TestPortableRelativePathSettings, failures); + Test("Portable move healing", TestPortableMoveHealing, failures); + Test("Install transaction rollback", TestInstallTransactionRollback, failures); + Test("Install scratch recovery", TestInstallScratchRecovery, failures); + Test("Shortcut cleanup is best effort", TestShortcutCleanupIsBestEffort, failures); + Test("Canonical Retro Rewind resolution", TestCanonicalRetroRewindResolution, failures); + Test("Retro Rewind compile-inputs golden vector", + TestCompileInputsFingerprintGoldenVector, failures); + Test("Retro Rewind compile-inputs fingerprint", TestCompileInputsFingerprint, failures); + Test("Retro Rewind compile-inputs snapshot", TestCompileInputsSnapshot, failures); + Test("Toolkit fingerprint", TestToolkitFingerprint, failures); + Test("Retro Rewind inspection state machine", TestCodePulStateMachine, failures); + Test("Products report contract", TestProductsReport, failures); + foreach (var failure in failures) Console.Error.WriteLine("FAILED " + failure); + return failures.Count == 0 ? 0 : 1; + } + + private static void TestAcceptedExtension(string extension) + { + var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + extension); + File.WriteAllBytes(path, [0]); + try { InputValidation.ValidateExtension(path); } finally { File.Delete(path); } + } + + private static void TestPayloadTimestampNormalization() + { + var path = Path.Combine(Path.GetTempPath(), "mkwc-timestamp-" + Guid.NewGuid().ToString("N")); + try + { + File.WriteAllText(path, "timestamp"); + File.SetLastWriteTimeUtc(path, DateTime.UtcNow.AddDays(1)); + PayloadArchive.NormalizeExtractedTimestamp(path); + if (File.GetLastWriteTimeUtc(path).Year != 2000) + throw new Exception("Extracted payload timestamps were not normalized."); + } + finally + { + File.Delete(path); + } + } + + private static void TestWorkspaceTimestampReconciliation() + { + var root = Path.Combine(Path.GetTempPath(), "mkwc-wsstamp-" + Guid.NewGuid().ToString("N")); + try + { + var installed = Path.Combine(root, "Install", "BuildWorkspace"); + var staged = Path.Combine(root, "staging", "BuildWorkspace"); + Directory.CreateDirectory(Path.Combine(installed, "runtime", "src")); + Directory.CreateDirectory(Path.Combine(staged, "runtime", "src")); + File.WriteAllText(Path.Combine(installed, "runtime", "src", "same.cpp"), "int same();"); + File.WriteAllText(Path.Combine(staged, "runtime", "src", "same.cpp"), "int same();"); + File.WriteAllText(Path.Combine(installed, "runtime", "src", "edited.cpp"), "int old();"); + File.WriteAllText(Path.Combine(staged, "runtime", "src", "edited.cpp"), "int new_();"); + File.WriteAllText(Path.Combine(staged, "runtime", "src", "added.cpp"), "int added();"); + foreach (var file in Directory.EnumerateFiles(staged, "*", SearchOption.AllDirectories)) + PayloadArchive.NormalizeExtractedTimestamp(file); + + WorkspaceTimestamps.MarkChangedFiles(installed, staged, _ => { }); + + if (File.GetLastWriteTimeUtc(Path.Combine(staged, "runtime", "src", "same.cpp")).Year != 2000) + throw new Exception("An unchanged workspace file lost its normalized timestamp."); + if (File.GetLastWriteTimeUtc(Path.Combine(staged, "runtime", "src", "edited.cpp")).Year == 2000) + throw new Exception("A changed workspace file kept its normalized timestamp; " + + "Ninja would never recompile it."); + if (File.GetLastWriteTimeUtc(Path.Combine(staged, "runtime", "src", "added.cpp")).Year == 2000) + throw new Exception("A new workspace file kept its normalized timestamp."); + + // A fresh installation has no workspace to compare against; the pass must be a no-op. + WorkspaceTimestamps.MarkChangedFiles(Path.Combine(root, "absent"), staged, _ => { }); + } + finally + { + if (Directory.Exists(root)) Directory.Delete(root, recursive: true); + } + } + + /// + /// The translator and the project file it parses must never come from different releases. The + /// first case is the v0.2.5 regression: a compile-identity change republished the workspace + /// while an unchanged package identity retained the older toolkit. + /// + private static void TestToolkitRefreshDecision() + { + if (!InstallerEngine.MustRefreshToolkit(sameToolkit: false, samePackageContent: true, + dolphinToolPresent: true)) + throw new Exception("A republished workspace kept the installed toolkit; the shipped " + + "translator and project file could come from different releases."); + if (!InstallerEngine.MustRefreshToolkit(sameToolkit: true, samePackageContent: false, + dolphinToolPresent: true)) + throw new Exception("Changed toolkit package content was not extracted."); + if (!InstallerEngine.MustRefreshToolkit(sameToolkit: true, samePackageContent: true, + dolphinToolPresent: false)) + throw new Exception("A missing DolphinTool.exe did not force toolkit extraction."); + if (InstallerEngine.MustRefreshToolkit(sameToolkit: true, samePackageContent: true, + dolphinToolPresent: true)) + throw new Exception("An unchanged toolkit was needlessly re-extracted."); + } + + /// + /// Every [paths] key follows the same contract: writing preserves other settings, reads + /// back resolved, and uninstall removes it only while it still points at the recorded location. + /// + private static void TestPathSettingRoundTrip(string key, Action set, + Action removeIfOwned) + { + var root = Path.Combine(Path.GetTempPath(), $"mkwc-{key}-" + Guid.NewGuid().ToString("N")); + var config = Path.Combine(root, "Config.toml"); + var owned = Path.Combine(root, "Owned Location"); + try + { + Directory.CreateDirectory(owned); + File.WriteAllText(config, + "[video]\nwidescreen = true\n\n[paths]\nunrelated_root = \"D:\\\\Elsewhere\"\n"); + set(config, owned); + var updated = File.ReadAllText(config); + if (!updated.Contains("widescreen = true", StringComparison.Ordinal) || + !updated.Contains("unrelated_root = \"D:\\\\Elsewhere\"", StringComparison.Ordinal) || + !updated.Contains(key + " = ", StringComparison.Ordinal)) + throw new Exception($"Writing {key} discarded existing runtime settings."); + + var read = RuntimeConfiguration.GetResolvedPath(config, key); + if (read is null || !read.Equals(Path.GetFullPath(owned), StringComparison.OrdinalIgnoreCase)) + throw new Exception($"The configured {key} did not round-trip."); + + removeIfOwned(config, Path.Combine(root, "Someone Else")); + if (RuntimeConfiguration.GetPath(config, key) is null) + throw new Exception($"A {key} owned by someone else was removed."); + removeIfOwned(config, owned); + if (RuntimeConfiguration.GetPath(config, key) is not null) + throw new Exception($"Uninstall did not remove its owned {key} setting."); + } + finally + { + if (Directory.Exists(root)) Directory.Delete(root, recursive: true); + } + } + + /// + /// TOML keys are case-sensitive and the runtime compares them exactly, so the host must match + /// case too, or it silently writes a key the runtime never reads. + /// + private static void TestRuntimeConfigurationKeyMatching() + { + var root = Path.Combine(Path.GetTempPath(), "mkwc-tomlcase-" + Guid.NewGuid().ToString("N")); + var config = Path.Combine(root, RuntimeConfiguration.ConfigFileName); + var owned = Path.Combine(root, "Owned Location"); + try + { + Directory.CreateDirectory(owned); + File.WriteAllText(config, "[Paths]\r\nDvd_Root = \"D:\\\\Wrong\"\r\n"); + RuntimeConfiguration.SetDvdRoot(config, owned); + if (!File.ReadAllText(config).Contains("Dvd_Root = \"D:\\\\Wrong\"", StringComparison.Ordinal)) + throw new Exception("A differently-cased key the runtime ignores was rewritten."); + var written = RuntimeConfiguration.GetResolvedPath(config, "dvd_root"); + if (written is null || !written.Equals(Path.GetFullPath(owned), StringComparison.OrdinalIgnoreCase)) + throw new Exception("dvd_root was not written as the exactly-cased key the runtime reads."); + + // An escaped quote does not end the value, so the '#' after it is data rather than the + // start of a comment - exactly what RemoveComment in runtime_config.h decides. + File.WriteAllText(config, "[paths]\r\ndvd_root = \"a\\\"#b\"\r\n"); + if (RuntimeConfiguration.GetPath(config, "dvd_root") != "a\"#b") + throw new Exception("A '#' inside an escaped quoted value was treated as a comment."); + } + finally + { + if (Directory.Exists(root)) Directory.Delete(root, recursive: true); + } + } + + /// + /// One location rule set covers both an installation directory and a portable root, so the + /// rejections a portable install performs are the ones an ordinary install performs too. + /// + private static void TestInstallLocationValidation() + { + void Rejects(string description, string path) + { + try + { + FileSystemUtilities.EnsureUsableLocation(path, "The installation folder"); + throw new Exception(description + " was accepted as an install location."); + } + catch (InvalidOperationException) { } + } + + FileSystemUtilities.EnsureUsableLocation("C:\\Games\\WiiCompiled\\Install", "The installation folder"); + Rejects("A network path", "\\\\server\\share\\WiiCompiled"); + Rejects("An over-long path", "C:\\" + new string('w', 200)); + Rejects("A drive root", "C:\\"); + Rejects("A folder inside Windows", + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "WiiCompiled")); + Rejects("The user profile itself", + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); + // The two entry points must not have separate opinions about any of the above. + foreach (var rejected in new[] { "\\\\server\\share\\WiiCompiled", "C:\\" }) + { + try + { + PortableRoot.EnsureUsableRoot(rejected); + throw new Exception($"{rejected} was accepted as a portable root."); + } + catch (InvalidOperationException) { } + } + } + + private static void TestCommandLineContract() + { + void Rejects(string description, params string[] args) + { + try + { + CommandLine.Parse(args); + throw new Exception(description + " was accepted."); + } + catch (ArgumentException) { } + } + + var version = CommandLine.Parse(["--version"]); + if (version.Mode != AppMode.Version) throw new Exception("--version was not recognised."); + + var silent = CommandLine.Parse([ + "--silent", "--game", "game.iso", "--install-dir", "C:\\Games\\MKW", + "--progress-json", "--retro-dir", "D:\\RetroRewind", "--skip-retro-wfc-payload" + ]); + + if (silent.Mode != AppMode.SilentInstall || !silent.ProgressJson || + silent.RetroDirectoryPath != "D:\\RetroRewind" || + silent.RetroWfcPayloadMode != RetroWfcPayloadMode.Skipped) + throw new Exception("The silent install contract did not parse."); + + var launch = CommandLine.Parse(["--launch-base"]); + if (launch.Mode != AppMode.LaunchBase) + throw new Exception("The launch contract did not parse."); + Rejects("runtime argument forwarding", "--launch-base", "--", "--windowed"); + + if (CommandLine.Parse(["--launch-retro"]).Mode != AppMode.LaunchRetro) + throw new Exception("--launch-retro was not recognised."); + + var check = CommandLine.Parse([ + "--check-products", "--install-dir", "C:\\Games\\MKW", "--retro-dir", "D:\\RetroRewind", + "--progress-json" + ]); + if (check.Mode != AppMode.CheckProducts || check.RetroDirectoryPath != "D:\\RetroRewind" || + check.RetroWfcPayloadMode != RetroWfcPayloadMode.NotApplicable) + throw new Exception("The product check contract did not parse."); + + var retroRepair = CommandLine.Parse([ + "--repair-products", "--install-dir", "C:\\Games\\MKW", "--retro-dir", "D:\\RetroRewind", + "--download-retro-wfc-payload", "--progress-json" + ]); + if (retroRepair.Mode != AppMode.RepairProducts || + retroRepair.RetroDirectoryPath != "D:\\RetroRewind" || + retroRepair.RetroWfcPayloadMode != RetroWfcPayloadMode.Online) + throw new Exception("The Retro Rewind repair contract did not parse."); + + Rejects("An unknown Retro Rewind source flag", "--silent", "--game", "g.iso", "--retro-zip", + "a.zip", "--skip-retro-wfc-payload"); + Rejects("--progress-json outside an install", "--launch-base", "--progress-json"); + // The NAND flag is gone: Wheel Wizard writes paths.nand_root into Config.toml itself. + Rejects("The removed NAND flag", "--silent", "--game", "g.iso", "--nand-dir", "D:\\Wii"); + Rejects("A payload option without the canonical Retro Rewind folder", "--silent", "--game", + "g.iso", "--skip-retro-wfc-payload"); + Rejects("A Retro Rewind folder without a payload decision", "--silent", "--game", "g.iso", + "--retro-dir", "b"); + Rejects("Repair without an installation", "--repair-products", "--retro-dir", "rr", + "--skip-retro-wfc-payload", "--progress-json"); + Rejects("Repair without the canonical Retro Rewind folder", "--repair-products", + "--install-dir", "fixed", "--progress-json"); + Rejects("Retro Rewind repair without a payload decision", "--repair-products", "--install-dir", "fixed", + "--retro-dir", "rr", "--progress-json"); + Rejects("A repair payload option without the canonical folder", "--repair-products", + "--install-dir", "fixed", "--skip-retro-wfc-payload", "--progress-json"); + + var portable = CommandLine.Parse([ + "--silent", "--game", "game.iso", "--install-dir", "D:\\Games\\Recomp\\Install", + "--portable", "--progress-json" + ]); + if (portable.Mode != AppMode.SilentInstall || !portable.Portable) + throw new Exception("The portable install contract did not parse."); + + Rejects("--portable without an install directory", "--silent", "--game", "g.iso", "--portable"); + Rejects("--portable outside an install", "--check-products", "--install-dir", "fixed", "--portable"); + // The config verbs are gone: WheelWizard reads and writes Config.toml directly now. + Rejects("The removed configuration read verb", "--get-config"); + Rejects("The removed configuration write verb", "--set-config", "--install-dir", "fixed"); + Rejects("A bare setting assignment", "--check-products", "video.show_fps=true"); + } + + /// + /// The marker walk-up is the one thing both languages must agree on. The depth bound keeps an + /// unrelated marker high up a drive from capturing an ordinary installation. + /// + private static void TestPortableRootDiscovery() + { + var root = Path.Combine(Path.GetTempPath(), "mkwc-portable-" + Guid.NewGuid().ToString("N")); + try + { + var install = Path.Combine(root, "Install"); + var deep = Path.Combine(install, "Base", "config", "a", "b", "c"); + Directory.CreateDirectory(deep); + + if (PortableRoot.TryFind(install) is not null) + throw new Exception("A directory without the marker was treated as portable."); + + File.WriteAllText(Path.Combine(root, PortableRoot.MarkerFileName), "portable"); + if (PortableRoot.TryFind(root) != root) + throw new Exception("The marker directory itself was not recognised as the root."); + if (PortableRoot.TryFind(install) != root) + throw new Exception("The install directory did not resolve to its portable root."); + if (PortableRoot.TryFind(Path.Combine(install, "Base", "config", "a")) != root) + throw new Exception("A path within the depth bound did not resolve to its portable root."); + if (PortableRoot.TryFind(deep) is not null) + throw new Exception("The portable search walked further up than its depth bound."); + + if (!PortableRoot.Contains(root, Path.Combine(install, "GameAssets", "DATA")) || + PortableRoot.Contains(root, Path.Combine(Path.GetTempPath(), "elsewhere"))) + throw new Exception("Portable containment did not classify paths correctly."); + + try + { + PortableRoot.EnsureUsableRoot(Path.GetPathRoot(root)!); + throw new Exception("A drive root was accepted as a portable root."); + } + catch (InvalidOperationException) { } + try + { + PortableRoot.EnsureUsableRoot( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); + throw new Exception("A well-known system folder was accepted as a portable root."); + } + catch (InvalidOperationException) { } + } + finally + { + if (Directory.Exists(root)) Directory.Delete(root, recursive: true); + } + } + + /// + /// A rejected game image must not turn the destination's parent into a portable root. Otherwise + /// the marker walk-up would redirect unrelated later installs into this root's UserData. + /// + private static void TestPortableValidationFailureLeavesNoMarker() + { + var root = Path.Combine(Path.GetTempPath(), "mkwc-portable-rejected-" + Guid.NewGuid().ToString("N")); + var game = Path.Combine(root, "not-a-disc-image.wad"); + try + { + Directory.CreateDirectory(root); + File.WriteAllBytes(game, [0]); + var options = new InstallOptions + { + GamePath = game, + InstallDirectory = Path.Combine(root, PortableRoot.InstallDirectoryName), + Portable = true + }; + + try + { + new InstallerEngine(new DelegatingInstallReporter(_ => { })) + .InstallAsync(options).GetAwaiter().GetResult(); + throw new Exception("An unsupported game image was accepted."); + } + catch (InvalidDataException) { } + + if (File.Exists(Path.Combine(root, PortableRoot.MarkerFileName))) + throw new Exception("A failed portable install left its marker behind."); + if (Directory.Exists(PortableRoot.UserDataDirectory(root))) + throw new Exception("A failed portable install left its UserData directory behind."); + if (PortableRoot.TryFind(options.InstallDirectory) is not null) + throw new Exception("A failed portable install captured later installs under its parent."); + } + finally + { + if (Directory.Exists(root)) Directory.Delete(root, recursive: true); + } + } + + private static void TestShortcutCleanupIsBestEffort() + { + var root = Path.Combine(Path.GetTempPath(), "mkwc-shortcuts-" + Guid.NewGuid().ToString("N")); + var desktop = Path.Combine(root, "Desktop"); + var startMenu = Path.Combine(root, "Start Menu", ProductInfo.Name); + try + { + Directory.CreateDirectory(desktop); + // File.Delete cannot delete a directory. This deterministically forces the first cleanup + // to fail while the remaining independently guarded targets must still be attempted. + Directory.CreateDirectory(Path.Combine(desktop, "WiiCompiled.lnk")); + var secondShortcut = Path.Combine(desktop, "Retro Rewind.lnk"); + File.WriteAllText(secondShortcut, "shortcut"); + Directory.CreateDirectory(startMenu); + File.WriteAllText(Path.Combine(startMenu, "WiiCompiled.lnk"), "shortcut"); + + try + { + ShellIntegration.RemoveAllShortcuts(desktop, startMenu); + throw new Exception("A shortcut deletion failure was not reported."); + } + catch (AggregateException) { } + + if (File.Exists(secondShortcut)) + throw new Exception("One failed shortcut deletion prevented the next file deletion."); + if (Directory.Exists(startMenu)) + throw new Exception("One failed shortcut deletion prevented Start Menu cleanup."); + } + finally + { + if (Directory.Exists(root)) Directory.Delete(root, recursive: true); + } + } + + /// + /// The configuration location is a property of the installation, not of this process: an install + /// under a portable root uses that root's UserData, everything else the shared per-user file. + /// + private static void TestPortableConfigPathResolution() + { + var root = Path.Combine(Path.GetTempPath(), "mkwc-portable-config-" + Guid.NewGuid().ToString("N")); + try + { + var install = Path.Combine(root, "Install"); + Directory.CreateDirectory(install); + if (!RuntimeConfiguration.ResolveConfigPath(install) + .Equals(RuntimeConfiguration.ApplicationDataConfigPath, StringComparison.OrdinalIgnoreCase)) + throw new Exception("A non-portable installation did not use per-user application data."); + + var created = PortableRoot.Create(root); + if (!Directory.Exists(PortableRoot.UserDataDirectory(created)) || + !File.Exists(Path.Combine(created, PortableRoot.MarkerFileName))) + throw new Exception("Creating a portable root did not produce its marker and user data."); + + var expected = Path.Combine(root, PortableRoot.UserDataDirectoryName, "Config.toml"); + if (!RuntimeConfiguration.ResolveConfigPath(install) + .Equals(expected, StringComparison.OrdinalIgnoreCase)) + throw new Exception("A portable installation did not use its own UserData configuration."); + if (!new Installation(install).ConfigPath.Equals(expected, StringComparison.OrdinalIgnoreCase)) + throw new Exception("The installation did not report its portable configuration path."); + } + finally + { + if (Directory.Exists(root)) Directory.Delete(root, recursive: true); + } + } + + /// + /// A portable installation stores every path inside its root relative to the configuration + /// directory, so the whole tree survives being moved. Ownership decisions must therefore compare + /// resolved paths, not stored text. + /// + private static void TestPortableRelativePathSettings() + { + var root = Path.Combine(Path.GetTempPath(), "mkwc-portable-paths-" + Guid.NewGuid().ToString("N")); + try + { + PortableRoot.Create(root); + var install = Path.Combine(root, "Install"); + var config = Path.Combine(root, PortableRoot.UserDataDirectoryName, "Config.toml"); + var dvdRoot = Path.Combine(install, "GameAssets", "DATA"); + var outsideRetro = Path.Combine(Path.GetTempPath(), "mkwc-outside-" + Guid.NewGuid().ToString("N")); + var insideRetro = Path.Combine(root, "RetroRewind6"); + + RuntimeConfiguration.SetDvdRoot(config, dvdRoot); + var stored = RuntimeConfiguration.GetPath(config, "dvd_root"); + if (stored is null || Path.IsPathRooted(stored)) + throw new Exception("A path inside the portable root was stored absolute."); + if (stored.Contains('\\', StringComparison.Ordinal)) + throw new Exception("A relative path was not stored with the canonical forward slashes."); + if (!RuntimeConfiguration.GetResolvedPath(config, "dvd_root")! + .Equals(Path.GetFullPath(dvdRoot), StringComparison.OrdinalIgnoreCase)) + throw new Exception("A relative dvd_root did not resolve against the configuration directory."); + + RuntimeConfiguration.SetRetroRewindRoot(config, insideRetro); + if (Path.IsPathRooted(RuntimeConfiguration.GetPath(config, "retro_rewind_root")!)) + throw new Exception("A Retro Rewind root inside the portable root was stored absolute."); + RuntimeConfiguration.SetRetroRewindRoot(config, outsideRetro); + if (!Path.IsPathRooted(RuntimeConfiguration.GetPath(config, "retro_rewind_root")!)) + throw new Exception("A Retro Rewind root outside the portable root was stored relative."); + + RuntimeConfiguration.RemoveDvdRootIfOwned(config, Path.Combine(install, "Elsewhere")); + if (RuntimeConfiguration.GetPath(config, "dvd_root") is null) + throw new Exception("A relative dvd_root owned by someone else was removed."); + RuntimeConfiguration.RemoveDvdRootIfOwned(config, dvdRoot); + if (RuntimeConfiguration.GetPath(config, "dvd_root") is not null) + throw new Exception("A relative dvd_root this installation owns was not removed."); + } + finally + { + if (Directory.Exists(root)) Directory.Delete(root, recursive: true); + } + } + + /// + /// A portable root can be moved. Healing adopts the directory the installation was actually found + /// in and discards the location-bound native build cache - and must never touch a non-portable + /// installation, where the same disagreement is a real problem to report. + /// + private static void TestPortableMoveHealing() + { + var root = Path.Combine(Path.GetTempPath(), "mkwc-move-" + Guid.NewGuid().ToString("N")); + try + { + var install = Path.Combine(root, "Install"); + var nativeBuild = Path.Combine(install, "BuildWorkspace", "native-build"); + Directory.CreateDirectory(nativeBuild); + File.WriteAllText(Path.Combine(nativeBuild, "CMakeCache.txt"), + "CMAKE_HOME_DIRECTORY:INTERNAL=D:/Old/Install/BuildWorkspace/runtime"); + var statePath = Path.Combine(install, "install-state.json"); + JsonState.Write(statePath, new InstallState { InstallDir = "D:\\Old\\Install" }); + + var plain = new Installation(install); + if (PortableInstallHealing.HealMovedInstall(plain)) + throw new Exception("A non-portable installation was healed."); + if (!Directory.Exists(nativeBuild)) + throw new Exception("A non-portable installation lost its native build directory."); + + PortableRoot.Create(root); + var portable = new Installation(install); + if (!PortableInstallHealing.HealMovedInstall(portable)) + throw new Exception("A moved portable installation was not healed."); + var healed = portable.ReadInstallState(); + if (healed is null || !healed.InstallDir.Equals(portable.Root, StringComparison.OrdinalIgnoreCase)) + throw new Exception("Healing did not adopt the actual installation directory."); + if (Directory.Exists(nativeBuild)) + throw new Exception("Healing kept the location-bound native build cache."); + + if (PortableInstallHealing.HealMovedInstall(portable)) + throw new Exception("An installation already at its recorded location was healed again."); + } + finally + { + if (Directory.Exists(root)) Directory.Delete(root, recursive: true); + } + } + + private static void TestCanonicalRetroRewindResolution() + { + var root = Path.Combine(Path.GetTempPath(), "mkwc-retro-" + Guid.NewGuid().ToString("N")); + try + { + var package = Path.Combine(root, "wrapper", "RetroRewind6"); + Directory.CreateDirectory(Path.Combine(package, "Binaries")); + File.WriteAllText(Path.Combine(package, "Binaries", "Code.pul"), "pretend-kamek"); + File.WriteAllText(Path.Combine(package, "version.txt"), "1.2.3"); + + var expected = InputValidation.Sha256File(Path.Combine(package, "Binaries", "Code.pul")); + // The canonical folder may be named directly, or reached through the one permitted + // wrapper level; all three selections resolve to exactly the same installation. + foreach (var selection in new[] { package, Path.Combine(root, "wrapper"), root }) + { + var inputs = CompileInputsFingerprint.Compute(selection); + if (!inputs.RetroRewindRoot.Equals(Path.GetFullPath(package), + StringComparison.OrdinalIgnoreCase)) + throw new Exception($"Selecting {selection} resolved to {inputs.RetroRewindRoot}."); + if (inputs.CodePulSha256 != expected) + throw new Exception($"The Code.pul hash was wrong when selecting {selection}."); + } + + try + { + _ = RetroRewindSource.ResolveRetroRewind6(Path.Combine(package, "Binaries")); + throw new Exception("A folder without Code.pul was accepted."); + } + catch (InvalidDataException) { } + } + finally + { + if (Directory.Exists(root)) Directory.Delete(root, recursive: true); + } + } + + private static void TestRetroWfcPayloadSnapshotReconciliation() + { + // The endpoint is pinned in exactly one place; this test must not restate it. + const string endpoint = InputValidation.CurrentRetroWfcPayloadUri; + const string shaB = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + var root = Path.Combine(Path.GetTempPath(), "mkwc-wfc-snapshot-" + Guid.NewGuid().ToString("N")); + try + { + InputValidation.ValidateRetroWfcPayloadUri(endpoint); + try + { + InputValidation.ValidateRetroWfcPayloadUri(endpoint + "&extra=1"); + throw new Exception("A non-canonical Retro-WFC endpoint was accepted."); + } + catch (InvalidDataException) { } + + var installation = new Installation(root); + Directory.CreateDirectory(installation.RetroDirectory); + var payloadRoot = Path.Combine(root, "download"); + var payloadPath = Path.Combine(payloadRoot, "binary", "payload.RMCPD00.bin"); + Directory.CreateDirectory(Path.GetDirectoryName(payloadPath)!); + using var signingRsa = RSA.Create(2048); + var signingKey = signingRsa.ExportParameters(includePrivateParameters: false); + var bytes = new byte[0x140]; + "WWFC/Payload"u8.CopyTo(bytes); + BinaryPrimitives.WriteUInt32BigEndian(bytes.AsSpan(0x0C), (uint)bytes.Length); + bytes[^1] = 0x5A; + signingRsa.SignData(bytes.AsSpan(0x110), HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1).CopyTo(bytes.AsSpan(0x10)); + File.WriteAllBytes(payloadPath, bytes); + _ = InputValidation.ValidateStagedRetroWfcPayloadDirectory(payloadRoot, signingKey); + try + { + _ = InputValidation.ValidateStagedRetroWfcPayloadDirectory(payloadRoot); + throw new Exception("A payload signed by a foreign key passed the pinned production key."); + } + catch (InvalidDataException) { } + var shaA = InputValidation.ComputeRetroWfcPayloadSha256(payloadRoot, signingKey); + var snapshot = new RetroWfcPayloadSnapshot(payloadRoot, shaA, bytes.Length); + JsonState.Write(Path.Combine(installation.RetroDirectory, ProductFingerprint.FileName), + new ProductFingerprint + { + Profile = "retro-rewind", ToolkitFingerprint = "toolkit", + RetroWfcPayloadMode = "downloaded", RetroWfcPayloadSha256 = shaA, + RetroWfcPayloadLength = bytes.Length + }); + JsonState.Write(installation.InstallStatePath, new InstallState + { + InstallDir = root, RetroWfcPayloadMode = "downloaded", + RetroWfcPayloadSha256 = shaA, RetroWfcPayloadLength = bytes.Length + }); + if (!installation.MatchesRetroWfcPayloadSnapshot("toolkit", snapshot)) + throw new Exception("The same downloaded payload snapshot did not produce a no-op."); + if (ProductRepairService.PlanRetroWfcPayloadReconciliation(productMatchesSnapshot: true, + cacheMatchesSnapshot: true) != ProductRepairService.RetroWfcPayloadAction.None) + throw new Exception("The same downloaded snapshot did not remain a no-op."); + if (ProductRepairService.PlanRetroWfcPayloadReconciliation(productMatchesSnapshot: true, + cacheMatchesSnapshot: false) != + ProductRepairService.RetroWfcPayloadAction.PublishCacheOnly) + throw new Exception("A damaged local payload cache requested an unnecessary recompile."); + + var changedSnapshot = snapshot with { Sha256 = shaB }; + if (installation.MatchesRetroWfcPayloadSnapshot("toolkit", changedSnapshot)) + throw new Exception("A changed downloaded payload snapshot did not require recompilation."); + if (ProductRepairService.PlanRetroWfcPayloadReconciliation(productMatchesSnapshot: false, + cacheMatchesSnapshot: false) != ProductRepairService.RetroWfcPayloadAction.RebuildRetro) + throw new Exception("A changed payload did not rebuild Retro Rewind."); + + bytes[0x0F]++; + File.WriteAllBytes(payloadPath, bytes); + try + { + _ = InputValidation.ValidateStagedRetroWfcPayloadDirectory(payloadRoot, signingKey); + throw new Exception("A payload with a malformed declared length was accepted."); + } + catch (InvalidDataException) { } + + bytes[0x0F]--; + bytes[^1] ^= 1; + File.WriteAllBytes(payloadPath, bytes); + try + { + _ = InputValidation.ValidateStagedRetroWfcPayloadDirectory(payloadRoot, signingKey); + throw new Exception("A payload with tampered signed content was accepted."); + } + catch (InvalidDataException) { } + + bytes[^1] ^= 1; + bytes[0] = 0; + File.WriteAllBytes(payloadPath, bytes); + try + { + _ = InputValidation.ValidateStagedRetroWfcPayloadDirectory(payloadRoot, signingKey); + throw new Exception("A payload with a malformed header was accepted."); + } + catch (InvalidDataException) { } + } + finally + { + if (Directory.Exists(root)) Directory.Delete(root, recursive: true); + } + } + + private static void TestRetroWfcTransientRetryPolicy() + { + if (!InputValidation.IsTransientRetroWfcDownloadFailure( + new HttpRequestException("Connection reset"), CancellationToken.None)) + throw new Exception("A transport failure would not receive its one retry."); + if (!InputValidation.IsTransientRetroWfcDownloadFailure( + new HttpRequestException("Service unavailable", null, + System.Net.HttpStatusCode.ServiceUnavailable), CancellationToken.None)) + throw new Exception("An HTTP 5xx response would not receive its one retry."); + if (!InputValidation.IsTransientRetroWfcDownloadFailure( + new TimeoutException("Timed out"), CancellationToken.None)) + throw new Exception("A download timeout would not receive its one retry."); + if (InputValidation.IsTransientRetroWfcDownloadFailure( + new HttpRequestException("Not found", null, System.Net.HttpStatusCode.NotFound), + CancellationToken.None)) + throw new Exception("A permanent HTTP 4xx response would be retried."); + if (InputValidation.IsTransientRetroWfcDownloadFailure( + new InvalidDataException("Malformed payload"), CancellationToken.None)) + throw new Exception("A malformed payload would be retried."); + + using var cancelled = new CancellationTokenSource(); + cancelled.Cancel(); + if (InputValidation.IsTransientRetroWfcDownloadFailure( + new TimeoutException("Cancelled"), cancelled.Token)) + throw new Exception("Caller cancellation would be retried."); + } + + private static void TestInstallTransactionRollback() + { + var parent = Path.Combine(Path.GetTempPath(), + "mkwc-transaction-self-test-" + Guid.NewGuid().ToString("N")); + var installRoot = Path.Combine(parent, "install"); + var liveComponent = Path.Combine(installRoot, "Component"); + var preparedComponent = Path.Combine(parent, "prepared-component"); + var preparedFile = Path.Combine(parent, "prepared-added.json"); + var addedFile = Path.Combine(installRoot, "added.json"); + try + { + Directory.CreateDirectory(Path.Combine(liveComponent, "nested")); + File.WriteAllText(Path.Combine(liveComponent, "nested", "original.txt"), "original-bytes"); + File.WriteAllText(Path.Combine(installRoot, "untouched.txt"), "untouched-bytes"); + Directory.CreateDirectory(preparedComponent); + File.WriteAllText(Path.Combine(preparedComponent, "replacement.txt"), "replacement-bytes"); + File.WriteAllText(preparedFile, "new-live-file"); + + using (var transaction = InstallTransaction.Begin(installRoot, null, + InstallTransactionEntry.Directory(preparedComponent, liveComponent), + InstallTransactionEntry.File(preparedFile, addedFile))) + { + transaction.Publish(); + if (!File.Exists(Path.Combine(liveComponent, "replacement.txt")) || + File.Exists(Path.Combine(liveComponent, "nested", "original.txt")) || + File.ReadAllText(addedFile) != "new-live-file") + throw new Exception("The transaction did not publish its prepared replacements."); + // Deliberately omit Commit. Dispose must restore the exact pre-transaction tree. + } + + var original = Path.Combine(liveComponent, "nested", "original.txt"); + if (!File.Exists(original) || File.ReadAllText(original) != "original-bytes" || + File.Exists(Path.Combine(liveComponent, "replacement.txt")) || File.Exists(addedFile) || + Directory.Exists(preparedComponent) || File.Exists(preparedFile) || + File.ReadAllText(Path.Combine(installRoot, "untouched.txt")) != "untouched-bytes") + throw new Exception("Disposing an uncommitted transaction did not restore the exact original tree."); + if (File.Exists(InstallOperationPaths.GetJournalPath(installRoot))) + throw new Exception("A rolled-back transaction left its journal behind."); + } + finally + { + if (Directory.Exists(parent)) Directory.Delete(parent, recursive: true); + } + } + + private static void TestInstallScratchRecovery() + { + var parent = Path.Combine(Path.GetTempPath(), + "mkwc-scratch-self-test-" + Guid.NewGuid().ToString("N")); + var installRoot = Path.Combine(parent, "install"); + var otherInstallRoot = Path.Combine(parent, "other-install"); + InstallScratchSpace? scratch = null; + InstallScratchSpace? otherScratch = null; + try + { + Directory.CreateDirectory(installRoot); + Directory.CreateDirectory(otherInstallRoot); + scratch = InstallScratchSpace.CreateSibling(installRoot); + File.WriteAllText(Path.Combine(scratch.Root, "owned.txt"), "owned"); + + // Scratch is keyed by the installation's scope id, so a neighbouring installation's + // scratch lives beside it under a different prefix and must survive this recovery. + otherScratch = InstallScratchSpace.CreateSibling(otherInstallRoot); + var sentinel = Path.Combine(otherScratch.Root, "preserve.txt"); + File.WriteAllText(sentinel, "preserve"); + + using (InstallOperationLock.Acquire(installRoot)) + { + if (Directory.Exists(scratch.Root)) + throw new Exception("Lock acquisition did not recover this installation's scratch space."); + if (!File.Exists(sentinel) || File.ReadAllText(sentinel) != "preserve") + throw new Exception("Scratch recovery removed another installation's scratch space."); + } + } + finally + { + scratch?.Dispose(); + otherScratch?.Dispose(); + if (Directory.Exists(parent)) Directory.Delete(parent, recursive: true); + } + } + + private static void TestCompileInputsFingerprint() + { + var root = Path.Combine(Path.GetTempPath(), "mkwc-compile-inputs-" + Guid.NewGuid().ToString("N")); + try + { + var package = Path.Combine(root, "RetroRewind6"); + Directory.CreateDirectory(Path.Combine(package, "Binaries")); + Directory.CreateDirectory(Path.Combine(package, "RaceAssets")); + File.WriteAllText(Path.Combine(package, "Binaries", "Code.pul"), "code-A"); + File.WriteAllText(Path.Combine(package, "RaceAssets", "tracks.txt"), "assets-A"); + + var first = CompileInputsFingerprint.Compute(package); + var throughParent = CompileInputsFingerprint.Compute(root); + if (first.CodePulSha256 != throughParent.CodePulSha256 || + first.CompileInputsSha256 != throughParent.CompileInputsSha256) + throw new Exception("The compile identity changed when selected through its parent folder."); + if (first.CompileInputsSha256 != CompileInputsFingerprint.ComputeCompileInputsSha256(package)) + throw new Exception("The compile-input hash is not deterministic."); + + // The runtime reads assets live from the canonical installation, so an asset-only Retro + // Rewind update must not disturb any identity the backend keeps. + File.WriteAllText(Path.Combine(package, "RaceAssets", "tracks.txt"), "assets-B"); + Directory.CreateDirectory(Path.Combine(package, "RaceAssets", "empty-runtime-directory")); + var assetsChanged = CompileInputsFingerprint.Compute(package); + if (assetsChanged.CodePulSha256 != first.CodePulSha256 || + assetsChanged.CompileInputsSha256 != first.CompileInputsSha256) + throw new Exception("An asset-only Retro Rewind update changed a compile identity."); + + // The registered mod directories affect compilation even when empty. + var files = Path.Combine(package, "files"); + Directory.CreateDirectory(files); + var filesTopology = CompileInputsFingerprint.Compute(package); + if (filesTopology.CompileInputsSha256 == assetsChanged.CompileInputsSha256) + throw new Exception("The top-level files directory topology was not part of compile identity."); + File.WriteAllText(Path.Combine(files, "runtime-asset.bin"), "asset"); + if (CompileInputsFingerprint.Compute(package).CompileInputsSha256 != filesTopology.CompileInputsSha256) + throw new Exception("A normal files asset incorrectly triggered a recompile."); + + // Only Code.pul and the recorded file layout should trigger recompilation. + File.WriteAllText(Path.Combine(package, "unrelated.cpp"), "// ignored"); + var straySource = CompileInputsFingerprint.Compute(package); + if (straySource.CodePulSha256 != filesTopology.CodePulSha256 || + straySource.CompileInputsSha256 != filesTopology.CompileInputsSha256) + throw new Exception("A stray source file changed a compile identity."); + + var codePulPath = Path.Combine(package, "Binaries", "Code.pul"); + var linkTarget = Path.Combine(package, "Binaries", "Code.pul.copy"); + File.WriteAllText(linkTarget, "linked"); + File.Delete(codePulPath); + if (TryCreateFileLink(codePulPath, linkTarget)) + { + try + { + _ = CompileInputsFingerprint.Compute(package); + throw new Exception("A linked compile input was accepted into the compile identity."); + } + catch (InvalidDataException) { } + finally + { + File.Delete(codePulPath); + } + } + + File.WriteAllText(codePulPath, "code-B"); + var codeChanged = CompileInputsFingerprint.Compute(package); + if (codeChanged.CodePulSha256 == assetsChanged.CodePulSha256 || + codeChanged.CompileInputsSha256 == assetsChanged.CompileInputsSha256) + throw new Exception("A Code.pul update did not change both compile identities."); + } + finally + { + if (Directory.Exists(root)) Directory.Delete(root, recursive: true); + } + } + + /// + /// A build snapshot must copy megabytes of compile inputs, not the multi-gigabyte asset tree, + /// and must be provably the same inputs the canonical installation had when it was read. + /// + private static void TestCompileInputsSnapshot() + { + var root = Path.Combine(Path.GetTempPath(), "mkwc-snapshot-" + Guid.NewGuid().ToString("N")); + try + { + var package = Path.Combine(root, "RetroRewind6"); + Directory.CreateDirectory(Path.Combine(package, "Binaries")); + Directory.CreateDirectory(Path.Combine(package, "files", "Race", "Course")); + Directory.CreateDirectory(Path.Combine(package, "PulsarEngine", "MarioKartWii")); + File.WriteAllText(Path.Combine(package, "Binaries", "Code.pul"), "code-A"); + File.WriteAllText(Path.Combine(package, "files", "Race", "Course", "beginner_course.szs"), + "a pretend multi-gigabyte asset"); + File.WriteAllText(Path.Combine(package, "PulsarEngine", "MarioKartWii", "patch.cpp"), "int patch;"); + + var source = CompileInputsFingerprint.Compute(package); + var snapshot = CompileInputsFingerprint.Snapshot(package, Path.Combine(root, "scratch")); + if (snapshot.CodePulSha256 != source.CodePulSha256 || + snapshot.CompileInputsSha256 != source.CompileInputsSha256) + throw new Exception("The compile-input snapshot is not identical to its source."); + // The mod root name becomes the product's relative DVD overlay root, so it is contractual. + if (Path.GetFileName(snapshot.RetroRewindRoot) != "RetroRewind6") + throw new Exception("The compile-input snapshot is not named RetroRewind6."); + if (!File.Exists(Path.Combine(snapshot.RetroRewindRoot, "Binaries", "Code.pul"))) + throw new Exception("The compile-input snapshot is missing the Code.pul."); + if (File.Exists(Path.Combine(snapshot.RetroRewindRoot, "PulsarEngine", "MarioKartWii", "patch.cpp"))) + throw new Exception("The compile-input snapshot copied Pulsar sources no build consumes."); + if (File.Exists(Path.Combine(snapshot.RetroRewindRoot, "files", "Race", "Course", + "beginner_course.szs"))) + throw new Exception("The compile-input snapshot copied the runtime asset tree."); + if (!Directory.Exists(Path.Combine(snapshot.RetroRewindRoot, "files"))) + throw new Exception("The compile-input snapshot lost the compile-relevant files topology."); + } + finally + { + if (Directory.Exists(root)) Directory.Delete(root, recursive: true); + } + } + + private static void TestCompileInputsFingerprintGoldenVector() + { + const string expected = GoldenCompileInputsDigest; + var root = Path.Combine(Path.GetTempPath(), "mkwc-vector-" + Guid.NewGuid().ToString("N")); + try + { + Directory.CreateDirectory(Path.Combine(root, "Binaries")); + Directory.CreateDirectory(Path.Combine(root, "tracks")); + Directory.CreateDirectory(Path.Combine(root, "empty", "nested")); + File.WriteAllBytes(Path.Combine(root, "Binaries", "Code.pul"), [0x00, 0x01, 0x02, 0xff]); + File.WriteAllBytes(Path.Combine(root, "readme.txt"), + System.Text.Encoding.UTF8.GetBytes("retro-rewind\n")); + File.WriteAllBytes(Path.Combine(root, "tracks", "course.bin"), []); + + var actual = CompileInputsFingerprint.ComputeCompileInputsSha256(root); + if (!actual.Equals(expected, StringComparison.Ordinal)) + throw new Exception($"Compile-inputs golden vector changed: {actual} != {expected}."); + } + finally + { + if (Directory.Exists(root)) Directory.Delete(root, recursive: true); + } + } + + /// + /// The compile-input digest decides whether an installed product must be recompiled, so it is + /// pinned here: changing the hashed set or its encoding must be a deliberate edit of this value. + /// + private const string GoldenCompileInputsDigest = + "19b2ead4f83f030aac110b82ac801605fa81b4ed86f7be6a1b2bb9dd20147184"; + + private static void TestToolkitFingerprint() + { + var root = Path.Combine(Path.GetTempPath(), "mkwc-toolkit-" + Guid.NewGuid().ToString("N")); + try + { + CreateFakeToolkit(root); + var first = ToolkitFingerprint.Compute(root); + if (first != ToolkitFingerprint.Compute(root)) + throw new Exception("The toolkit fingerprint is not stable."); + + // A file that has nothing to do with generated code must not invalidate every install. + File.WriteAllText(Path.Combine(root, "Toolkit", "DolphinTool.exe"), "irrelevant"); + if (ToolkitFingerprint.Compute(root) != first) + throw new Exception("An unrelated toolkit file changed the fingerprint."); + + File.WriteAllText(Path.Combine(root, "Toolkit", "Translator", "Translator.Cli.exe"), "translator-v2"); + var afterTranslator = ToolkitFingerprint.Compute(root); + if (afterTranslator == first) + throw new Exception("A new translator did not change the fingerprint."); + + File.WriteAllText(Path.Combine(root, "BuildWorkspace", "runtime", "src", "main.cpp"), "int main2();"); + var afterRuntimeSource = ToolkitFingerprint.Compute(root); + if (afterRuntimeSource == afterTranslator) + throw new Exception("A runtime source change did not change the fingerprint."); + + var patch = Path.Combine(root, "BuildWorkspace", "aurora-main", "cmake", "patches", + "sdl3.patch"); + Directory.CreateDirectory(Path.GetDirectoryName(patch)!); + File.WriteAllText(patch, "patch-v1"); + var afterPatch = ToolkitFingerprint.Compute(root); + if (afterPatch == afterRuntimeSource) + throw new Exception("An Aurora patch consumed by the provider did not change compile identity."); + + var installation = new Installation(root); + installation.WriteToolkitState(afterPatch, ToolkitFingerprint.ComputePackage(root), + "wiicompiled-v1", ToolkitFingerprint.ComputeRuntimeAssets(root)); + var repair = new ProductRepairService(installation, new DelegatingInstallReporter(_ => { })); + repair.VerifyToolkitForCompilation(afterPatch); + + void ExpectModifiedToolkitRejected(string description) + { + try + { + repair.VerifyToolkitForCompilation(afterPatch); + throw new Exception(description); + } + catch (InvalidDataException) { } + } + + File.WriteAllText(patch, "patch-v2"); + ExpectModifiedToolkitRejected("A modified mandatory Aurora patch was accepted for compilation."); + File.Delete(patch); + ExpectModifiedToolkitRejected("A deleted mandatory Aurora patch was accepted for compilation."); + File.WriteAllText(patch, "patch-v1"); + repair.VerifyToolkitForCompilation(afterPatch); + + var runtimeAssets = ToolkitFingerprint.ComputeRuntimeAssets(root); + var pipeline = Path.Combine(root, "BuildWorkspace", "runtime", "assets", "pipeline", + "initial_pipeline_cache.db"); + File.WriteAllText(pipeline, "pipeline-v2"); + if (ToolkitFingerprint.Compute(root) != afterPatch) + throw new Exception("A copied-only runtime asset needlessly changed compile identity."); + var changedRuntimeAssets = ToolkitFingerprint.ComputeRuntimeAssets(root); + if (changedRuntimeAssets == runtimeAssets) + throw new Exception("A copied runtime asset did not change its publication identity."); + + var sourceEmptyDirectory = Path.Combine(root, "BuildWorkspace", "runtime", "assets", "wii", + "empty-runtime-directory"); + Directory.CreateDirectory(sourceEmptyDirectory); + var topologyChangedRuntimeAssets = ToolkitFingerprint.ComputeRuntimeAssets(root); + if (topologyChangedRuntimeAssets == changedRuntimeAssets) + throw new Exception("An empty copied runtime-asset directory was omitted from its identity."); + + // Cache-reuse subsets: translator or runtime/src changes (the translator regex-scans it + // for native overrides and effect contracts) invalidate translation but not toolchain, + // since CMake/Ninja rebuild exactly the affected objects; a compiler change invalidates + // only toolchain; a native_prebuilt provenance rewrite invalidates neither. + var components = ToolkitFingerprint.ComputeComponents(root); + if (components.Compile != afterPatch) + throw new Exception("ComputeComponents disagrees with the compile identity."); + File.WriteAllText(Path.Combine(root, "Toolkit", "Translator", "Translator.Cli.exe"), + "translator-v3"); + var afterNewTranslator = ToolkitFingerprint.ComputeComponents(root); + if (afterNewTranslator.Translation == components.Translation) + throw new Exception("A new translator did not change the translation identity."); + if (afterNewTranslator.NativeToolchain != components.NativeToolchain) + throw new Exception("A new translator needlessly changed the native toolchain identity."); + File.WriteAllText(Path.Combine(root, "BuildWorkspace", "runtime", "src", "main.cpp"), + "int main3();"); + var afterNewRuntime = ToolkitFingerprint.ComputeComponents(root); + if (afterNewRuntime.Translation == afterNewTranslator.Translation) + throw new Exception("A runtime source change did not change the translation identity."); + if (afterNewRuntime.NativeToolchain != afterNewTranslator.NativeToolchain) + throw new Exception("A runtime source change needlessly changed the native toolchain identity."); + if (afterNewRuntime.Compile == afterNewTranslator.Compile) + throw new Exception("A runtime source change did not change the compile identity."); + File.WriteAllText(Path.Combine(root, "Toolkit", "llvm-mingw", "bin", "clang-22.exe"), + "clang-v2"); + var afterNewCompiler = ToolkitFingerprint.ComputeComponents(root); + if (afterNewCompiler.NativeToolchain == afterNewRuntime.NativeToolchain) + throw new Exception("A new compiler did not change the native toolchain identity."); + if (afterNewCompiler.Translation != afterNewRuntime.Translation) + throw new Exception("A new compiler needlessly changed the translation identity."); + File.WriteAllText(Path.Combine(root, "BuildWorkspace", "Dependencies", "native_prebuilt", + "provenance.json"), "{\"BuiltUtc\":\"rewritten\"}"); + if (ToolkitFingerprint.Compute(root) != afterNewCompiler.Compile) + throw new Exception( + "A native_prebuilt provenance rewrite needlessly changed the compile identity."); + + var runtimeLink = Path.Combine(root, "BuildWorkspace", "runtime", "assets", "wii", "linked-assets"); + if (TryCreateDirectoryLink(runtimeLink, + Path.Combine(root, "BuildWorkspace", "runtime", "assets", "wii", "shared2"))) + { + try + { + try + { + _ = ToolkitFingerprint.ComputeRuntimeAssets(root); + throw new Exception("A runtime-asset reparse point was accepted into its identity."); + } + catch (InvalidDataException) { } + } + finally + { + Directory.Delete(runtimeLink); + } + } + + var product = Path.Combine(root, "product"); + Directory.CreateDirectory(product); + FileSystemUtilities.CopyDirectory( + Path.Combine(root, "BuildWorkspace", "runtime", "assets", "wii"), + Path.Combine(product, "wii_bootstrap")); + File.Copy(Path.Combine(root, "BuildWorkspace", "runtime", "assets", "dsp", "dsp_coef.bin"), + Path.Combine(product, "dsp_coef.bin")); + File.Copy(pipeline, Path.Combine(product, "initial_pipeline_cache.db")); + if (!ToolkitFingerprint.ProductRuntimeAssetsMatch(product, topologyChangedRuntimeAssets)) + throw new Exception("Copied product assets did not match their normalized source identity."); + Directory.Delete(Path.Combine(product, "wii_bootstrap", "empty-runtime-directory")); + if (ToolkitFingerprint.ProductRuntimeAssetsMatch(product, topologyChangedRuntimeAssets)) + throw new Exception("A missing copied runtime-asset directory was not detected."); + } + finally + { + if (Directory.Exists(root)) Directory.Delete(root, recursive: true); + } + } + + private static void CreateFakeToolkit(string root) + { + Directory.CreateDirectory(Path.Combine(root, "Toolkit", "Translator")); + Directory.CreateDirectory(Path.Combine(root, "Toolkit", "CMake", "bin")); + Directory.CreateDirectory(Path.Combine(root, "Toolkit", "Ninja")); + Directory.CreateDirectory(Path.Combine(root, "Toolkit", "llvm-mingw", "bin")); + Directory.CreateDirectory(Path.Combine(root, "BuildWorkspace", "projects", "mkwii")); + Directory.CreateDirectory(Path.Combine(root, "BuildWorkspace", "runtime", "src")); + Directory.CreateDirectory(Path.Combine(root, "BuildWorkspace", "runtime", "assets", "wii", "shared2", + "wc24")); + Directory.CreateDirectory(Path.Combine(root, "BuildWorkspace", "runtime", "assets", "dsp")); + Directory.CreateDirectory(Path.Combine(root, "BuildWorkspace", "runtime", "assets", "pipeline")); + var aurora = Path.Combine(root, "BuildWorkspace", "aurora-main"); + Directory.CreateDirectory(aurora); + File.WriteAllText(Path.Combine(aurora, "README.md"), "aurora source"); + foreach (var dependency in InstalledLayout.DependencyNames) + { + var directory = Path.Combine(root, "BuildWorkspace", "Dependencies", dependency); + Directory.CreateDirectory(directory); + File.WriteAllText(Path.Combine(directory, ".mkwc-layout"), dependency); + } + File.WriteAllText(Path.Combine(root, "Toolkit", "Translator", "Translator.Cli.exe"), "translator-v1"); + File.WriteAllText(Path.Combine(root, "Toolkit", "CMake", "bin", "cmake.exe"), "cmake"); + File.WriteAllText(Path.Combine(root, "Toolkit", "Ninja", "ninja.exe"), "ninja"); + foreach (var executable in new[] + { + "clang-22.exe", "ld.lld.exe", "x86_64-w64-mingw32-clang.exe", + "x86_64-w64-mingw32-clang++.exe", "x86_64-w64-mingw32-windres.exe" + }) + File.WriteAllText(Path.Combine(root, "Toolkit", "llvm-mingw", "bin", executable), executable); + File.WriteAllText(Path.Combine(root, "Toolkit", "DolphinTool.exe"), "tool"); + File.WriteAllText(Path.Combine(root, "BuildWorkspace", "LocalBuild.ps1"), "# build"); + File.WriteAllText(Path.Combine(root, "BuildWorkspace", "NativeBuildFlags.ps1"), "# flags"); + File.WriteAllText(Path.Combine(root, "BuildWorkspace", "projects", "mkwii", "recomp.yml"), "profiles: {}"); + File.WriteAllText(Path.Combine(root, "BuildWorkspace", "runtime", "CMakeLists.txt"), "cmake_minimum_required(VERSION 3.20)"); + File.WriteAllText(Path.Combine(root, "BuildWorkspace", "runtime", "src", "main.cpp"), "int main();"); + File.WriteAllText(Path.Combine(root, "BuildWorkspace", "runtime", "assets", "wii", "shared2", "wc24", + "bootstrap.bin"), "bootstrap"); + File.WriteAllText(Path.Combine(root, "BuildWorkspace", "runtime", "assets", "dsp", "dsp_coef.bin"), + "dsp"); + File.WriteAllText(Path.Combine(root, "BuildWorkspace", "runtime", "assets", "pipeline", + "initial_pipeline_cache.db"), "pipeline"); + } + + private static void CopyFakeRuntimeAssets(string root, string productDirectory) + { + var source = Path.Combine(root, "BuildWorkspace", "runtime", "assets"); + FileSystemUtilities.CopyDirectory(Path.Combine(source, "wii"), + Path.Combine(productDirectory, "wii_bootstrap")); + File.Copy(Path.Combine(source, "dsp", "dsp_coef.bin"), + Path.Combine(productDirectory, "dsp_coef.bin"), overwrite: true); + File.Copy(Path.Combine(source, "pipeline", "initial_pipeline_cache.db"), + Path.Combine(productDirectory, "initial_pipeline_cache.db"), overwrite: true); + } + + private static bool TryCreateFileLink(string linkPath, string targetPath) + { + try + { + File.CreateSymbolicLink(linkPath, targetPath); + return true; + } + catch (Exception ex) when (ex is UnauthorizedAccessException or IOException or NotSupportedException) + { + return false; + } + } + + private static bool TryCreateDirectoryLink(string linkPath, string targetPath) + { + try + { + Directory.CreateSymbolicLink(linkPath, targetPath); + return true; + } + catch (Exception ex) when (ex is UnauthorizedAccessException or IOException or NotSupportedException) + { + // Developer Mode / symlink permission is not guaranteed on a user's Windows system. + // The identity check is still covered whenever the platform can create a link. + return false; + } + } + + /// + /// The full inspection state machine against the canonical Retro Rewind installation. The + /// installation itself is never copied here, exactly as in production: only its compile inputs + /// are ever read, and asset-only changes to it must leave the product current. + /// + private static void TestCodePulStateMachine() + { + var root = Path.Combine(Path.GetTempPath(), "mkwc-install-" + Guid.NewGuid().ToString("N")); + try + { + CreateFakeToolkit(root); + var installation = new Installation(root); + var fingerprint = ToolkitFingerprint.Compute(root); + installation.WriteToolkitState(fingerprint, ToolkitFingerprint.ComputePackage(root), + "wiicompiled-v1", ToolkitFingerprint.ComputeRuntimeAssets(root)); + + // Wheel Wizard's own Retro Rewind installation, deliberately outside the install root. + var canonical = Path.Combine(root, "canonical", "RetroRewind6"); + Directory.CreateDirectory(Path.Combine(canonical, "Binaries")); + Directory.CreateDirectory(Path.Combine(canonical, "PulsarEngine")); + File.WriteAllText(Path.Combine(canonical, "Binaries", "Code.pul"), "code-pul-A"); + File.WriteAllText(Path.Combine(canonical, "PulsarEngine", "patch.cpp"), "int patch;"); + File.WriteAllText(Path.Combine(canonical, "asset.txt"), "asset-A"); + + Directory.CreateDirectory(installation.BaseDirectory); + Directory.CreateDirectory(installation.RetroDirectory); + File.WriteAllBytes(installation.BaseExecutable, new byte[64 * 1024]); + File.WriteAllBytes(installation.RetroExecutable, new byte[64 * 1024]); + CopyFakeRuntimeAssets(root, installation.BaseDirectory); + CopyFakeRuntimeAssets(root, installation.RetroDirectory); + var gameSys = Path.Combine(installation.GameDataDirectory, "sys"); + var gameRel = Path.Combine(installation.GameDataDirectory, "files", "rel"); + Directory.CreateDirectory(gameSys); + Directory.CreateDirectory(gameRel); + File.WriteAllText(Path.Combine(gameSys, "main.dol"), "dol"); + File.WriteAllText(Path.Combine(gameSys, "fst.bin"), "fst"); + File.WriteAllText(Path.Combine(gameRel, "StaticR.rel"), "rel"); + var dolSha = InputValidation.Sha256File(Path.Combine(gameSys, "main.dol")); + var relSha = InputValidation.Sha256File(Path.Combine(gameRel, "StaticR.rel")); + var inputsA = CompileInputsFingerprint.Compute(canonical); + LocalBuildService.WriteFingerprint(installation.BaseDirectory, BuildProfile.Base, fingerprint, + dolSha, relSha, "", RetroWfcPayloadMode.NotApplicable); + JsonState.Write(Path.Combine(installation.BaseDirectory, LocalBuildProvenance.FileName), + new LocalBuildProvenance { SchemaVersion = 1, Profile = "base", DolSha256 = dolSha, + RelSha256 = relSha }); + JsonState.Write(Path.Combine(installation.RetroDirectory, ProductFingerprint.FileName), + new ProductFingerprint + { + Profile = "retro-rewind", ToolkitFingerprint = fingerprint, DolSha256 = dolSha, RelSha256 = relSha, + ExecutableSha256 = InputValidation.Sha256File(installation.RetroExecutable), + CodePulSha256 = inputsA.CodePulSha256, + RetroRewindCompileInputsSha256 = inputsA.CompileInputsSha256, + RetroWfcPayloadMode = "skipped" + }); + JsonState.Write(Path.Combine(installation.RetroDirectory, LocalBuildProvenance.FileName), + new LocalBuildProvenance { SchemaVersion = 1, Profile = "retro-rewind", DolSha256 = dolSha, + RelSha256 = relSha, CodePulSha256 = inputsA.CodePulSha256, + RetroWfcPayloadMode = "skipped" }); + JsonState.Write(installation.InstallStatePath, new InstallState + { + InstallDir = root, RetroRewindInstalled = true, + DolSha256 = dolSha, RelSha256 = relSha, + RetroRewindCodePulSha256 = inputsA.CodePulSha256, + RetroRewindCompileInputsSha256 = inputsA.CompileInputsSha256, + RetroWfcPayloadMode = "skipped", + RetroRewindRoot = canonical + }); + + ProductState Inspect() => + installation.CheckRetroRewind(installation.ResolveToolkitFingerprint(), + CompileInputsFingerprint.Compute(canonical)); + + void Expect(ProductStatus expected, string description) + { + var inspected = Inspect(); + if (inspected.Status != expected) + throw new Exception( + $"{description}: expected {expected}, got {inspected.Status}: {inspected.Detail}"); + } + + Expect(ProductStatus.Current, "A freshly built Retro Rewind"); + + // The whole point of the canonical-source model: assets change constantly and the + // runtime reads them live, so they must never make the backend rebuild anything. + File.WriteAllText(Path.Combine(canonical, "asset.txt"), "asset-B"); + Directory.CreateDirectory(Path.Combine(canonical, "new-course-directory")); + Expect(ProductStatus.Current, "An asset-only Retro Rewind update"); + + var unreadableCanonical = installation.CheckRetroRewind(fingerprint, null, + "The canonical Retro Rewind folder cannot be read: missing."); + if (unreadableCanonical.Status != ProductStatus.InputsMissing || + !unreadableCanonical.ActionRequired) + throw new Exception("A missing canonical Retro Rewind folder did not fail closed."); + + var unchangedInputs = ProductRepairService.FindDesiredInputDrift(installation, fingerprint, + dolSha, relSha); + if (unchangedInputs.Base || unchangedInputs.Retro) + throw new Exception("Current authoritative game inputs requested an unnecessary rebuild."); + var changedInputs = ProductRepairService.FindDesiredInputDrift(installation, fingerprint, + new string('a', 64), relSha); + if (!changedInputs.Base || !changedInputs.Retro) + throw new Exception("Desired DOL drift did not force both installed products to rebuild."); + + File.Delete(installation.ToolkitStatePath); + if (!string.IsNullOrEmpty(installation.ResolveToolkitFingerprint()) || + Inspect().Status != ProductStatus.Blocked) + throw new Exception("A missing toolkit-state.json was inferred instead of failing closed."); + installation.WriteToolkitState(fingerprint, ToolkitFingerprint.ComputePackage(root), + "wiicompiled-v1", ToolkitFingerprint.ComputeRuntimeAssets(root)); + Expect(ProductStatus.Current, "A restored toolkit-state.json"); + + var cmake = Path.Combine(root, "Toolkit", "CMake", "bin", "cmake.exe"); + File.Delete(cmake); + if (!string.IsNullOrEmpty(installation.ResolveToolkitFingerprint()) || + Inspect().Status != ProductStatus.Blocked) + throw new Exception("A missing bundled build tool did not block product inspection."); + File.WriteAllText(cmake, "cmake"); + Expect(ProductStatus.Current, "A restored bundled build tool"); + + var auroraMarker = Path.Combine(root, "BuildWorkspace", "aurora-main", "README.md"); + File.Delete(auroraMarker); + if (!string.IsNullOrEmpty(installation.ResolveToolkitFingerprint()) || + Inspect().Status != ProductStatus.Blocked) + throw new Exception("A missing Aurora source tree did not block product inspection early."); + File.WriteAllText(auroraMarker, "aurora source"); + + var dependencyMarker = Path.Combine(root, "BuildWorkspace", "Dependencies", "fmt", ".mkwc-layout"); + File.Delete(dependencyMarker); + if (!string.IsNullOrEmpty(installation.ResolveToolkitFingerprint()) || + Inspect().Status != ProductStatus.Blocked) + throw new Exception("An empty required dependency tree did not block product inspection early."); + File.WriteAllText(dependencyMarker, "fmt"); + Expect(ProductStatus.Current, "A restored required toolkit layout"); + + var baseDsp = Path.Combine(installation.BaseDirectory, "dsp_coef.bin"); + File.Delete(baseDsp); + var missingBaseRuntimeAssets = installation.CheckBase(installation.ResolveToolkitFingerprint()); + if (missingBaseRuntimeAssets.Status != ProductStatus.Broken || + installation.CheckBaseCore(installation.ResolveToolkitFingerprint()).Status != + ProductStatus.Current) + throw new Exception("Missing copied base runtime assets were not reported as broken."); + + var retroPipeline = Path.Combine(installation.RetroDirectory, "initial_pipeline_cache.db"); + File.Delete(retroPipeline); + if (Inspect().Status != ProductStatus.Broken) + throw new Exception("Missing copied Retro Rewind runtime assets were not reported as broken."); + + // Copied support files are republished from the installed workspace: repairing them must + // never require the translator or the compiler. + using (InstallOperationLock.Acquire(root)) + { + var repair = new ProductRepairService(installation, new DelegatingInstallReporter(_ => { })); + _ = repair.RepairBaseAsync(new ProductRepairService.ReconcileOptions()) + .GetAwaiter().GetResult(); + } + if (!File.Exists(baseDsp) || + installation.CheckBase(installation.ResolveToolkitFingerprint()).Status != ProductStatus.Current) + throw new Exception("Runtime-only repair did not restore the base support assets and health."); + if (!File.Exists(retroPipeline)) + throw new Exception("Runtime-only repair did not restore the Retro Rewind support assets."); + Expect(ProductStatus.Current, "Runtime-only Retro Rewind repair"); + + // Pulsar sources are no longer compile inputs; only the Code.pul built from them is. + File.WriteAllText(Path.Combine(canonical, "PulsarEngine", "patch.cpp"), "int patched;"); + Expect(ProductStatus.Current, "A changed Pulsar source without a Code.pul change"); + File.WriteAllText(Path.Combine(canonical, "PulsarEngine", "patch.cpp"), "int patch;"); + + File.WriteAllText(Path.Combine(canonical, "Binaries", "Code.pul"), "code-pul-B"); + Expect(ProductStatus.CodePulChanged, "A changed canonical Code.pul"); + File.WriteAllText(Path.Combine(canonical, "Binaries", "Code.pul"), "code-pul-A"); + Expect(ProductStatus.Current, "A restored canonical Code.pul"); + + var alteredExecutable = File.ReadAllBytes(installation.RetroExecutable); + alteredExecutable[0] = 1; + File.WriteAllBytes(installation.RetroExecutable, alteredExecutable); + var tamperedExecutable = Inspect(); + if (tamperedExecutable.Status != ProductStatus.Blocked || !tamperedExecutable.ActionRequired) + throw new Exception("A modified Retro Rewind executable did not fail closed."); + File.WriteAllBytes(installation.RetroExecutable, new byte[64 * 1024]); + Expect(ProductStatus.Current, "A restored Retro Rewind executable"); + + File.Delete(installation.RetroExecutable); + Expect(ProductStatus.Broken, "A missing Retro Rewind executable"); + + File.WriteAllBytes(installation.RetroExecutable, new byte[64 * 1024]); + File.WriteAllText(Path.Combine(root, "Toolkit", "Translator", "Translator.Cli.exe"), "translator-v2"); + installation.WriteToolkitState(ToolkitFingerprint.Compute(root), + ToolkitFingerprint.ComputePackage(root), "wiicompiled-v2", + ToolkitFingerprint.ComputeRuntimeAssets(root)); + Expect(ProductStatus.ToolkitChanged, "A replaced translator"); + if (installation.CheckBase(installation.ResolveToolkitFingerprint()).Status != ProductStatus.ToolkitChanged) + throw new Exception("A replaced translator did not invalidate the base product."); + } + finally + { + if (Directory.Exists(root)) Directory.Delete(root, recursive: true); + } + } + + /// + /// The frontend matches the products line's keys case-sensitively and branches on the typed + /// status alone, so the exact v1 shape is pinned here rather than left to a serializer default. + /// + private static void TestProductsReport() + { + var root = Path.Combine(Path.GetTempPath(), "mkwc-products-" + Guid.NewGuid().ToString("N")); + try + { + // A path nothing was installed to must answer "absent", not fail and not ask for a rebuild. + var missing = ConsoleCommands.InspectProducts(new Installation(root)); + if (missing.RebuildRequired || missing.Base.Reason != "absent" || + missing.RetroRewind.Reason != "absent") + throw new Exception("A missing installation directory was not reported as absent."); + if (Directory.Exists(root)) + throw new Exception("Inspecting a missing installation created the directory."); + + Directory.CreateDirectory(root); + var empty = ConsoleCommands.InspectProducts(new Installation(root)); + if (empty.RebuildRequired || empty.Base.Reason != "absent" || empty.RetroRewind.Reason != "absent") + throw new Exception("An empty installation directory was not reported as absent."); + if (File.Exists(Path.Combine(root, ToolkitState.FileName))) + throw new Exception("Inspecting an empty installation wrote installation state."); + + var json = ConsoleCommands.SerializeProductsReport(empty); + foreach (var key in new[] + { + "\"type\":\"products\"", "\"setupVersion\":", "\"installDir\":", + "\"rebuildRequired\":false", "\"base\":{\"status\":\"absent\",\"detail\":", + "\"retroRewind\":{\"status\":\"absent\",\"detail\":" + }) + if (!json.Contains(key, StringComparison.Ordinal)) + throw new Exception($"The products line is missing {key}: {json}"); + // v1 reports a status and a detail per product and nothing else; a frontend must not be + // able to read a second, possibly disagreeing, opinion out of the same record. + foreach (var removed in new[] { "\"health\"", "\"action\"", "\"compileRequired\"", + "\"packageSyncRequired\"", "\"actionRequired\"" }) + if (json.Contains(removed, StringComparison.Ordinal)) + throw new Exception($"The products line still carries {removed}: {json}"); + foreach (var wrongCase in new[] { "\"Base\"", "\"RetroRewind\"", "\"SetupVersion\"", + "\"InstallDir\"", "\"RebuildRequired\"", "\"Status\"", "\"Detail\"" }) + if (json.Contains(wrongCase, StringComparison.Ordinal)) + throw new Exception($"The products line used PascalCase for {wrongCase}: {json}"); + + foreach (var status in new[] + { + ProductStatus.Absent, ProductStatus.Current, ProductStatus.ToolkitChanged, + ProductStatus.CodePulChanged, ProductStatus.CompileInputsChanged, + ProductStatus.PayloadChanged, ProductStatus.InputsMissing, + ProductStatus.Blocked, ProductStatus.Broken + }) + { + var reason = new ProductState(status, "").Reason; + if (!new[] + { + "absent", "current", "toolkit-changed", "code-pul-changed", + "compile-inputs-changed", "payload-changed", "inputs-missing", "blocked", + "broken" + }.Contains(reason, StringComparer.Ordinal)) + throw new Exception($"{status} reports the undeclared status {reason}."); + } + } + finally + { + if (Directory.Exists(root)) Directory.Delete(root, recursive: true); + } + } + + private static void Test(string name, Action action, List failures) + { + try { action(); } + catch (Exception ex) { failures.Add($"{name}: {ex.Message}"); } + } +} diff --git a/Launcher/WiiCompiled.Setup/ShellIntegration.cs b/Launcher/WiiCompiled.Setup/ShellIntegration.cs new file mode 100644 index 0000000..eeae1de --- /dev/null +++ b/Launcher/WiiCompiled.Setup/ShellIntegration.cs @@ -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); + + /// Removes shortcuts left by GUI-capable releases from before Wheel Wizard owned the UI. + 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(); + 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 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 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; + } + } +} diff --git a/Launcher/WiiCompiled.Setup/ToolkitFingerprint.cs b/Launcher/WiiCompiled.Setup/ToolkitFingerprint.cs new file mode 100644 index 0000000..ded4feb --- /dev/null +++ b/Launcher/WiiCompiled.Setup/ToolkitFingerprint.cs @@ -0,0 +1,286 @@ +using System.Security.Cryptography; +using System.Text; + +namespace WiiCompiled.Setup; + +/// +/// Content identity of everything that decides what the locally produced executables contain. +/// First link of toolkit content -> toolkit-state.json -> per-product build-fingerprint.json: +/// unchanged skips retranslation, any change forces a rebuild. Content-based so re-tags are free. +/// +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"]; + + /// + /// 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. + /// + private static readonly string[] TranslationPrefixes = + [ + InstalledLayout.ToolkitEntryPrefix + "Translator/", + InstalledLayout.WorkspaceEntryPrefix + "projects/", + InstalledLayout.WorkspaceEntryPrefix + "runtime/src/" + ]; + + private static readonly string[] TranslationFiles = + [InstalledLayout.WorkspaceEntryPrefix + "LocalBuild.ps1"]; + + /// + /// 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. + /// + 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" + ]; + + /// + /// Fingerprint of the toolkit at : 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. + /// + public static string Compute(string root, CancellationToken cancellationToken = default) => + ComputeComponents(root, cancellationToken).Compile; + + /// + /// 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. + /// + 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(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 entries, + string[] prefixes, string[] files) + { + var subset = new SortedDictionary(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); + } + + /// + /// Content identity of the source-owned files copied verbatim beside every product. This is + /// intentionally independent of : an asset-only release is published + /// transactionally without translating or compiling unchanged code. + /// + 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(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? 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; + } + } + + /// Checks the actual copied product files against a source runtime-assets identity. + public static bool ProductRuntimeAssetsMatch(string productDirectory, string expectedFingerprint, + CancellationToken cancellationToken = default, Action? diagnostic = null) + { + try + { + var entries = new SortedDictionary(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; + } + } + + /// Identity of the shipped Toolkit directory, including installer-only tools. + public static string ComputePackage(string root, CancellationToken cancellationToken = default) + { + root = Path.GetFullPath(root); + var entries = new SortedDictionary(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); + } + + /// + /// 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. + /// + private static void AddDirectory(SortedDictionary entries, string root, string directory, + string[]? extensions, CancellationToken cancellationToken = default, + Func? include = null) + { + if (!Directory.Exists(directory)) return; + var selected = new List(); + 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 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 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 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 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(); + } +} + +/// +/// Compile identity plus the two subsets deciding cache reuse: is the +/// authoritative freshness identity, decides if the base translation is +/// still current, decides if compiled objects may still be linked. +/// +internal sealed record ToolkitFingerprintComponents(string Compile, string Translation, + string NativeToolchain); diff --git a/Launcher/WiiCompiled.Setup/UninstallService.cs b/Launcher/WiiCompiled.Setup/UninstallService.cs new file mode 100644 index 0000000..a76a9e3 --- /dev/null +++ b/Launcher/WiiCompiled.Setup/UninstallService.cs @@ -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); +} diff --git a/Launcher/WiiCompiled.Setup/WiiCompiled.Setup.csproj b/Launcher/WiiCompiled.Setup/WiiCompiled.Setup.csproj new file mode 100644 index 0000000..9ea644e --- /dev/null +++ b/Launcher/WiiCompiled.Setup/WiiCompiled.Setup.csproj @@ -0,0 +1,17 @@ + + + Exe + net8.0-windows + enable + enable + WiiCompiled.Setup + WiiCompiled.Setup + app.manifest + 0.2.13 + patchzy + WiiCompiled + Command-line installer and launcher for WiiCompiled + embedded + en + + diff --git a/Launcher/WiiCompiled.Setup/WorkspaceTimestamps.cs b/Launcher/WiiCompiled.Setup/WorkspaceTimestamps.cs new file mode 100644 index 0000000..b3a0941 --- /dev/null +++ b/Launcher/WiiCompiled.Setup/WorkspaceTimestamps.cs @@ -0,0 +1,61 @@ +namespace WiiCompiled.Setup; + + +internal static class WorkspaceTimestamps +{ + public static void MarkChangedFiles(string installedWorkspace, string stagedWorkspace, + Action 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; + } + } +} diff --git a/Launcher/WiiCompiled.Setup/app.manifest b/Launcher/WiiCompiled.Setup/app.manifest new file mode 100644 index 0000000..abcca86 --- /dev/null +++ b/Launcher/WiiCompiled.Setup/app.manifest @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + true + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..fcbb091 --- /dev/null +++ b/README.md @@ -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. diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md new file mode 100644 index 0000000..88fccb5 --- /dev/null +++ b/THIRD-PARTY-NOTICES.md @@ -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: - 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: +- **libogc-derived SRAM structures** - zlib-style license, Copyright (c) Michael Wiedenbauer + (shagkur) and Dave Murphy (WinterMute). `aurora-main/lib/card/SRAM.hpp`. + Source: + +> [!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: - license at + + +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: + +### 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: + +### 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: . 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: . Full license text: +`runtime/third_party/toml11/LICENSE`. + +### YamlDotNet - MIT + +Copyright (c) Antoine Aubry and contributors. +Referenced by `translator/src/Translator.Core`. Source: + +--- + +## 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 | | +| Tint (part of Dawn) | with Dawn | BSD-3-Clause | | +| DirectXShaderCompiler (`dxcompiler.dll`) | with Dawn | NCSA / University of Illinois Open Source | | +| SDL | 3.4.4 | zlib | | +| Abseil | LTS 20240722.0 | Apache-2.0 | | +| Dear ImGui | 1.91.9b-docking | MIT | | +| {fmt} | 11.1.4 | MIT | | +| xxHash | 0.8.3 | BSD-2-Clause | | +| zlib | 1.3.2 | zlib | | +| libpng | 1.6.58 | PNG Reference Library License v2 | | +| FreeType | 2.14.3 | **FreeType License (FTL)** - see below | | +| Zstandard | 1.5.7 | **BSD-3-Clause** - see below | | +| SQLite | 3.51.3 amalgamation | Public domain | | +| Tracy Profiler | pinned commit | BSD-3-Clause | | +| C++/WinRT | - | MIT (Microsoft) | | + +### 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 | | +| CMake | BSD-3-Clause | | +| Ninja | Apache-2.0 | | +| DolphinTool (disc image extraction) | GPL-2.0-or-later | | +| 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. diff --git a/aurora-main/.clang-format b/aurora-main/.clang-format new file mode 100644 index 0000000..c144259 --- /dev/null +++ b/aurora-main/.clang-format @@ -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 diff --git a/aurora-main/.gitignore b/aurora-main/.gitignore new file mode 100644 index 0000000..5418932 --- /dev/null +++ b/aurora-main/.gitignore @@ -0,0 +1,10 @@ +.buildcache/ +.DS_Store +.idea/ +.cache/ +.vscode/ +.vs/ +build/ +cmake-build-*/ +CMakeUserPresets.json +out/ diff --git a/aurora-main/CMakeLists.txt b/aurora-main/CMakeLists.txt new file mode 100644 index 0000000..8ff456f --- /dev/null +++ b/aurora-main/CMakeLists.txt @@ -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 () diff --git a/aurora-main/LICENSE b/aurora-main/LICENSE new file mode 100644 index 0000000..baf2790 --- /dev/null +++ b/aurora-main/LICENSE @@ -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. diff --git a/aurora-main/README.md b/aurora-main/README.md new file mode 100644 index 0000000..9d16c77 --- /dev/null +++ b/aurora-main/README.md @@ -0,0 +1,46 @@ +
+ Logo +
+
+ +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). diff --git a/aurora-main/assets/aurora.png b/aurora-main/assets/aurora.png new file mode 100644 index 0000000..ac897bb Binary files /dev/null and b/aurora-main/assets/aurora.png differ diff --git a/aurora-main/cmake/AuroraCopyRuntimeDLLs.cmake b/aurora-main/cmake/AuroraCopyRuntimeDLLs.cmake new file mode 100644 index 0000000..b4fa166 --- /dev/null +++ b/aurora-main/cmake/AuroraCopyRuntimeDLLs.cmake @@ -0,0 +1,55 @@ +include_guard(GLOBAL) + +# aurora_copy_runtime_dlls( [...]) +# +# 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 $ $ + 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}" $ + ) + endif () + endforeach () + endif () + endif () +endfunction() + +# aurora_install_runtime_dlls( ) +# +# 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 \"$/*.dll\")\n\ +if(_aurora_runtime_dlls)\n\ + file(INSTALL DESTINATION \"${install_destination}\" TYPE FILE FILES \${_aurora_runtime_dlls})\n\ +endif()" + ) +endfunction() diff --git a/aurora-main/cmake/AuroraDawnProvider.cmake b/aurora-main/cmake/AuroraDawnProvider.cmake new file mode 100644 index 0000000..002344b --- /dev/null +++ b/aurora-main/cmake/AuroraDawnProvider.cmake @@ -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 () diff --git a/aurora-main/cmake/AuroraNodProvider.cmake b/aurora-main/cmake/AuroraNodProvider.cmake new file mode 100644 index 0000000..dbb9a71 --- /dev/null +++ b/aurora-main/cmake/AuroraNodProvider.cmake @@ -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 () diff --git a/aurora-main/cmake/AuroraSDL3Provider.cmake b/aurora-main/cmake/AuroraSDL3Provider.cmake new file mode 100644 index 0000000..458d424 --- /dev/null +++ b/aurora-main/cmake/AuroraSDL3Provider.cmake @@ -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 () diff --git a/aurora-main/cmake/aurora_card.cmake b/aurora-main/cmake/aurora_card.cmake new file mode 100644 index 0000000..5eace0f --- /dev/null +++ b/aurora-main/cmake/aurora_card.cmake @@ -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) diff --git a/aurora-main/cmake/aurora_core.cmake b/aurora-main/cmake/aurora_core.cmake new file mode 100644 index 0000000..3e53a62 --- /dev/null +++ b/aurora-main/cmake/aurora_core.cmake @@ -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 () diff --git a/aurora-main/cmake/aurora_dvd.cmake b/aurora-main/cmake/aurora_dvd.cmake new file mode 100644 index 0000000..c95f72e --- /dev/null +++ b/aurora-main/cmake/aurora_dvd.cmake @@ -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) diff --git a/aurora-main/cmake/aurora_gd.cmake b/aurora-main/cmake/aurora_gd.cmake new file mode 100644 index 0000000..80bd667 --- /dev/null +++ b/aurora-main/cmake/aurora_gd.cmake @@ -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) diff --git a/aurora-main/cmake/aurora_gx.cmake b/aurora-main/cmake/aurora_gx.cmake new file mode 100644 index 0000000..ff740a7 --- /dev/null +++ b/aurora-main/cmake/aurora_gx.cmake @@ -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) diff --git a/aurora-main/cmake/aurora_main.cmake b/aurora-main/cmake/aurora_main.cmake new file mode 100644 index 0000000..b712164 --- /dev/null +++ b/aurora-main/cmake/aurora_main.cmake @@ -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}) diff --git a/aurora-main/cmake/aurora_ms.cmake b/aurora-main/cmake/aurora_ms.cmake new file mode 100644 index 0000000..9df3020 --- /dev/null +++ b/aurora-main/cmake/aurora_ms.cmake @@ -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) diff --git a/aurora-main/cmake/aurora_mtx.cmake b/aurora-main/cmake/aurora_mtx.cmake new file mode 100644 index 0000000..580a942 --- /dev/null +++ b/aurora-main/cmake/aurora_mtx.cmake @@ -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) diff --git a/aurora-main/cmake/aurora_os.cmake b/aurora-main/cmake/aurora_os.cmake new file mode 100644 index 0000000..77156f1 --- /dev/null +++ b/aurora-main/cmake/aurora_os.cmake @@ -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) diff --git a/aurora-main/cmake/aurora_pad.cmake b/aurora-main/cmake/aurora_pad.cmake new file mode 100644 index 0000000..5790fd0 --- /dev/null +++ b/aurora-main/cmake/aurora_pad.cmake @@ -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) diff --git a/aurora-main/cmake/aurora_si.cmake b/aurora-main/cmake/aurora_si.cmake new file mode 100644 index 0000000..0682c62 --- /dev/null +++ b/aurora-main/cmake/aurora_si.cmake @@ -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) diff --git a/aurora-main/cmake/aurora_vi.cmake b/aurora-main/cmake/aurora_vi.cmake new file mode 100644 index 0000000..38544af --- /dev/null +++ b/aurora-main/cmake/aurora_vi.cmake @@ -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) diff --git a/aurora-main/docs/building.md b/aurora-main/docs/building.md new file mode 100644 index 0000000..ca1cabd --- /dev/null +++ b/aurora-main/docs/building.md @@ -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 diff --git a/aurora-main/examples/CMakeLists.txt b/aurora-main/examples/CMakeLists.txt new file mode 100644 index 0000000..c5977f6 --- /dev/null +++ b/aurora-main/examples/CMakeLists.txt @@ -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 () diff --git a/aurora-main/examples/simple.c b/aurora-main/examples/simple.c new file mode 100644 index 0000000..cb8a6c4 --- /dev/null +++ b/aurora-main/examples/simple.c @@ -0,0 +1,90 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +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; +} diff --git a/aurora-main/extern/CMakeLists.txt b/aurora-main/extern/CMakeLists.txt new file mode 100644 index 0000000..9d77bbb --- /dev/null +++ b/aurora-main/extern/CMakeLists.txt @@ -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 . +# 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 + $<$:-include> + $<$: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 () diff --git a/aurora-main/include/aurora/aurora.h b/aurora-main/include/aurora/aurora.h new file mode 100644 index 0000000..c3f70d2 --- /dev/null +++ b/aurora-main/include/aurora/aurora.h @@ -0,0 +1,172 @@ +#ifndef AURORA_AURORA_H +#define AURORA_AURORA_H + +#ifdef __cplusplus +#include +#include + +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 diff --git a/aurora-main/include/aurora/dvd.h b/aurora-main/include/aurora/dvd.h new file mode 100644 index 0000000..4a7f53b --- /dev/null +++ b/aurora-main/include/aurora/dvd.h @@ -0,0 +1,27 @@ +#ifndef AURORA_DVD_H +#define AURORA_DVD_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * 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 diff --git a/aurora-main/include/aurora/event.h b/aurora-main/include/aurora/event.h new file mode 100644 index 0000000..46ebf40 --- /dev/null +++ b/aurora-main/include/aurora/event.h @@ -0,0 +1,44 @@ +#ifndef AURORA_EVENT_H +#define AURORA_EVENT_H + +#include "aurora.h" + +#include +#include + +#ifdef __cplusplus +#include + +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 diff --git a/aurora-main/include/aurora/gfx.h b/aurora-main/include/aurora/gfx.h new file mode 100644 index 0000000..f58252f --- /dev/null +++ b/aurora-main/include/aurora/gfx.h @@ -0,0 +1,97 @@ +#ifndef AURORA_GFX_H +#define AURORA_GFX_H + +#ifdef __cplusplus +#include +#include + +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 diff --git a/aurora-main/include/aurora/imgui.h b/aurora-main/include/aurora/imgui.h new file mode 100644 index 0000000..77eba9a --- /dev/null +++ b/aurora-main/include/aurora/imgui.h @@ -0,0 +1,20 @@ +#ifndef AURORA_IMGUI_H +#define AURORA_IMGUI_H + +#include + +#ifdef __cplusplus +#include + +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 diff --git a/aurora-main/include/aurora/imgui_config.h b/aurora-main/include/aurora/imgui_config.h new file mode 100644 index 0000000..3e62e59 --- /dev/null +++ b/aurora-main/include/aurora/imgui_config.h @@ -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); diff --git a/aurora-main/include/aurora/main.h b/aurora-main/include/aurora/main.h new file mode 100644 index 0000000..68ee67c --- /dev/null +++ b/aurora-main/include/aurora/main.h @@ -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 diff --git a/aurora-main/include/aurora/math.hpp b/aurora-main/include/aurora/math.hpp new file mode 100644 index 0000000..cb15cf1 --- /dev/null +++ b/aurora-main/include/aurora/math.hpp @@ -0,0 +1,254 @@ +#pragma once + +#include +#include +#include + +#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 +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 +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 +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 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 + [[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 +[[nodiscard]] Vec4 operator+(const Vec4& a, const Vec4& 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 +[[nodiscard]] Vec4 operator*(const Vec4& a, const Vec4& 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 +struct Mat3x2 { + Vec2 m0{}; + Vec2 m1{}; + Vec2 m2{}; + + constexpr Mat3x2() = default; + constexpr Mat3x2(const Vec2& m0, const Vec2& m1, const Vec2& 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 +struct Mat4x2 { + Vec2 m0{}; + Vec2 m1{}; + Vec2 m2{}; + Vec2 m3{}; + + constexpr Mat4x2() = default; + constexpr Mat4x2(const Vec2& m0, const Vec2& m1, const Vec2& m2, const Vec2& 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 +struct Mat2x4 { + Vec4 m0{}; + Vec4 m1{}; + + constexpr Mat2x4() = default; + constexpr Mat2x4(const Vec4& m0, const Vec4& m1, const Vec4& 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) == 32); +template +struct Mat4x4; +template +struct Mat3x4 { + Vec4 m0{}; + Vec4 m1{}; + Vec4 m2{}; + + constexpr Mat3x4() = default; + constexpr Mat3x4(const Vec4& m0, const Vec4& m1, const Vec4& m2) : m0(m0), m1(m1), m2(m2) {} + + [[nodiscard]] Mat4x4 to4x4() const; + [[nodiscard]] Mat4x4 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) == 48); +template +struct Mat4x4 { + Vec4 m0{}; + Vec4 m1{}; + Vec4 m2{}; + Vec4 m3{}; + + constexpr Mat4x4() = default; + constexpr Mat4x4(const Vec4& m0, const Vec4& m1, const Vec4& m2, const Vec4& 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& operator[](size_t i) { return *(&m0 + i); } + const Vec4& 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) == 64); +template +[[nodiscard]] Mat4x4 operator*(const Mat4x4& a, const Mat4x4& b) { + Mat4x4 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 +[[nodiscard]] Mat4x4 Mat3x4::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 +[[nodiscard]] Mat4x4 Mat3x4::toTransposed4x4() const { + return Mat4x4{ + {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 diff --git a/aurora-main/include/aurora/render_size_limits.hpp b/aurora-main/include/aurora/render_size_limits.hpp new file mode 100644 index 0000000..20d92f3 --- /dev/null +++ b/aurora-main/include/aurora/render_size_limits.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include +#include +#include +#include + +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(std::lround(static_cast(width) * scale))); + const auto scaledHeight = std::max(1u, static_cast(std::lround(static_cast(height) * scale))); + const float aspect = clamp_dynamic_aspect(requestedAspect); + if (aspect >= static_cast(width) / static_cast(height)) { + return { + std::max(1u, static_cast(std::lround(static_cast(scaledHeight) * aspect))), + scaledHeight, + }; + } + return { + scaledWidth, + std::max(1u, static_cast(std::lround(static_cast(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::max()) { + dimensionLimit = std::min(dimensionLimit, adapterMaxDimension); + } + dimensionLimit = std::max(dimensionLimit, 1u); + + double fitScale = 1.0; + fitScale = std::min(fitScale, static_cast(dimensionLimit) / static_cast(width)); + fitScale = std::min(fitScale, static_cast(dimensionLimit) / static_cast(height)); + + const double requestedPixels = static_cast(width) * static_cast(height); + if (requestedPixels > static_cast(kMaxFramebufferPixels)) { + fitScale = std::min(fitScale, std::sqrt(static_cast(kMaxFramebufferPixels) / requestedPixels)); + } + + if (fitScale >= 1.0) { + return {width, height}; + } + return { + std::max(1u, static_cast(std::floor(static_cast(width) * fitScale))), + std::max(1u, static_cast(std::floor(static_cast(height) * fitScale))), + }; +} + +} // namespace aurora::render_size_limits diff --git a/aurora-main/include/dolphin/ai.h b/aurora-main/include/dolphin/ai.h new file mode 100644 index 0000000..84db334 --- /dev/null +++ b/aurora-main/include/dolphin/ai.h @@ -0,0 +1,50 @@ +#ifndef _DOLPHIN_AI_H_ +#define _DOLPHIN_AI_H_ + +#include + +#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 diff --git a/aurora-main/include/dolphin/ar.h b/aurora-main/include/dolphin/ar.h new file mode 100644 index 0000000..bf09f24 --- /dev/null +++ b/aurora-main/include/dolphin/ar.h @@ -0,0 +1,79 @@ +#ifndef _DOLPHIN_AR_H_ +#define _DOLPHIN_AR_H_ + +#include + +#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 diff --git a/aurora-main/include/dolphin/base/PPCArch.h b/aurora-main/include/dolphin/base/PPCArch.h new file mode 100644 index 0000000..8bdd0dd --- /dev/null +++ b/aurora-main/include/dolphin/base/PPCArch.h @@ -0,0 +1,542 @@ +#ifndef _DOLPHIN_PPCARCH +#define _DOLPHIN_PPCARCH + +#include + +#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 diff --git a/aurora-main/include/dolphin/card.h b/aurora-main/include/dolphin/card.h new file mode 100644 index 0000000..33b9a5d --- /dev/null +++ b/aurora-main/include/dolphin/card.h @@ -0,0 +1,340 @@ +#ifndef _DOLPHIN_CARD_H_ +#define _DOLPHIN_CARD_H_ + +#include +#include +#include + +#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 diff --git a/aurora-main/include/dolphin/db.h b/aurora-main/include/dolphin/db.h new file mode 100644 index 0000000..c62f188 --- /dev/null +++ b/aurora-main/include/dolphin/db.h @@ -0,0 +1,22 @@ +#ifndef _DOLPHIN_DB_H_ +#define _DOLPHIN_DB_H_ + +#include +#include + +#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_ diff --git a/aurora-main/include/dolphin/db/DBInterface.h b/aurora-main/include/dolphin/db/DBInterface.h new file mode 100644 index 0000000..7bfa354 --- /dev/null +++ b/aurora-main/include/dolphin/db/DBInterface.h @@ -0,0 +1,31 @@ +#ifndef _DOLPHIN_DBINTERFACE_H_ +#define _DOLPHIN_DBINTERFACE_H_ + +#include + +#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 diff --git a/aurora-main/include/dolphin/dolphin.h b/aurora-main/include/dolphin/dolphin.h new file mode 100644 index 0000000..f0025ed --- /dev/null +++ b/aurora-main/include/dolphin/dolphin.h @@ -0,0 +1,24 @@ +#ifndef _DOLPHIN_H_ +#define _DOLPHIN_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +// #include +#include +#include + +#endif diff --git a/aurora-main/include/dolphin/dsp.h b/aurora-main/include/dolphin/dsp.h new file mode 100644 index 0000000..72ffa82 --- /dev/null +++ b/aurora-main/include/dolphin/dsp.h @@ -0,0 +1,57 @@ +#ifndef _DOLPHIN_DSP_H_ +#define _DOLPHIN_DSP_H_ + +#include + +#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 diff --git a/aurora-main/include/dolphin/dtk.h b/aurora-main/include/dolphin/dtk.h new file mode 100644 index 0000000..3a76a0f --- /dev/null +++ b/aurora-main/include/dolphin/dtk.h @@ -0,0 +1,56 @@ +#ifndef _DOLPHIN_DTK_H_ +#define _DOLPHIN_DTK_H_ + +#include + +#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 diff --git a/aurora-main/include/dolphin/dvd.h b/aurora-main/include/dolphin/dvd.h new file mode 100644 index 0000000..b2e1757 --- /dev/null +++ b/aurora-main/include/dolphin/dvd.h @@ -0,0 +1,237 @@ +#ifndef _DOLPHIN_DVD_H_ +#define _DOLPHIN_DVD_H_ + +#include + +#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 diff --git a/aurora-main/include/dolphin/exi.h b/aurora-main/include/dolphin/exi.h new file mode 100644 index 0000000..f094c36 --- /dev/null +++ b/aurora-main/include/dolphin/exi.h @@ -0,0 +1,103 @@ +#ifndef _DOLPHIN_EXI_H_ +#define _DOLPHIN_EXI_H_ + +#include + +#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 diff --git a/aurora-main/include/dolphin/gd.h b/aurora-main/include/dolphin/gd.h new file mode 100644 index 0000000..d4fabfd --- /dev/null +++ b/aurora-main/include/dolphin/gd.h @@ -0,0 +1,15 @@ +#ifndef _DOLPHIN_GD_H_ +#define _DOLPHIN_GD_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#endif diff --git a/aurora-main/include/dolphin/gd/GDAurora.h b/aurora-main/include/dolphin/gd/GDAurora.h new file mode 100644 index 0000000..f898ece --- /dev/null +++ b/aurora-main/include/dolphin/gd/GDAurora.h @@ -0,0 +1,22 @@ +#ifndef DOLPHIN_GDAURORA_H +#define DOLPHIN_GDAURORA_H + +/** + * Pushes a debug group to the backend graphics API. These may show in debugging tools such as RenderDoc. + * + * It is considered an error to have unpopped debug groups at the end of the frame. They will be automatically cleared. + * Debug groups are *not* automatically scoped to a display list. + */ +void GDPushDebugGroup(const char* label); + +/** + * Pop a debug group previously pushed via GXPushDebugGroup(). + */ +void GDPopDebugGroup(); + +/** + * Sends a debug marker to the backend graphics API. These may show in debugging tools such as RenderDoc. + */ +void GDInsertDebugMarker(const char* label); + +#endif // DOLPHIN_GDAURORA_H diff --git a/aurora-main/include/dolphin/gd/GDBase.h b/aurora-main/include/dolphin/gd/GDBase.h new file mode 100644 index 0000000..1c88333 --- /dev/null +++ b/aurora-main/include/dolphin/gd/GDBase.h @@ -0,0 +1,177 @@ +#ifndef _DOLPHIN_GD_BASE_H +#define _DOLPHIN_GD_BASE_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*GDOverflowCb)(void); + +typedef struct _GDLObj { + u8* start; + u32 length; + u8* ptr; + u8* top; +} GDLObj; + +extern GDLObj* __GDCurrentDL; + +void GDInitGDLObj(GDLObj* dl, void* start, u32 length); +void GDFlushCurrToMem(void); +void GDPadCurr32(void); +void GDOverflowed(void); +void GDSetOverflowCallback(GDOverflowCb callback); +GDOverflowCb GDGetOverflowCallback(void); + +inline static void GDOverflowCheck(u32 size) { + if (__GDCurrentDL->ptr + size > __GDCurrentDL->top) { + GDOverflowed(); + } +} + +inline static void __GDWrite(u8 data) { + *__GDCurrentDL->ptr++ = data; +} + +inline static void GDWrite_data(const void* data, u32 length) { + GDOverflowCheck(length); + // The compiler realizes this is memcpy, trust. + const u8* dataPtr = static_cast(data); + for (u32 i = 0; i < length; i++) { + __GDWrite(dataPtr[i]); + } +} + +inline static void GDWrite_u8(u8 data) { + GDOverflowCheck(sizeof(u8)); + __GDWrite(data); +} + +inline static void GDWrite_u16(u16 data) { + GDOverflowCheck(sizeof(u16)); + __GDWrite((u8)((data >> 8))); + __GDWrite((u8)((data >> 0) & 0xFF)); +} + +inline static void GDWrite_u24(u32 data) { + GDOverflowCheck(3); + __GDWrite((u8)((data >> 16) & 0xFF)); + __GDWrite((u8)((data >> 8) & 0xFF)); + __GDWrite((u8)((data >> 0) & 0xFF)); +} + +inline static void GDWrite_u32(u32 data) { + GDOverflowCheck(sizeof(u32)); + __GDWrite((u8)((data >> 24) & 0xFF)); + __GDWrite((u8)((data >> 16) & 0xFF)); + __GDWrite((u8)((data >> 8) & 0xFF)); + __GDWrite((u8)((data >> 0) & 0xFF)); +} + +inline static void GDWrite_u64(u64 data) { + GDOverflowCheck(sizeof(u64)); + __GDWrite((u8)((data >> 56) & 0xFF)); + __GDWrite((u8)((data >> 48) & 0xFF)); + __GDWrite((u8)((data >> 40) & 0xFF)); + __GDWrite((u8)((data >> 32) & 0xFF)); + __GDWrite((u8)((data >> 24) & 0xFF)); + __GDWrite((u8)((data >> 16) & 0xFF)); + __GDWrite((u8)((data >> 8) & 0xFF)); + __GDWrite((u8)((data >> 0) & 0xFF)); +} + +inline static void GDWrite_f32(f32 data) { + union { + f32 f; + u32 u; + } fid; + fid.f = data; + GDWrite_u32(fid.u); +} + +inline static void GDWriteXFCmdHdr(u16 addr, u8 len) { + GDWrite_u8(GX_LOAD_XF_REG); + GDWrite_u16(len - 1); + GDWrite_u16(addr); +} + +inline static void GDWriteXFCmd(u16 addr, u32 val) { + GDWrite_u8(GX_LOAD_XF_REG); + GDWrite_u16(0); + GDWrite_u16(addr); + GDWrite_u32(val); +} + +inline static void GDWriteXFIndxDCmd(u16 addr, u8 len, u16 index) { + GDWrite_u8(GX_LOAD_INDX_D); + GDWrite_u16(index); + GDWrite_u16((len - 1) << 12 | addr); +} + +inline static void GDWriteXFIndxACmd(u16 addr, u8 len, u16 index) { + GDWrite_u8(GX_LOAD_INDX_A); + GDWrite_u16(index); + GDWrite_u16(((len - 1) << 12) | addr); +} + +inline static void GDWriteXFIndxBCmd(u16 addr, u8 len, u16 index) { + GDWrite_u8(GX_LOAD_INDX_B); + GDWrite_u16(index); + GDWrite_u16(((len - 1) << 12) | addr); +} + +inline static void GDWriteXFIndxCCmd(u16 addr, u8 len, u16 index) { + GDWrite_u8(GX_LOAD_INDX_C); + GDWrite_u16(index); + GDWrite_u16(((len - 1) << 12) | addr); +} + +inline static void GDWriteCPCmd(u8 addr, u32 val) { + GDWrite_u8(GX_LOAD_CP_REG); + GDWrite_u8(addr); + GDWrite_u32(val); +} + +inline static void GDWriteBPCmd(u32 regval) { + GDWrite_u8(GX_LOAD_BP_REG); + GDWrite_u32(regval); +} + +inline static void GDWriteAuroraCmd(u16 subCommand) { + GDWrite_u8(GX_LOAD_AURORA); + GDWrite_u16(subCommand); +} + +inline static void GDSetCurrent(GDLObj* dl) { + __GDCurrentDL = dl; +} + +static inline u32 GDGetCurrOffset(void) { + return (u32)(__GDCurrentDL->ptr - __GDCurrentDL->start); +} + +static inline void GDSetCurrOffset(u32 offset) { + __GDCurrentDL->ptr = __GDCurrentDL->start + offset; +} + +static inline void* GDGetCurrPointer(void) { + return (void*)__GDCurrentDL->ptr; +} + +static inline u8* GDGetCurrPointer2(void) { + return __GDCurrentDL->ptr; +} + +static inline u32 GDGetGDLObjOffset(const GDLObj* dl) { + return (u32)(dl->ptr - dl->start); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gd/GDFile.h b/aurora-main/include/dolphin/gd/GDFile.h new file mode 100644 index 0000000..3589fbb --- /dev/null +++ b/aurora-main/include/dolphin/gd/GDFile.h @@ -0,0 +1,31 @@ +#ifndef _DOLPHIN_GD_FILE_H +#define _DOLPHIN_GD_FILE_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define GD_FILE_VERSION_NUMBER 0x11223344 + +typedef struct _GDGList { + void* ptr; + u32 byteLength; +} GDGList; + +typedef struct _GDFileHeader { + u32 versionNumber; + u32 numDLs; + u32 numPLs; + GDGList* DLDescArray; + GDGList* PLDescArray; +} GDFileHeader; + +s32 GDReadDLFile(const char* fName, u32* numDLs, u32* numPLs, GDGList** DLDescArray, GDGList** PLDescArray); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gd/GDGeometry.h b/aurora-main/include/dolphin/gd/GDGeometry.h new file mode 100644 index 0000000..ef80885 --- /dev/null +++ b/aurora-main/include/dolphin/gd/GDGeometry.h @@ -0,0 +1,525 @@ +#ifndef _DOLPHIN_GD_GEOMETRY_H_ +#define _DOLPHIN_GD_GEOMETRY_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Command processor register IDs +#define CP_REG_MTXIDXA_ID 0x30 // Matrix index A +#define CP_REG_MTXIDXB_ID 0x40 // Matrix index B +#define CP_REG_VCD_LO_ID 0x50 // Vertex descriptor (lo) +#define CP_REG_VCD_HI_ID 0x60 // Vertex descriptor (hi) +#define CP_REG_VAT_GRP0_ID 0x70 // Vertex attribute table (group 0) +#define CP_REG_VAT_GRP1_ID 0x80 // Vertex attribute table (group 1) +#define CP_REG_VAT_GRP2_ID 0x90 // Vertex attribute table (group 2) +#define CP_REG_ARRAYBASE_ID 0xA0 // Vertex array start/base +#define CP_REG_ARRAYSTRIDE_ID 0xB0 // Vertex array stride + +// XF locators for textures +// Projection type [30-30] +#define GX_XF_TEX_PROJTYPE_ST 30 +#define GX_XF_TEX_PROJTYPE_END 30 + +// Input format [29-29] +#define GX_XF_TEX_INPUTFORM_ST 29 +#define GX_XF_TEX_INPUTFORM_END 29 + +// Texture gen type [25-27] +#define GX_XF_TEX_TEXGENTYPE_ST 25 +#define GX_XF_TEX_TEXGENTYPE_END 27 + +// Source row [20-24] +#define GX_XF_TEX_SRCROW_ST 20 +#define GX_XF_TEX_SRCROW_END 24 + +// Bump source texture [17-19] +#define GX_XF_TEX_BUMPSRCTEX_ST 17 +#define GX_XF_TEX_BUMPSRCTEX_END 19 + +// Bump source light [14-16] +#define GX_XF_TEX_BUMPSRCLIGHT_ST 14 +#define GX_XF_TEX_BUMPSRCLIGHT_END 16 + +// Blitting processor registers. +#define GX_BP_REG_GENMODE 0x0 // gen mode + +// display copy filters +#define GX_BP_REG_DISPCOPYFILTER0 0x1 // display copy filter 0 +#define GX_BP_REG_DISPCOPYFILTER1 0x2 // display copy filter 1 +#define GX_BP_REG_DISPCOPYFILTER2 0x3 // display copy filter 2 +#define GX_BP_REG_DISPCOPYFILTER3 0x4 // display copy filter 3 + +// indirect matrices +#define GX_BP_REG_INDMTX0A 0x6 // indirect matrix 0A +#define GX_BP_REG_INDMTX0B 0x7 // indirect matrix 0B +#define GX_BP_REG_INDMTX0C 0x8 // indirect matrix 0C +#define GX_BP_REG_INDMTX1A 0x9 // indirect matrix 1A +#define GX_BP_REG_INDMTX1B 0xA // indirect matrix 1B +#define GX_BP_REG_INDMTX1C 0xB // indirect matrix 1C +#define GX_BP_REG_INDMTX2A 0xC // indirect matrix 2A +#define GX_BP_REG_INDMTX2B 0xD // indirect matrix 2B +#define GX_BP_REG_INDMTX2C 0xE // indirect matrix 2C +#define GX_BP_REG_INDIMASK 0xF // indirect mask + +// indirect TEV stages +#define GX_BP_REG_INDTEVSTAGE0 0x10 // indirect TEV stage 0 +#define GX_BP_REG_INDTEVSTAGE1 0x11 // indirect TEV stage 1 +#define GX_BP_REG_INDTEVSTAGE2 0x12 // indirect TEV stage 2 +#define GX_BP_REG_INDTEVSTAGE3 0x13 // indirect TEV stage 3 +#define GX_BP_REG_INDTEVSTAGE4 0x14 // indirect TEV stage 4 +#define GX_BP_REG_INDTEVSTAGE5 0x15 // indirect TEV stage 5 +#define GX_BP_REG_INDTEVSTAGE6 0x16 // indirect TEV stage 6 +#define GX_BP_REG_INDTEVSTAGE7 0x17 // indirect TEV stage 7 +#define GX_BP_REG_INDTEVSTAGE8 0x18 // indirect TEV stage 8 +#define GX_BP_REG_INDTEVSTAGE9 0x19 // indirect TEV stage 9 +#define GX_BP_REG_INDTEVSTAGE10 0x1A // indirect TEV stage 10 +#define GX_BP_REG_INDTEVSTAGE11 0x1B // indirect TEV stage 11 +#define GX_BP_REG_INDTEVSTAGE12 0x1C // indirect TEV stage 12 +#define GX_BP_REG_INDTEVSTAGE13 0x1D // indirect TEV stage 13 +#define GX_BP_REG_INDTEVSTAGE14 0x1E // indirect TEV stage 14 +#define GX_BP_REG_INDTEVSTAGE15 0x1F // indirect TEV stage 15 + +// performance manips +#define GX_BP_REG_SCISSORTL 0x20 // scissor top left +#define GX_BP_REG_SCISSORBR 0x21 // scissor bottom right +#define GX_BP_REG_LINEPTWIDTH 0x22 // line point width +#define GX_BP_REG_PERF0TRI 0x23 // performance 0 (triangle) +#define GX_BP_REG_PERF0QUAD 0x24 // performance 0 (quad) + +// rasters +#define GX_BP_REG_RAS1_SS0 0x25 +#define GX_BP_REG_RAS1_SS1 0x26 +#define GX_BP_REG_RAS1_IREF 0x27 +#define GX_BP_REG_RAS1_TREF0 0x28 +#define GX_BP_REG_RAS1_TREF1 0x29 +#define GX_BP_REG_RAS1_TREF2 0x2A +#define GX_BP_REG_RAS1_TREF3 0x2B +#define GX_BP_REG_RAS1_TREF4 0x2C +#define GX_BP_REG_RAS1_TREF5 0x2D +#define GX_BP_REG_RAS1_TREF6 0x2E +#define GX_BP_REG_RAS1_TREF7 0x2F + +// setup sizes +#define GX_BP_REG_SU_SSIZE0 0x30 +#define GX_BP_REG_SU_TSIZE0 0x31 +#define GX_BP_REG_SU_SSIZE1 0x32 +#define GX_BP_REG_SU_TSIZE1 0x33 +#define GX_BP_REG_SU_SSIZE2 0x34 +#define GX_BP_REG_SU_TSIZE2 0x35 +#define GX_BP_REG_SU_SSIZE3 0x36 +#define GX_BP_REG_SU_TSIZE3 0x37 +#define GX_BP_REG_SU_SSIZE4 0x38 +#define GX_BP_REG_SU_TSIZE4 0x39 +#define GX_BP_REG_SU_SSIZE5 0x3A +#define GX_BP_REG_SU_TSIZE5 0x3B +#define GX_BP_REG_SU_SSIZE6 0x3C +#define GX_BP_REG_SU_TSIZE6 0x3D +#define GX_BP_REG_SU_SSIZE7 0x3E +#define GX_BP_REG_SU_TSIZE7 0x3F + +// Z and blend controls +#define GX_BP_REG_ZMODE 0x40 +#define GX_BP_REG_BLENDMODE 0x41 +#define GX_BP_REG_DSTALPHA 0x42 +#define GX_BP_REG_ZCONTROL 0x43 +#define GX_BP_REG_FIELDMASK 0x44 +#define GX_BP_REG_DRAWDONE 0x45 +#define GX_BP_REG_PETOKEN 0x47 +#define GX_BP_REG_PETOKENINT 0x48 + +// copying +#define GX_BP_REG_TEXCOPYSRCXY 0x49 +#define GX_BP_REG_TEXCOPYSRCWH 0x4A +#define GX_BP_REG_TEXCOPYDST 0x4B +#define GX_BP_REG_DISPCOPYSTRIDE 0x4D +#define GX_BP_REG_DISPCOPYSCALEY 0x4E +#define GX_BP_REG_COPYCLEARAR 0x4F +#define GX_BP_REG_COPYCLEARGB 0x50 +#define GX_BP_REG_COPYCLEARZ 0x51 +#define GX_BP_REG_COPYFILTER0 0x53 +#define GX_BP_REG_COPYFILTER1 0x54 + +// +#define GX_BP_REG_BOUNDINGBOX0 0x55 +#define GX_BP_REG_BOUNDINGBOX1 0x56 + +#define GX_BP_REG_SCISSOROFFSET 0x59 + +// texture memory +#define GX_BP_REG_TMEMPRELOADADDR 0x60 +#define GX_BP_REG_TMEMPRELOADEVEN 0x61 +#define GX_BP_REG_TMEMPRELOADODD 0x62 +#define GX_BP_REG_TMEMPRELOADMODE 0x63 +#define GX_BP_REG_TMEMTLUTSRC 0x64 +#define GX_BP_REG_TMEMTLUTDST 0x65 +#define GX_BP_REG_TMEMTEXINVALIDATE 0x66 + +// performance 1 +#define GX_BP_REG_PERF1 0x67 +#define GX_BP_REG_FIELDMODE 0x68 + +// set modes +#define GX_BP_REG_SETMODE0_TEX0 0x80 +#define GX_BP_REG_SETMODE0_TEX1 0x81 +#define GX_BP_REG_SETMODE0_TEX2 0x82 +#define GX_BP_REG_SETMODE0_TEX3 0x83 +#define GX_BP_REG_SETMODE1_TEX0 0x84 +#define GX_BP_REG_SETMODE1_TEX1 0x85 +#define GX_BP_REG_SETMODE1_TEX2 0x86 +#define GX_BP_REG_SETMODE1_TEX3 0x87 + +// set images +#define GX_BP_REG_SETIMAGE0_TEX0 0x88 +#define GX_BP_REG_SETIMAGE0_TEX1 0x89 +#define GX_BP_REG_SETIMAGE0_TEX2 0x8A +#define GX_BP_REG_SETIMAGE0_TEX3 0x8B +#define GX_BP_REG_SETIMAGE1_TEX0 0x8C +#define GX_BP_REG_SETIMAGE1_TEX1 0x8D +#define GX_BP_REG_SETIMAGE1_TEX2 0x8E +#define GX_BP_REG_SETIMAGE1_TEX3 0x8F +#define GX_BP_REG_SETIMAGE2_TEX0 0x90 +#define GX_BP_REG_SETIMAGE2_TEX1 0x91 +#define GX_BP_REG_SETIMAGE2_TEX2 0x92 +#define GX_BP_REG_SETIMAGE2_TEX3 0x93 +#define GX_BP_REG_SETIMAGE3_TEX0 0x94 +#define GX_BP_REG_SETIMAGE3_TEX1 0x95 +#define GX_BP_REG_SETIMAGE3_TEX2 0x96 +#define GX_BP_REG_SETIMAGE3_TEX3 0x97 + +// set texture lookups +#define GX_BP_REG_SETTLUT_TEX0 0x98 +#define GX_BP_REG_SETTLUT_TEX1 0x99 +#define GX_BP_REG_SETTLUT_TEX2 0x9A +#define GX_BP_REG_SETTLUT_TEX3 0x9B + +// set modes continued +#define GX_BP_REG_SETMODE0_TEX4 0xA0 +#define GX_BP_REG_SETMODE0_TEX5 0xA1 +#define GX_BP_REG_SETMODE0_TEX6 0xA2 +#define GX_BP_REG_SETMODE0_TEX7 0xA3 +#define GX_BP_REG_SETMODE1_TEX4 0xA4 +#define GX_BP_REG_SETMODE1_TEX5 0xA5 +#define GX_BP_REG_SETMODE1_TEX6 0xA6 +#define GX_BP_REG_SETMODE1_TEX7 0xA7 + +// set images continued +#define GX_BP_REG_SETIMAGE0_TEX4 0xA8 +#define GX_BP_REG_SETIMAGE0_TEX5 0xA9 +#define GX_BP_REG_SETIMAGE0_TEX6 0xAA +#define GX_BP_REG_SETIMAGE0_TEX7 0xAB +#define GX_BP_REG_SETIMAGE1_TEX4 0xAC +#define GX_BP_REG_SETIMAGE1_TEX5 0xAD +#define GX_BP_REG_SETIMAGE1_TEX6 0xAE +#define GX_BP_REG_SETIMAGE1_TEX7 0xAF +#define GX_BP_REG_SETIMAGE2_TEX4 0xB0 +#define GX_BP_REG_SETIMAGE2_TEX5 0xB1 +#define GX_BP_REG_SETIMAGE2_TEX6 0xB2 +#define GX_BP_REG_SETIMAGE2_TEX7 0xB3 +#define GX_BP_REG_SETIMAGE3_TEX4 0xB4 +#define GX_BP_REG_SETIMAGE3_TEX5 0xB5 +#define GX_BP_REG_SETIMAGE3_TEX6 0xB6 +#define GX_BP_REG_SETIMAGE3_TEX7 0xB7 + +// set texture lookups continued +#define GX_BP_REG_SETTLUT_TEX4 0xB8 +#define GX_BP_REG_SETTLUT_TEX5 0xB9 +#define GX_BP_REG_SETTLUT_TEX6 0xBA +#define GX_BP_REG_SETTLUT_TEX7 0xBB + +// TEV color manips +#define GX_BP_REG_TEVCOLORCOMBINER0 0xC0 +#define GX_BP_REG_TEVALPHACOMBINER0 0xC1 +#define GX_BP_REG_TEVCOLORCOMBINER1 0xC2 +#define GX_BP_REG_TEVALPHACOMBINER1 0xC3 +#define GX_BP_REG_TEVCOLORCOMBINER2 0xC4 +#define GX_BP_REG_TEVALPHACOMBINER2 0xC5 +#define GX_BP_REG_TEVCOLORCOMBINER3 0xC6 +#define GX_BP_REG_TEVALPHACOMBINER3 0xC7 +#define GX_BP_REG_TEVCOLORCOMBINER4 0xC8 +#define GX_BP_REG_TEVALPHACOMBINER4 0xC9 +#define GX_BP_REG_TEVCOLORCOMBINER5 0xCA +#define GX_BP_REG_TEVALPHACOMBINER5 0xCB +#define GX_BP_REG_TEVCOLORCOMBINER6 0xCC +#define GX_BP_REG_TEVALPHACOMBINER6 0xCD +#define GX_BP_REG_TEVCOLORCOMBINER7 0xCE +#define GX_BP_REG_TEVALPHACOMBINER7 0xCF +#define GX_BP_REG_TEVCOLORCOMBINER8 0xD0 +#define GX_BP_REG_TEVALPHACOMBINER8 0xD1 +#define GX_BP_REG_TEVCOLORCOMBINER9 0xD2 +#define GX_BP_REG_TEVALPHACOMBINER9 0xD3 +#define GX_BP_REG_TEVCOLORCOMBINER10 0xD4 +#define GX_BP_REG_TEVALPHACOMBINER10 0xD5 +#define GX_BP_REG_TEVCOLORCOMBINER11 0xD6 +#define GX_BP_REG_TEVALPHACOMBINER11 0xD7 +#define GX_BP_REG_TEVCOLORCOMBINER12 0xD8 +#define GX_BP_REG_TEVALPHACOMBINER12 0xD9 +#define GX_BP_REG_TEVCOLORCOMBINER13 0xDA +#define GX_BP_REG_TEVALPHACOMBINER13 0xDB +#define GX_BP_REG_TEVCOLORCOMBINER14 0xDC +#define GX_BP_REG_TEVALPHACOMBINER14 0xDD +#define GX_BP_REG_TEVCOLORCOMBINER15 0xDE +#define GX_BP_REG_TEVALPHACOMBINER15 0xDF + +// TEV registers +#define GX_BP_REG_TEVREG0LO 0xE0 +#define GX_BP_REG_TEVREG0HI 0xE1 +#define GX_BP_REG_TEVREG1LO 0xE2 +#define GX_BP_REG_TEVREG1HI 0xE3 +#define GX_BP_REG_TEVREG2LO 0xE4 +#define GX_BP_REG_TEVREG2HI 0xE5 +#define GX_BP_REG_TEVREG3LO 0xE6 +#define GX_BP_REG_TEVREG3HI 0xE7 + +// fog registers +#define GX_BP_REG_FOGRANGE 0xE8 +#define GX_BP_REG_FOGRANGEK0 0xE9 +#define GX_BP_REG_FOGRANGEK1 0xEA +#define GX_BP_REG_FOGRANGEK2 0xEB +#define GX_BP_REG_FOGRANGEK3 0xEC +#define GX_BP_REG_FOGRANGEK4 0xED +#define GX_BP_REG_FOGPARAM0 0xEE +#define GX_BP_REG_FOGPARAM1 0xEF +#define GX_BP_REG_FOGPARAM2 0xF0 +#define GX_BP_REG_FOGPARAM3 0xF1 +#define GX_BP_REG_FOGCOLOR 0xF2 + +// performance manip registers +#define GX_BP_REG_ALPHACOMPARE 0xF3 +#define GX_BP_REG_ZTEXTURE0 0xF4 +#define GX_BP_REG_ZTEXTURE1 0xF5 + +// TEV K selectors +#define GX_BP_REG_TEVKSEL0 0xF6 +#define GX_BP_REG_TEVKSEL1 0xF7 +#define GX_BP_REG_TEVKSEL2 0xF8 +#define GX_BP_REG_TEVKSEL3 0xF9 +#define GX_BP_REG_TEVKSEL4 0xFA +#define GX_BP_REG_TEVKSEL5 0xFB +#define GX_BP_REG_TEVKSEL6 0xFC +#define GX_BP_REG_TEVKSEL7 0xFD + +// SS mask +#define GX_BP_REG_SSMASK 0xFE + +// Transform Unit Registers +#define GX_XF_REG_ERROR 0x1000 +#define GX_XF_REG_DIAGNOSTICS 0x1001 +#define GX_XF_REG_STATE0 0x1002 +#define GX_XF_REG_STATE1 0x1003 +#define GX_XF_REG_CLOCK 0x1004 +#define GX_XF_REG_CLIPDISABLE 0x1005 +#define GX_XF_REG_PERF0 0x1006 +#define GX_XF_REG_PERF1 0x1007 +#define GX_XF_REG_INVERTEXSPEC 0x1008 +#define GX_XF_REG_NUMCOLORS 0x1009 +#define GX_XF_REG_AMBIENT0 0x100A +#define GX_XF_REG_AMBIENT1 0x100B +#define GX_XF_REG_MATERIAL0 0x100C +#define GX_XF_REG_MATERIAL1 0x100D +#define GX_XF_REG_COLOR0CNTRL 0x100E +#define GX_XF_REG_COLOR1CNTRL 0x100F +#define GX_XF_REG_ALPHA0CNTRL 0x1010 +#define GX_XF_REG_ALPHA1CNTRL 0x1011 +#define GX_XF_REG_DUALTEXTRAN 0x1012 +#define GX_XF_REG_MATRIXINDEX0 0x1018 +#define GX_XF_REG_MATRIXINDEX1 0x1019 +#define GX_XF_REG_SCALEX 0x101A +#define GX_XF_REG_SCALEY 0x101B +#define GX_XF_REG_SCALEZ 0x101C +#define GX_XF_REG_OFFSETX 0x101D +#define GX_XF_REG_OFFSETY 0x101E +#define GX_XF_REG_OFFSETZ 0x101F +#define GX_XF_REG_PROJECTIONA 0x1020 +#define GX_XF_REG_PROJECTIONB 0x1021 +#define GX_XF_REG_PROJECTIONC 0x1022 +#define GX_XF_REG_PROJECTIOND 0x1023 +#define GX_XF_REG_PROJECTIONE 0x1024 +#define GX_XF_REG_PROJECTIONF 0x1025 +#define GX_XF_REG_PROJECTORTHO 0x1026 +#define GX_XF_REG_NUMTEX 0x103F +#define GX_XF_REG_TEX0 0x1040 +#define GX_XF_REG_TEX1 0x1041 +#define GX_XF_REG_TEX2 0x1042 +#define GX_XF_REG_TEX3 0x1043 +#define GX_XF_REG_TEX4 0x1044 +#define GX_XF_REG_TEX5 0x1045 +#define GX_XF_REG_TEX6 0x1046 +#define GX_XF_REG_TEX7 0x1047 +#define GX_XF_REG_DUALTEX0 0x1050 +#define GX_XF_REG_DUALTEX1 0x1051 +#define GX_XF_REG_DUALTEX2 0x1052 +#define GX_XF_REG_DUALTEX3 0x1053 +#define GX_XF_REG_DUALTEX4 0x1054 +#define GX_XF_REG_DUALTEX5 0x1055 +#define GX_XF_REG_DUALTEX6 0x1056 +#define GX_XF_REG_DUALTEX7 0x1057 + +#define CP_REG_VCD_LO(pnMtxIdx, txMtxIdxMask, posn, norm, col0, col1) \ + ( \ + (pnMtxIdx) << 0 | \ + (txMtxIdxMask) << 1 | \ + (posn) << 9 | \ + (norm) << 11 | \ + (col0) << 13 | \ + (col1) << 15 \ + ) + +#define CP_REG_VCD_HI(tex0, tex1, tex2, tex3, tex4, tex5, tex6, tex7) \ + ( \ + (tex0) << 0 | \ + (tex1) << 2 | \ + (tex2) << 4 | \ + (tex3) << 6 | \ + (tex4) << 8 | \ + (tex5) << 10 | \ + (tex6) << 12 | \ + (tex7) << 14 \ + ) + +#define CP_REG_VAT_GRP0(posCnt, posType, posFrac, nrmCnt, nrmType, c0Cnt, c0Type, c1Cnt, c1Type, tx0Cnt, tx0Type, tx0Frac, p12, nrmIdx3) \ + ( \ + (posCnt) << 0 | \ + (posType) << 1 | \ + (posFrac) << 4 | \ + (nrmCnt) << 9 | \ + (nrmType) << 10 | \ + (c0Cnt) << 13 | \ + (c0Type) << 14 | \ + (c1Cnt) << 17 | \ + (c1Type) << 18 | \ + (tx0Cnt) << 21 | \ + (tx0Type) << 22 | \ + (tx0Frac) << 25 | \ + (p12) << 30 | \ + (nrmIdx3) << 31 \ + ) + +#define CP_REG_VAT_GRP1(tx1Cnt, tx1Type, tx1Frac, tx2Cnt, tx2Type, tx2Frac, tx3Cnt, tx3Type, tx3Frac, tx4Cnt, tx4Type, p11) \ + ( \ + (tx1Cnt) << 0 | \ + (tx1Type) << 1 | \ + (tx1Frac) << 4 | \ + (tx2Cnt) << 9 | \ + (tx2Type) << 10 | \ + (tx2Frac) << 13 | \ + (tx3Cnt) << 18 | \ + (tx3Type) << 19 | \ + (tx3Frac) << 22 | \ + (tx4Cnt) << 27 | \ + (tx4Type) << 28 | \ + p11 << 31 \ + ) + +#define CP_REG_VAT_GRP2(tx4Frac, tx5Cnt, tx5Type, tx5Frac, tx6Cnt, tx6Type, tx6Frac, tx7Cnt, tx7Type, tx7Frac) \ + ( \ + (tx4Frac) << 0 | \ + (tx5Cnt) << 5 | \ + (tx5Type) << 6 | \ + (tx5Frac) << 9 | \ + (tx6Cnt) << 14 | \ + (tx6Type) << 15 | \ + (tx6Frac) << 18 | \ + (tx7Cnt) << 23 | \ + (tx7Type) << 24 | \ + (tx7Frac) << 27 \ + ) + +// Transform unit register IDs +#define XF_REG_ERROR_ID 0x1000 +#define XF_REG_DIAGNOSTICS_ID 0x1001 +#define XF_REG_STATE0_ID 0x1002 +#define XF_REG_STATE1_ID 0x1003 +#define XF_REG_CLOCK_ID 0x1004 +#define XF_REG_CLIPDISABLE_ID 0x1005 +#define XF_REG_PERF0_ID 0x1006 +#define XF_REG_PERF1_ID 0x1007 +#define XF_REG_INVERTEXSPEC_ID 0x1008 +#define XF_REG_NUMCOLORS_ID 0x1009 +#define XF_REG_DUALTEXTRAN_ID 0x1012 +#define XF_REG_SCALEX_ID 0x101A +#define XF_REG_SCALEY_ID 0x101B +#define XF_REG_SCALEZ_ID 0x101C +#define XF_REG_OFFSETX_ID 0x101D +#define XF_REG_OFFSETY_ID 0x101E +#define XF_REG_OFFSETZ_ID 0x101F +#define XF_REG_NUMTEX_ID 0x103F +#define XF_REG_TEX0_ID 0x1040 +#define XF_REG_TEX1_ID 0x1041 +#define XF_REG_TEX2_ID 0x1042 +#define XF_REG_TEX3_ID 0x1043 +#define XF_REG_TEX4_ID 0x1044 +#define XF_REG_TEX5_ID 0x1045 +#define XF_REG_TEX6_ID 0x1046 +#define XF_REG_TEX7_ID 0x1047 +#define XF_REG_DUALTEX0_ID 0x1050 +#define XF_REG_DUALTEX1_ID 0x1051 +#define XF_REG_DUALTEX2_ID 0x1052 +#define XF_REG_DUALTEX3_ID 0x1053 +#define XF_REG_DUALTEX4_ID 0x1054 +#define XF_REG_DUALTEX5_ID 0x1055 +#define XF_REG_DUALTEX6_ID 0x1056 +#define XF_REG_DUALTEX7_ID 0x1057 + +#define XF_REG_INVTXSPEC(ncols, nnorms, ntexs) \ + ( \ + (ncols) << 0 | \ + (nnorms) << 2 | \ + (ntexs) << 4 \ + ) + +#define XF_REG_TEX(proj, form, tgType, row, embossRow, embossLit) \ + ( \ + (proj) << 1 | \ + (form) << 2 | \ + (tgType) << 4 | \ + (row) << 7 | \ + (embossRow) << 12 | \ + (embossLit) << 15 \ + ) + +#define XF_REG_DUALTEX(mtx, normalize) \ + ( \ + (mtx) << 0 | \ + (normalize) << 8 \ + ) + +#define BP_GEN_MODE(nTexGens, nChans, nTevs, p4, nInds) \ + ( \ + (u32)(nTexGens) << 0 | \ + (u32)(nChans) << 4 | \ + (u32)(nTevs) << 10 | \ + (u32)(p4) << 14 | \ + (u32)(nInds) << 16 \ + ) + +#define BP_LP_SIZE(lineWidth, pointSize, lineOffset, pointOffset, lineHalfAspect, p5) \ + ( \ + (u32)(lineWidth) << 0 | \ + (u32)(pointSize) << 8 | \ + (u32)(lineOffset) << 16 | \ + (u32)(pointOffset) << 19 | \ + (u32)(lineHalfAspect) << 22 | \ + (u32)(p5) << 24 \ + ) + +void GDSetVtxDescv(const GXVtxDescList* attrPtr); +void GDSetVtxAttrFmtv(GXVtxFmt vtxfmt, const GXVtxAttrFmtList* list); +void GDSetArraySized(GXAttr attr, void* base_ptr, u32 size, u8 stride, bool le); +void GDSetArray(GXAttr attr, void* base_ptr, u8 stride); +void GDSetArrayRaw(GXAttr attr, u32 base_ptr_raw, u8 stride); +void GDPatchArrayPtr(void* base_ptr); +void GDSetTexCoordGen(GXTexCoordID dst_coord, GXTexGenType func, GXTexGenSrc src_param, u8 normalize, u32 postmtx); +void GDSetCullMode(GXCullMode mode); +void GDSetGenMode(u8 nTexGens, u8 nChans, u8 nTevs); +void GDSetGenMode2(u8 nTexGens, u8 nChans, u8 nTevs, u8 nInds, GXCullMode cm); +void GDSetLPSize(u8 lineWidth, u8 pointSize, GXTexOffset lineOffset, GXTexOffset pointOffset, u8 lineHalfAspect); +void GDSetCoPlanar(u8 enable); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gd/GDIndirect.h b/aurora-main/include/dolphin/gd/GDIndirect.h new file mode 100644 index 0000000..4ca854c --- /dev/null +++ b/aurora-main/include/dolphin/gd/GDIndirect.h @@ -0,0 +1,91 @@ +#ifndef _DOLPHIN_GD_INDIRECT_H_ +#define _DOLPHIN_GD_INDIRECT_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define BP_TEV_INDIRECT(ind_stage, format, bias_sel, alpha_sel, mtx_sel, wrap_s, wrap_t, utc_lod, add_prev, id) \ + ( \ + (u32)(ind_stage) << 0 | \ + (u32)(format) << 2 | \ + (u32)(bias_sel) << 4 | \ + (u32)(alpha_sel) << 7 | \ + (u32)(mtx_sel) << 9 | \ + (u32)(wrap_s) << 13 | \ + (u32)(wrap_t) << 16 | \ + (u32)(utc_lod) << 19 | \ + (u32)(add_prev) << 20 | \ + (u32)(id) << 24 \ + ) + +#define BP_IND_MTX(m0, m1, scale_exp, id) \ + ( \ + (u32)(m0) << 0 | \ + (u32)(m1) << 11 | \ + (u32)(scale_exp) << 22 | \ + (u32)(id) << 24 \ + ) + +#define BP_IND_TEXCOORD_SCALE(scaleS0, scaleT0, scaleS1, scaleT1, id) \ + ( \ + (u32)(scaleS0) << 0 | \ + (u32)(scaleT0) << 4 | \ + (u32)(scaleS1) << 8 | \ + (u32)(scaleT1) << 12 | \ + (u32)(id) << 24 \ + ) + +#define BP_IND_TEX_ORDER(map0, coord0, map1, coord1, map2, coord2, map3, coord3, id) \ + ( \ + (u32)(map0) << 0 | \ + (u32)(coord0) << 3 | \ + (u32)(map1) << 6 | \ + (u32)(coord1) << 9 | \ + (u32)(map2) << 12 | \ + (u32)(coord2) << 15 | \ + (u32)(map3) << 18 | \ + (u32)(coord3) << 21 | \ + (u32)(id) << 24 \ + ) + +#define BP_IND_MASK(mask, id) \ + ( \ + (u32)(mask) << 0 | \ + (u32)(id) << 24 \ + ) + +void GDSetTevIndirect(GXTevStageID tev_stage, GXIndTexStageID ind_stage, + GXIndTexFormat format, GXIndTexBiasSel bias_sel, + GXIndTexMtxID matrix_sel, GXIndTexWrap wrap_s, + GXIndTexWrap wrap_t, u8 add_prev, u8 utc_lod, + GXIndTexAlphaSel alpha_sel); +void GDSetIndTexMtx(GXIndTexMtxID mtx_id, const f32 offset[2][3], s8 scale_exp); +void GDSetIndTexCoordScale(GXIndTexStageID indStageEven, GXIndTexScale scaleS0, + GXIndTexScale scaleT0, GXIndTexScale scaleS1, + GXIndTexScale scaleT1); +void GDSetIndTexOrder(GXTexCoordID texCoord0, GXTexMapID texMap0, + GXTexCoordID texCoord1, GXTexMapID texMap1, + GXTexCoordID texCoord2, GXTexMapID texMap2, + GXTexCoordID texCoord3, GXTexMapID texMap3); +void GDSetTevDirect(GXTevStageID tev_stage); +void GDSetTevIndWarp(GXTevStageID tev_stage, GXIndTexStageID ind_stage, u8 signed_offset, u8 replace_mode, GXIndTexMtxID matrix_sel); +void GDSetTevIndTile(GXTevStageID tev_stage, GXIndTexStageID ind_stage, + u16 tilesize_s, u16 tilesize_t, u16 tilespacing_s, + u16 tilespacing_t, GXIndTexFormat format, + GXIndTexMtxID matrix_sel, GXIndTexBiasSel bias_sel, + GXIndTexAlphaSel alpha_sel); +void GDSetTevIndBumpST(GXTevStageID tev_stage, GXIndTexStageID ind_stage, GXIndTexMtxID matrix_sel); +void GDSetTevIndBumpXYZ(GXTevStageID tev_stage, GXIndTexStageID ind_stage, GXIndTexMtxID matrix_sel); +void GDSetTevIndRepeat(GXTevStageID tev_stage); +void __GDSetIndTexMask(u32 mask); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gd/GDLight.h b/aurora-main/include/dolphin/gd/GDLight.h new file mode 100644 index 0000000..512a9d7 --- /dev/null +++ b/aurora-main/include/dolphin/gd/GDLight.h @@ -0,0 +1,65 @@ +#ifndef _DOLPHIN_GD_LIGHT_H_ +#define _DOLPHIN_GD_LIGHT_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define XF_LIGHT_ID 0x0600 +#define XF_LIGHT_COLOR_ID 0x0603 +#define XF_LIGHT_ATTN_ID 0x0604 +#define XF_LIGHT_DISTATTN_ID 0x0607 +#define XF_LIGHT_POS_ID 0x060A +#define XF_LIGHT_SPEC_DIR_ID 0x060A +#define XF_LIGHT_DIR_ID 0x060D + +#define XF_REG_AMBIENT0_ID 0x100A +#define XF_REG_AMBIENT1_ID 0x100B +#define XF_REG_MATERIAL0_ID 0x100C +#define XF_REG_MATERIAL1_ID 0x100D +#define XF_REG_COLOR0CNTRL_ID 0x100E +#define XF_REG_COLOR1CNTRL_ID 0x100F +#define XF_REG_ALPHA0CNTRL_ID 0x1010 +#define XF_REG_ALPHA1CNTRL_ID 0x1011 + +#define XF_REG_CHAN_CTRL(matsrc, enable, lightmask0, ambsrc, diff_fn, p5, p6, lightmask4) \ + ( \ + (u32)(matsrc) << 0 | \ + (u32)(enable) << 1 | \ + (u32)(lightmask0) << 2 | \ + (u32)(ambsrc) << 6 | \ + (u32)(diff_fn) << 7 | \ + (u32)(p5) << 9 | \ + (u32)(p6) << 10 | \ + (u32)(lightmask4) << 11 \ + ) + +void GDSetLightAttn(GXLightID light, f32 a0, f32 a1, f32 a2, f32 k0, f32 k1, f32 k2); +void GDSetLightSpot(GXLightID light, f32 cutoff, GXSpotFn spot_func); +void GDSetLightDistAttn(GXLightID light, f32 ref_dist, f32 ref_br, GXDistAttnFn dist_func); +void GDSetLightColor(GXLightID light, GXColor color); +void GDSetLightPos(GXLightID light, f32 x, f32 y, f32 z); +void GDSetLightDir(GXLightID light, f32 nx, f32 ny, f32 nz); +void GDSetSpecularDirHA(GXLightID light, f32 nx, f32 ny, f32 nz, f32 hx, f32 hy, f32 hz); +void GDSetSpecularDir(GXLightID light, f32 nx, f32 ny, f32 nz); +void GDLoadLightObjIndx(u32 lt_obj_indx, GXLightID light); +void GDSetChanAmbColor(GXChannelID chan, GXColor color); +void GDSetChanMatColor(GXChannelID chan, GXColor color); +void GDSetChanCtrl(GXChannelID chan, u8 enable, GXColorSrc amb_src, + GXColorSrc mat_src, u32 light_mask, GXDiffuseFn diff_fn, + GXAttnFn attn_fn); + +u16 __GDLightID2Index(GXLightID id); + +inline static u16 __GDLightID2Offset(GXLightID id) { + return __GDLightID2Index(id) * 16; +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gd/GDPixel.h b/aurora-main/include/dolphin/gd/GDPixel.h new file mode 100644 index 0000000..158b04e --- /dev/null +++ b/aurora-main/include/dolphin/gd/GDPixel.h @@ -0,0 +1,95 @@ +#ifndef _DOLPHIN_GD_PIXEL_H +#define _DOLPHIN_GD_PIXEL_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define BP_FOG_UNK0(a, id) \ + ( \ + (u32)(a) << 0 | \ + (u32)(id) << 24 \ + ) + +#define BP_FOG_UNK1(b_m, id) \ + ( \ + (u32)(b_m) << 0 | \ + (u32)(id) << 24 \ + ) + +#define BP_FOG_UNK2(b_expn, id) \ + ( \ + (u32)(b_expn) << 0 | \ + (u32)(id) << 24 \ + ) + +#define BP_FOG_UNK3(c, proj, fsel, id) \ + ( \ + (u32)(c) << 0 | \ + (u32)(proj) << 20 | \ + (u32)(fsel) << 21 | \ + (u32)(id) << 24 \ + ) + +#define BP_FOG_COLOR(r, g, b, id) \ + ( \ + (u32)(b) << 0 | \ + (u32)(g) << 8 | \ + (u32)(r) << 16 | \ + (u32)(id) << 24 \ + ) + +#define BP_BLEND_MODE(enable, enable_logic, enable_dither, enable_color_update, enable_alpha_update, dst_factor, src_factor, blend_sub, logic_op, id) \ + ( \ + (u32)(enable) << 0 | \ + (u32)(enable_logic) << 1 | \ + (u32)(enable_dither) << 2 | \ + (u32)(enable_color_update) << 3 | \ + (u32)(enable_alpha_update) << 4 | \ + (u32)(dst_factor) << 5 | \ + (u32)(src_factor) << 8 | \ + (u32)(blend_sub) << 11 | \ + (u32)(logic_op) << 12 | \ + (u32)(id) << 24 \ + ) + +#define BP_Z_MODE(enable_compare, compare_fn, enable_update, id) \ + ( \ + (u32)(enable_compare) << 0 | \ + (u32)(compare_fn) << 1 | \ + (u32)(enable_update) << 4 | \ + (u32)(id) << 24 \ + ) + +#define BP_DST_ALPHA(alpha, enable, id) \ + ( \ + (u32)(alpha) << 0 | \ + (u32)(enable) << 8 | \ + (u32)(id) << 24 \ + ) + +#define BP_TOKEN(token, id) \ + ( \ + (u32)(token) << 0 | \ + (u32)(id) << 24 \ + ) + +void GDSetFog(GXFogType type, f32 startz, f32 endz, f32 nearz, f32 farz, GXColor color); +void GDSetBlendMode(GXBlendMode type, GXBlendFactor src_factor, GXBlendFactor dst_factor, GXLogicOp logic_op); +void GDSetBlendModeEtc(GXBlendMode type, GXBlendFactor src_factor, + GXBlendFactor dst_factor, GXLogicOp logic_op, + u8 color_update_enable, + u8 alpha_update_enable, + u8 dither_enable); +void GDSetZMode(u8 compare_enable, GXCompare func, u8 update_enable); +void GDSetDstAlpha(u8 enable, u8 alpha); +void GDSetDrawSync(u16 token); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gd/GDTev.h b/aurora-main/include/dolphin/gd/GDTev.h new file mode 100644 index 0000000..baec8b5 --- /dev/null +++ b/aurora-main/include/dolphin/gd/GDTev.h @@ -0,0 +1,133 @@ +#ifndef _DOLPHIN_GD_TEV_H +#define _DOLPHIN_GD_TEV_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define BP_TEV_COLOR(d, c, b, a, bias, op, clamp, scale, out, id) \ + ( \ + (u32)(d) << 0 | \ + (u32)(c) << 4 | \ + (u32)(b) << 8 | \ + (u32)(a) << 12 | \ + (u32)(bias) << 16 | \ + (u32)(op) << 18 | \ + (u32)(clamp) << 19 | \ + (u32)(scale) << 20 | \ + (u32)(out) << 22 | \ + (u32)(id) << 24 \ + ) + +#define BP_TEV_ALPHA(ras_sel, tex_sel, d, c, b, a, bias, op, clamp, scale, out, id) \ + ( \ + (u32)(ras_sel) << 0 | \ + (u32)(tex_sel) << 2 | \ + (u32)(d) << 4 | \ + (u32)(c) << 7 | \ + (u32)(b) << 10 | \ + (u32)(a) << 13 | \ + (u32)(bias) << 16 | \ + (u32)(op) << 18 | \ + (u32)(clamp) << 19 | \ + (u32)(scale) << 20 | \ + (u32)(out) << 22 | \ + (u32)(id) << 24 \ + ) + +#define BP_TEV_COLOR_REG_RA(r, a, reg, id) \ + ( \ + (u32)(r) << 0 | \ + (u32)(a) << 12 | \ + (u32)(reg) << 23 | \ + (u32)(id) << 24 \ + ) + +#define BP_TEV_COLOR_REG_BG(b, g, reg, id) \ + ( \ + (u32)(b) << 0 | \ + (u32)(g) << 12 | \ + (u32)(reg) << 23 | \ + (u32)(id) << 24 \ + ) + +#define BP_TEV_KSEL(rb, ga, kcsel0, kasel0, kcsel1, kasel1, id) \ + ( \ + (u32)(rb) << 0 | \ + (u32)(ga) << 2 | \ + (u32)(kcsel0) << 4 | \ + (u32)(kasel0) << 9 | \ + (u32)(kcsel1) << 14 | \ + (u32)(kasel1) << 19 | \ + (u32)(id) << 24 \ + ) + +#define BP_ALPHA_COMPARE(ref0, ref1, comp0, comp1, op, id) \ + ( \ + (u32)(ref0) << 0 | \ + (u32)(ref1) << 8 | \ + (u32)(comp0) << 16 | \ + (u32)(comp1) << 19 | \ + (u32)(op) << 22 | \ + (u32)(id) << 24 \ + ) + +#define BP_ZTEX_PARAMS_0(bias, id) \ + ( \ + (u32)(bias) << 0 | \ + (u32)(id) << 24 \ + ) + +#define BP_ZTEX_PARAMS_1(zfmt, op, id) \ + ( \ + (u32)(zfmt) << 0 | \ + (u32)(op) << 2 | \ + (u32)(id) << 24 \ + ) + +#define BP_TEV_ORDER(map0, coord0, enable0, color0, map1, coord1, enable1, color1, id) \ + ( \ + (u32)(map0) << 0 | \ + (u32)(coord0) << 3 | \ + (u32)(enable0) << 6 | \ + (u32)(color0) << 7 | \ + (u32)(map1) << 12 | \ + (u32)(coord1) << 15 | \ + (u32)(enable1) << 18 | \ + (u32)(color1) << 19 | \ + (u32)(id) << 24 \ + ) + +void GDSetTevOp(GXTevStageID stage, GXTevMode mode); +void GDSetTevColorCalc(GXTevStageID stage, GXTevColorArg a, GXTevColorArg b, + GXTevColorArg c, GXTevColorArg d, GXTevOp op, + GXTevBias bias, GXTevScale scale, u8 clamp, + GXTevRegID out_reg); +void GDSetTevAlphaCalcAndSwap(GXTevStageID stage, GXTevAlphaArg a, + GXTevAlphaArg b, GXTevAlphaArg c, GXTevAlphaArg d, + GXTevOp op, GXTevBias bias, GXTevScale scale, + u8 clamp, GXTevRegID out_reg, + GXTevSwapSel ras_sel, GXTevSwapSel tex_sel); +void GDSetTevColor(GXTevRegID reg, GXColor color); +void GDSetTevColorS10(GXTevRegID reg, GXColorS10 color); +void GDSetTevKColor(GXTevKColorID reg, GXColor color); +void GDSetTevKonstantSel(GXTevStageID evenStage, GXTevKColorSel kcsel0, + GXTevKAlphaSel kasel0, GXTevKColorSel kcsel1, + GXTevKAlphaSel kasel1); +void GDSetTevSwapModeTable(GXTevSwapSel table, GXTevColorChan red, + GXTevColorChan green, GXTevColorChan blue, + GXTevColorChan alpha); +void GDSetAlphaCompare(GXCompare comp0, u8 ref0, GXAlphaOp op, GXCompare comp1, u8 ref1); +void GDSetZTexture(GXZTexOp op, GXTexFmt fmt, u32 bias); +void GDSetTevOrder(GXTevStageID evenStage, GXTexCoordID coord0, GXTexMapID map0, + GXChannelID color0, GXTexCoordID coord1, GXTexMapID map1, + GXChannelID color1); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gd/GDTexture.h b/aurora-main/include/dolphin/gd/GDTexture.h new file mode 100644 index 0000000..2370cbb --- /dev/null +++ b/aurora-main/include/dolphin/gd/GDTexture.h @@ -0,0 +1,127 @@ +#ifndef _DOLPHIN_GD_TEXTURE_H +#define _DOLPHIN_GD_TEXTURE_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define BP_TEX_MODE0(wrap_s, wrap_t, mag_filt, min_filt, edge_lod, lod_bias, max_aniso, bias_clamp, id) \ + ( \ + (u32)(wrap_s) << 0 | \ + (u32)(wrap_t) << 2 | \ + (u32)(mag_filt) << 4 | \ + (u32)(min_filt) << 5 | \ + (u32)(edge_lod) << 8 | \ + (u32)(lod_bias) << 9 | \ + (u32)(max_aniso) << 19 | \ + (u32)(bias_clamp) << 21 | \ + (u32)(id) << 24 \ + ) + +#define BP_TEX_MODE1(min_lod, max_lod, id) \ + ( \ + (u32)(min_lod) << 0 | \ + (u32)(max_lod) << 8 | \ + (u32)(id) << 24 \ + ) + +#define BP_IMAGE_ATTR(width, height, format, id) \ + ( \ + (u32)(width) << 0 | \ + (u32)(height) << 10 | \ + (u32)(format) << 20 | \ + (u32)(id) << 24 \ + ) + +#define BP_IMAGE_PTR(ptr, id) \ + ( \ + (u32)(ptr) << 0 | \ + (u32)(id) << 24 \ + ) + +#define BP_TEX_CACHE_EVEN(tmem, size0, size1, type, id) \ + ( \ + (u32)(tmem) << 0 | \ + (u32)(size0) << 15 | \ + (u32)(size1) << 18 | \ + (u32)(type) << 21 | \ + (u32)(id) << 24 \ + ) + +#define BP_TEX_CACHE_ODD(tmem, size0, size1, id) \ + ( \ + (u32)(tmem) << 0 | \ + (u32)(size0) << 15 | \ + (u32)(size1) << 18 | \ + (u32)(id) << 24 \ + ) + +#define BP_TEX_TLUT(tmem, format, id) \ + ( \ + (u32)(tmem) << 0 | \ + (u32)(format) << 10 | \ + (u32)(id) << 24 \ + ) + +#define BP_TEXCOORD_S_SCALE(s_scale, s_bias, s_wrap, line_offset, point_offset, id) \ + ( \ + (u32)(s_scale) << 0 | \ + (u32)(s_bias) << 16 | \ + (u32)(s_wrap) << 17 | \ + (u32)(line_offset) << 18 | \ + (u32)(point_offset) << 19 | \ + (u32)(id) << 24 \ + ) + +#define BP_TEXCOORD_T_SCALE(t_scale, t_bias, t_wrap, id) \ + ( \ + (u32)(t_scale) << 0 | \ + (u32)(t_bias) << 16 | \ + (u32)(t_wrap) << 17 | \ + (u32)(id) << 24 \ + ) + +#define BP_LOAD_TLUT0(ptr, id) \ + ( \ + (u32)(ptr) << 0 | \ + (u32)(id) << 24 \ + ) + +#define BP_LOAD_TLUT1(ptr, size, id) \ + ( \ + (u32)(ptr) << 0 | \ + (u32)(size) << 10 | \ + (u32)(id) << 24 \ + ) + +void GDSetTexLookupMode(GXTexMapID id, GXTexWrapMode wrap_s, + GXTexWrapMode wrap_t, GXTexFilter min_filt, + GXTexFilter mag_filt, f32 min_lod, f32 max_lod, + f32 lod_bias, u8 bias_clamp, u8 do_edge_lod, + GXAnisotropy max_aniso); + +void GDSetTexImgAttr(GXTexMapID id, u16 width, u16 height, GXTexFmt format); +void GDSetTexImgPtr(GXTexMapID id, void* image_ptr); +void GDSetTexImgPtrRaw(GXTexMapID id, u32 image_ptr_raw); +void GDPatchTexImgPtr(void* image_ptr); +void GDSetTexCached(GXTexMapID id, u32 tmem_even, GXTexCacheSize size_even, + u32 tmem_odd, GXTexCacheSize size_odd); +void GDSetTexPreLoaded(GXTexMapID id, u32 tmem_even, u32 tmem_odd); +void GDSetTexTlut(GXTexMapID id, u32 tmem_addr, GXTlutFmt format); +void GDSetTexCoordScale(GXTexCoordID coord, u16 s_scale, u16 t_scale); +void GDSetTexCoordScale2(GXTexCoordID coord, u16 s_scale, u8 s_bias, + u8 s_wrap, u16 t_scale, u8 t_bias, u8 t_wrap); +void GDSetTexCoordScaleAndTOEs(GXTexCoordID coord, u16 s_scale, u8 s_bias, + u8 s_wrap, u16 t_scale, u8 t_bias, u8 t_wrap, + u8 line_offset, u8 point_offset); +void GDLoadTlut(void* tlut_ptr, u32 tmem_addr, GXTlutSize size); +void GDLoadTlutRaw(u32 tlut_ptr_raw, u32 tmem_addr, GXTlutSize size); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gd/GDTransform.h b/aurora-main/include/dolphin/gd/GDTransform.h new file mode 100644 index 0000000..8d0159f --- /dev/null +++ b/aurora-main/include/dolphin/gd/GDTransform.h @@ -0,0 +1,56 @@ +#ifndef _DOLPHIN_GD_TRANSFORM_H_ +#define _DOLPHIN_GD_TRANSFORM_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define CP_MTX_REG_A_ID 0x30 +#define CP_MTX_REG_B_ID 0x40 + +#define XF_REG_MATRIXINDEX0_ID 0x1018 +#define XF_REG_MATRIXINDEX1_ID 0x1019 +#define XF_REG_PROJECTIONA_ID 0x1020 +#define XF_REG_PROJECTIONB_ID 0x1021 +#define XF_REG_PROJECTIONC_ID 0x1022 +#define XF_REG_PROJECTIOND_ID 0x1023 +#define XF_REG_PROJECTIONE_ID 0x1024 +#define XF_REG_PROJECTIONF_ID 0x1025 +#define XF_REG_PROJECTORTHO_ID 0x1026 + +#define CP_MTX_REG_A(pn, t0, t1, t2, t3) \ + ( \ + (u32)(pn) << 0 | \ + (u32)(t0) << 6 | \ + (u32)(t1) << 12 | \ + (u32)(t2) << 18 | \ + (u32)(t3) << 24 \ + ) + +#define CP_MTX_REG_B(t4, t5, t6, t7) \ + ( \ + (u32)(t4) << 0 | \ + (u32)(t5) << 6 | \ + (u32)(t6) << 12 | \ + (u32)(t7) << 18 \ + ) + +void GDLoadPosMtxImm(const Mtx mtx, u32 id); +void GDLoadPosMtxIndx(u16 mtx_indx, u32 id); +void GDLoadNrmMtxImm(const Mtx mtx, u32 id); +void GDLoadNrmMtxImm3x3(const f32 mtx[3][3], u32 id); +void GDLoadNrmMtxIndx3x3(u16 mtx_indx, u32 id); +void GDLoadTexMtxImm(const Mtx mtx, u32 id, GXTexMtxType type); +void GDLoadTexMtxIndx(u16 mtx_indx, u32 id, GXTexMtxType type); +void GDSetCurrentMtx(u32 pn, u32 t0, u32 t1, u32 t2, u32 t3, u32 t4, u32 t5, u32 t6, u32 t7); +void GDSetProjection(const Mtx44 mtx, GXProjectionType type); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gf.h b/aurora-main/include/dolphin/gf.h new file mode 100644 index 0000000..50676ae --- /dev/null +++ b/aurora-main/include/dolphin/gf.h @@ -0,0 +1,9 @@ +#ifndef _DOLPHIN_GF_H_ +#define _DOLPHIN_GF_H_ + +#include +#include +#include +#include + +#endif diff --git a/aurora-main/include/dolphin/gf/GFGeometry.h b/aurora-main/include/dolphin/gf/GFGeometry.h new file mode 100644 index 0000000..c0866a5 --- /dev/null +++ b/aurora-main/include/dolphin/gf/GFGeometry.h @@ -0,0 +1,41 @@ +#ifndef _DOLPHIN_GF_GFGEOMETRY_H_ +#define _DOLPHIN_GF_GFGEOMETRY_H_ + +#include + +#define GF_GEN_MODE(nTexGens, nChans, nTevs, cm, nInds) \ + ( \ + (u32)(nTexGens) << 0 | \ + (u32)(nChans) << 4 | \ + (u32)(nTevs) << 10 | \ + (u32)(cm) << 14 | \ + (u32)(nInds) << 16 \ + ) + +static inline void GFWrite_u8(u8 data) { + GXCmd1u8(data); +} + +static inline void GFWrite_u32(u32 data) { + GXCmd1u32(data); +} + +static inline void GFWrite_u16(u16 data) { + GXCmd1u16(data); +} + +static inline void GFWriteBPCmd(u32 regval) { + GFWrite_u8(GX_LOAD_BP_REG); + GFWrite_u32(regval); +} + +static inline void GFWriteXFCmd(u16 addr, u32 val) { + GFWrite_u8(GX_LOAD_XF_REG); + GFWrite_u16(0); + GFWrite_u16(addr); + GFWrite_u32(val); +} + +void GFSetGenMode2(u8 nTexGens, u8 nChans, u8 nTevs, u8 nInds, GXCullMode cm); + +#endif /* _DOLPHIN_GF_GFGEOMETRY_H_ */ diff --git a/aurora-main/include/dolphin/gf/GFLight.h b/aurora-main/include/dolphin/gf/GFLight.h new file mode 100644 index 0000000..3c4e222 --- /dev/null +++ b/aurora-main/include/dolphin/gf/GFLight.h @@ -0,0 +1,8 @@ +#ifndef _DOLPHIN_GF_GFLIGHT_H +#define _DOLPHIN_GF_GFLIGHT_H + +#include + +void GFSetChanAmbColor(GXChannelID chan, GXColor color); + +#endif /* _DOLPHIN_GF_GFLIGHT_H */ diff --git a/aurora-main/include/dolphin/gf/GFPixel.h b/aurora-main/include/dolphin/gf/GFPixel.h new file mode 100644 index 0000000..93b7ff9 --- /dev/null +++ b/aurora-main/include/dolphin/gf/GFPixel.h @@ -0,0 +1,13 @@ +#ifndef _DOLPHIN_GF_GFPIXEL_H +#define _DOLPHIN_GF_GFPIXEL_H + +#include + +void GFSetFog(GXFogType type, f32 startz, f32 endz, f32 nearz, f32 farz, GXColor color); +void GFSetBlendModeEtc(GXBlendMode type, GXBlendFactor src_factor, + GXBlendFactor dst_factor, GXLogicOp logic_op, + u8 color_update_enable, u8 alpha_update_enable, + u8 dither_enable); +void GFSetZMode(u8 compare_enable, GXCompare func, u8 update_enable); + +#endif /* _DOLPHIN_GF_GFPIXEL_H */ diff --git a/aurora-main/include/dolphin/gf/GFTev.h b/aurora-main/include/dolphin/gf/GFTev.h new file mode 100644 index 0000000..ade4c41 --- /dev/null +++ b/aurora-main/include/dolphin/gf/GFTev.h @@ -0,0 +1,8 @@ +#ifndef _DOLPHIN_GF_GFTEV_H +#define _DOLPHIN_GF_GFTEV_H + +#include + +void GFSetTevColorS10(GXTevRegID reg, GXColorS10 color); + +#endif /* _DOLPHIN_GF_GFTEV_H */ diff --git a/aurora-main/include/dolphin/gx.h b/aurora-main/include/dolphin/gx.h new file mode 100644 index 0000000..b4cf7ef --- /dev/null +++ b/aurora-main/include/dolphin/gx.h @@ -0,0 +1,34 @@ +#ifndef DOLPHIN_GX_H +#define DOLPHIN_GX_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gx/GXAurora.h b/aurora-main/include/dolphin/gx/GXAurora.h new file mode 100644 index 0000000..953a19a --- /dev/null +++ b/aurora-main/include/dolphin/gx/GXAurora.h @@ -0,0 +1,148 @@ +#ifndef DOLPHIN_GXAURORA_H +#define DOLPHIN_GXAURORA_H + +#include + +#if __cplusplus +extern "C" { +#endif + +// +// Subcommands for GX_LOAD_AURORA. +// + +/** + * Sets the actual render viewport in native framebuffer coordinates. + * Must be followed by six f32 values: left, top, width, height, nearz, farz. + */ +#define GX_LOAD_AURORA_VIEWPORT_RENDER 0x0001 + +/** + * Sets the actual render scissor in native framebuffer coordinates. + * Must be followed by four u32 values: left, top, width, height. + */ +#define GX_LOAD_AURORA_SCISSOR_RENDER 0x0002 + +/** + * Aurora equivalent of CP_REG_ARRAYBASE_ID: sets the base address and size of a vertex array. + * This command must be followed by a 64-bit memory address, 32-bit size, and 1-byte little-endian flag. + * The index of the vertex array is given by the lowest 4 bits of the command ID, + * e.g. writing GX_LOAD_AURORA_ARRAYBASE + 5 will set the vertex array for the sixth vertex attribute. + * To set strides, use the normal CP_REG_ARRAYSTRIDE_ID register. + */ +#define GX_LOAD_AURORA_ARRAYBASE 0x0010 + +/** + * Pushes a debug group to the backend graphics API. These may show in debugging tools such as RenderDoc. + * Must be followed by a u16 string length and that many UTF-8 characters (no null terminator required). + * It is considered an error to have unpopped debug groups at the end of the frame. They will be automatically cleared. + */ +#define GX_LOAD_AURORA_DEBUG_GROUP_PUSH 0x0020 + +/** + * Pops a previously pushed debug group. + * Followed by nothing. + */ +#define GX_LOAD_AURORA_DEBUG_GROUP_POP 0x0021 + +/** + * Sends a debug marker to the backend graphics API. + * Must be followed by a u16 string length and that many UTF-8 characters (no null terminator required). + */ +#define GX_LOAD_AURORA_DEBUG_MARKER_INSERT 0x0022 + +#define GX_LOAD_AURORA_TEXOBJ 0x0030 + +#define GX_LOAD_AURORA_TLUT 0x0031 + +#define GX_LOAD_AURORA_DESTROY_TEXOBJ 0x0032 + +#define GX_LOAD_AURORA_DESTROY_TLUT 0x0033 + +#define GX_LOAD_AURORA_DESTROY_COPY_TEX 0x0034 + +#define GX_LOAD_AURORA_INVALIDATE_TEX_ALL 0x0035 + + +/* + * Debug marker stuff + */ + +/** + * Pushes a debug group to the backend graphics API. These may show in debugging tools such as RenderDoc. + * It is considered an error to have unpopped debug groups at the end of the frame. They will be automatically cleared. + */ +void GXPushDebugGroup(const char* label); + +/** + * Pop a debug group previously pushed via GXPushDebugGroup(). + */ +void GXPopDebugGroup(); + +/** + * Sends a debug marker to the backend graphics API. These may show in debugging tools such as RenderDoc. + */ +void GXInsertDebugMarker(const char* label); + +typedef enum _AuroraViewportPolicy { + AURORA_VIEWPORT_FIT = 0, // Preserve logical aspect in the content framebuffer + AURORA_VIEWPORT_STRETCH = 1, // Match content framebuffer aspect to the native surface + AURORA_VIEWPORT_NATIVE = 2, // Use active framebuffer pixels directly +} AuroraViewportPolicy; + +/** + * Configures content framebuffer sizing and how GXSetViewport/GXSetScissor parameters are applied to rendering. + * When AURORA_VIEWPORT_NATIVE is used, GXSetTexCopySrc/GXSetTexCopyDst will use native framebuffer resolution. + */ +void AuroraSetViewportPolicy(AuroraViewportPolicy policy); + +/** + * Retrieves the current content framebuffer size. + */ +void AuroraGetRenderSize(u32* width, u32* height); + +/** Retrieves the current native presentation-surface size, ignoring the GX sizing policy. */ +void AuroraGetSurfaceSize(u32* width, u32* height); + +/** + * Sets the actual render viewport in native framebuffer coordinates. + * Overrides the automatically scaled values set by the logical GXSetViewport. + */ +void GXSetViewportRender(f32 left, f32 top, f32 wd, f32 ht, f32 nearz, f32 farz); + +/** + * Sets the actual render scissor in native framebuffer coordinates. + * Overrides the automatically scaled values set by the logical GXSetScissor. + */ +void GXSetScissorRender(u32 left, u32 top, u32 wd, u32 ht); + +/** Maps the logical GX viewport and scissor into a centered safe area, for fixed-aspect UI bridges. */ +void GXSetViewportScissorRenderSafeArea(f32 aspect); + +/** Restores the viewport and scissor from logical GX state after GXSetViewportScissorRenderSafeArea. */ +void GXRestoreViewportScissorRender(void); + +/** Sets the texture-copy source in active render-target coordinates, for bridges that already + * resolved VI/window scaling. */ +void GXSetTexCopySrcRender(u16 left, u16 top, u16 wd, u16 ht); + +/** + * Create an offscreen framebuffer and switch rendering to it. + * All subsequent GX rendering will target this framebuffer until GXRestoreFrameBuffer() is called. + * Use GXCopyTex to resolve the offscreen content into a texture. + */ +void GXCreateFrameBuffer(u32 width, u32 height); + +/** + * Restore rendering to the main EFB framebuffer. + * Must be called after GXCreateFrameBuffer() to resume normal rendering. + */ +void GXRestoreFrameBuffer(void); + +void GXApplyBPReg(u8 reg, u32 value); + +#if __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gx/GXBump.h b/aurora-main/include/dolphin/gx/GXBump.h new file mode 100644 index 0000000..aa99477 --- /dev/null +++ b/aurora-main/include/dolphin/gx/GXBump.h @@ -0,0 +1,32 @@ +#ifndef DOLPHIN_GXBUMP_H +#define DOLPHIN_GXBUMP_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void GXSetTevDirect(GXTevStageID tev_stage); +void GXSetTevIndTile(GXTevStageID tevStage, GXIndTexStageID indStage, u16 tileSizeS, u16 tileSizeT, u16 tileSizeS_exp, + u16 tileSizeT_exp, GXIndTexFormat fmt, GXIndTexMtxID matrixSel, GXIndTexBiasSel biasSel, + GXIndTexAlphaSel alphaSel); +void GXSetNumIndStages(u8 nIndStages); +#ifdef TARGET_PC +void GXSetIndTexMtx(GXIndTexMtxID mtx_sel, const void* offset, s8 scale_exp); +#else +void GXSetIndTexMtx(GXIndTexMtxID mtx_sel, f32 offset[2][3], s8 scale_exp); +#endif +void GXSetIndTexOrder(GXIndTexStageID ind_stage, GXTexCoordID tex_coord, GXTexMapID tex_map); +void GXSetIndTexCoordScale(GXIndTexStageID indStage, GXIndTexScale scaleS, GXIndTexScale scaleT); +void GXSetTevIndirect(GXTevStageID tev_stage, GXIndTexStageID ind_stage, GXIndTexFormat format, + GXIndTexBiasSel bias_sel, GXIndTexMtxID matrix_sel, GXIndTexWrap wrap_s, GXIndTexWrap wrap_t, + GXBool add_prev, GXBool ind_lod, GXIndTexAlphaSel alpha_sel); +void GXSetTevIndWarp(GXTevStageID tev_stage, GXIndTexStageID ind_stage, GXBool signed_offsets, GXBool replace_mode, + GXIndTexMtxID matrix_sel); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gx/GXCommandList.h b/aurora-main/include/dolphin/gx/GXCommandList.h new file mode 100644 index 0000000..069a086 --- /dev/null +++ b/aurora-main/include/dolphin/gx/GXCommandList.h @@ -0,0 +1,41 @@ +#ifndef DOLPHIN_GXCOMMANDLIST_H +#define DOLPHIN_GXCOMMANDLIST_H + +#ifdef __cplusplus +extern "C" { +#endif + +#define GX_NOP 0x00 +#define GX_DRAW_QUADS 0x80 +#define GX_DRAW_TRIANGLES 0x90 +#define GX_DRAW_TRIANGLE_STRIP 0x98 +#define GX_DRAW_TRIANGLE_FAN 0xA0 +#define GX_DRAW_LINES 0xA8 +#define GX_DRAW_LINE_STRIP 0xB0 +#define GX_DRAW_POINTS 0xB8 + +#define GX_LOAD_BP_REG 0x61 +#define GX_LOAD_CP_REG 0x08 +#define GX_LOAD_XF_REG 0x10 +#define GX_LOAD_INDX_A 0x20 +#define GX_LOAD_INDX_B 0x28 +#define GX_LOAD_INDX_C 0x30 +#define GX_LOAD_INDX_D 0x38 + +#define GX_CMD_CALL_DL 0x40 +#define GX_CMD_INVL_VC 0x48 + +/** + * Custom Aurora commands that are followed by another 2-byte identifier. + * See GXAurora.h for further documentation on these. + */ +#define GX_LOAD_AURORA 0x50 + +#define GX_OPCODE_MASK 0xF8 +#define GX_VAT_MASK 0x07 + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gx/GXCpu2Efb.h b/aurora-main/include/dolphin/gx/GXCpu2Efb.h new file mode 100644 index 0000000..eefbcd8 --- /dev/null +++ b/aurora-main/include/dolphin/gx/GXCpu2Efb.h @@ -0,0 +1,22 @@ +#ifndef _DOLPHIN_GX_GXCPU2EFB_H_ +#define _DOLPHIN_GX_GXCPU2EFB_H_ + +#include "GXEnum.h" + +void GXPokeAlphaMode(GXCompare func, u8 threshold); +void GXPokeAlphaRead(GXAlphaReadMode mode); +void GXPokeAlphaUpdate(GXBool update_enable); +void GXPokeBlendMode(GXBlendMode type, GXBlendFactor src_factor, GXBlendFactor dst_factor, GXLogicOp op); +void GXPokeColorUpdate(GXBool update_enable); +void GXPokeDstAlpha(GXBool enable, u8 alpha); +void GXPokeDither(GXBool dither); +void GXPokeZMode(GXBool compare_enable, GXCompare func, GXBool update_enable); +void GXPeekARGB(u16 x, u16 y, u32* color); +void GXPokeARGB(u16 x, u16 y, u32 color); +void GXPeekZ(u16 x, u16 y, u32* z); +void GXPokeZ(u16 x, u16 y, u32 z); +u32 GXCompressZ16(u32 z24, GXZFmt16 zfmt); +u32 GXDecompressZ16(u32 z16, GXZFmt16 zfmt); + + +#endif // _DOLPHIN_GX_GXCPU2EFB_H_ diff --git a/aurora-main/include/dolphin/gx/GXCull.h b/aurora-main/include/dolphin/gx/GXCull.h new file mode 100644 index 0000000..bf2d922 --- /dev/null +++ b/aurora-main/include/dolphin/gx/GXCull.h @@ -0,0 +1,18 @@ +#ifndef DOLPHIN_GXCULL_H +#define DOLPHIN_GXCULL_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void GXSetScissor(u32 left, u32 top, u32 wd, u32 ht); +void GXSetCullMode(GXCullMode mode); +void GXSetCoPlanar(GXBool enable); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gx/GXDispList.h b/aurora-main/include/dolphin/gx/GXDispList.h new file mode 100644 index 0000000..41597c1 --- /dev/null +++ b/aurora-main/include/dolphin/gx/GXDispList.h @@ -0,0 +1,25 @@ +#ifndef DOLPHIN_GXDISPLIST_H +#define DOLPHIN_GXDISPLIST_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef AURORA +#define GXCallDisplayListNative GXCallDisplayListLE +#else +#define GXCallDisplayListNative GXCallDisplayList +#endif + +void GXBeginDisplayList(void* list, u32 size); +u32 GXEndDisplayList(void); +void GXCallDisplayListLE(const void* list, u32 nbytes); +void GXCallDisplayList(const void* list, u32 nbytes); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gx/GXDraw.h b/aurora-main/include/dolphin/gx/GXDraw.h new file mode 100644 index 0000000..3c02a08 --- /dev/null +++ b/aurora-main/include/dolphin/gx/GXDraw.h @@ -0,0 +1,24 @@ +#ifndef DOLPHIN_GXDRAW_H +#define DOLPHIN_GXDRAW_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void GXDrawCylinder(u8 numEdges); +void GXDrawTorus(f32 rc, u8 numc, u8 numt); +void GXDrawSphere(u8 numMajor, u8 numMinor); +void GXDrawCube(void); +void GXDrawDodeca(void); +void GXDrawOctahedron(void); +void GXDrawIcosahedron(void); +void GXDrawSphere1(u8 depth); +u32 GXGenNormalTable(u8 depth, f32* table); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gx/GXEnum.h b/aurora-main/include/dolphin/gx/GXEnum.h new file mode 100644 index 0000000..d081f45 --- /dev/null +++ b/aurora-main/include/dolphin/gx/GXEnum.h @@ -0,0 +1,915 @@ +#ifndef DOLPHIN_GXENUM_H +#define DOLPHIN_GXENUM_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef TARGET_PC +#include + +typedef bool GXBool; +#else +typedef u8 GXBool; +#endif + +#define GX_FALSE ((GXBool)0) +#define GX_TRUE ((GXBool)1) + +#define GX_ENABLE ((GXBool)1) +#define GX_DISABLE ((GXBool)0) + +typedef enum { + GX_PERSPECTIVE, + GX_ORTHOGRAPHIC, +} GXProjectionType; + +typedef enum { + GX_NEVER, + GX_LESS, + GX_EQUAL, + GX_LEQUAL, + GX_GREATER, + GX_NEQUAL, + GX_GEQUAL, + GX_ALWAYS, +} GXCompare; + +typedef enum { + GX_AOP_AND, + GX_AOP_OR, + GX_AOP_XOR, + GX_AOP_XNOR, + GX_MAX_ALPHAOP, +} GXAlphaOp; + +typedef enum { + GX_ZC_LINEAR, + GX_ZC_NEAR, + GX_ZC_MID, + GX_ZC_FAR, +} GXZFmt16; + +typedef enum { + GX_GM_1_0, + GX_GM_1_7, + GX_GM_2_2, +} GXGamma; + +typedef enum { + GX_PF_RGB8_Z24, + GX_PF_RGBA6_Z24, + GX_PF_RGB565_Z16, + GX_PF_Z24, + GX_PF_Y8, + GX_PF_U8, + GX_PF_V8, + GX_PF_YUV420, +} GXPixelFmt; + +typedef enum { + GX_QUADS = 0x80, + GX_TRIANGLES = 0x90, + GX_TRIANGLESTRIP = 0x98, + GX_TRIANGLEFAN = 0xA0, + GX_LINES = 0xA8, + GX_LINESTRIP = 0xB0, + GX_POINTS = 0xB8, +} GXPrimitive; + +typedef enum { + GX_VTXFMT0, + GX_VTXFMT1, + GX_VTXFMT2, + GX_VTXFMT3, + GX_VTXFMT4, + GX_VTXFMT5, + GX_VTXFMT6, + GX_VTXFMT7, + GX_MAX_VTXFMT, +} GXVtxFmt; + +typedef enum { + GX_VA_PNMTXIDX, + GX_VA_TEX0MTXIDX, + GX_VA_TEX1MTXIDX, + GX_VA_TEX2MTXIDX, + GX_VA_TEX3MTXIDX, + GX_VA_TEX4MTXIDX, + GX_VA_TEX5MTXIDX, + GX_VA_TEX6MTXIDX, + GX_VA_TEX7MTXIDX, + GX_VA_POS, + GX_VA_NRM, + GX_VA_CLR0, + GX_VA_CLR1, + GX_VA_TEX0, + GX_VA_TEX1, + GX_VA_TEX2, + GX_VA_TEX3, + GX_VA_TEX4, + GX_VA_TEX5, + GX_VA_TEX6, + GX_VA_TEX7, + GX_POS_MTX_ARRAY, + GX_NRM_MTX_ARRAY, + GX_TEX_MTX_ARRAY, + GX_LIGHT_ARRAY, + GX_VA_NBT, + GX_VA_MAX_ATTR, + GX_VA_NULL = 0xFF, +} GXAttr; + +typedef enum { + GX_NONE, + GX_DIRECT, + GX_INDEX8, + GX_INDEX16, +} GXAttrType; + +#define _GX_TF_CTF 0x20 +#define _GX_TF_ZTF 0x10 + +// Aurora-specific linear texture types +#define _GX_TF_PC 0x40 + +typedef enum { + GX_TF_I4 = 0x0, + GX_TF_I8 = 0x1, + GX_TF_IA4 = 0x2, + GX_TF_IA8 = 0x3, + GX_TF_RGB565 = 0x4, + GX_TF_RGB5A3 = 0x5, + GX_TF_RGBA8 = 0x6, + GX_TF_CMPR = 0xE, + + GX_CTF_R4 = 0x0 | _GX_TF_CTF, + GX_CTF_RA4 = 0x2 | _GX_TF_CTF, + GX_CTF_RA8 = 0x3 | _GX_TF_CTF, + GX_CTF_YUVA8 = 0x6 | _GX_TF_CTF, + GX_CTF_A8 = 0x7 | _GX_TF_CTF, + GX_CTF_R8 = 0x8 | _GX_TF_CTF, + GX_CTF_G8 = 0x9 | _GX_TF_CTF, + GX_CTF_B8 = 0xA | _GX_TF_CTF, + GX_CTF_RG8 = 0xB | _GX_TF_CTF, + GX_CTF_GB8 = 0xC | _GX_TF_CTF, + + GX_TF_Z8 = 0x1 | _GX_TF_ZTF, + GX_TF_Z16 = 0x3 | _GX_TF_ZTF, + GX_TF_Z24X8 = 0x6 | _GX_TF_ZTF, + + GX_CTF_Z4 = 0x0 | _GX_TF_ZTF | _GX_TF_CTF, + GX_CTF_Z8M = 0x9 | _GX_TF_ZTF | _GX_TF_CTF, + GX_CTF_Z8L = 0xA | _GX_TF_ZTF | _GX_TF_CTF, + GX_CTF_Z16L = 0xC | _GX_TF_ZTF | _GX_TF_CTF, + + GX_TF_A8 = GX_CTF_A8, + +#ifdef TARGET_PC + GX_TF_R8_PC = 0x1 | _GX_TF_PC, + GX_TF_RGBA8_PC = 0x6 | _GX_TF_PC, +#endif +} GXTexFmt; + +typedef enum { + GX_TF_C4 = 0x8, + GX_TF_C8 = 0x9, + GX_TF_C14X2 = 0xa, +} GXCITexFmt; + +typedef enum { + GX_CLAMP, + GX_REPEAT, + GX_MIRROR, + GX_MAX_TEXWRAPMODE, +} GXTexWrapMode; + +typedef enum { + GX_NEAR, + GX_LINEAR, + GX_NEAR_MIP_NEAR, + GX_LIN_MIP_NEAR, + GX_NEAR_MIP_LIN, + GX_LIN_MIP_LIN, +} GXTexFilter; + +typedef enum { + GX_ANISO_1, + GX_ANISO_2, + GX_ANISO_4, + GX_MAX_ANISOTROPY, +} GXAnisotropy; + +typedef enum { + GX_TEXMAP0, + GX_TEXMAP1, + GX_TEXMAP2, + GX_TEXMAP3, + GX_TEXMAP4, + GX_TEXMAP5, + GX_TEXMAP6, + GX_TEXMAP7, + GX_MAX_TEXMAP, + GX_TEXMAP_NULL = 0xFF, + GX_TEX_DISABLE = 0x100, +} GXTexMapID; + +typedef enum { + GX_TEXCOORD0, + GX_TEXCOORD1, + GX_TEXCOORD2, + GX_TEXCOORD3, + GX_TEXCOORD4, + GX_TEXCOORD5, + GX_TEXCOORD6, + GX_TEXCOORD7, + GX_MAX_TEXCOORD, + GX_TEXCOORD_NULL = 0xFF, +} GXTexCoordID; + +typedef enum { + GX_TEVSTAGE0, + GX_TEVSTAGE1, + GX_TEVSTAGE2, + GX_TEVSTAGE3, + GX_TEVSTAGE4, + GX_TEVSTAGE5, + GX_TEVSTAGE6, + GX_TEVSTAGE7, + GX_TEVSTAGE8, + GX_TEVSTAGE9, + GX_TEVSTAGE10, + GX_TEVSTAGE11, + GX_TEVSTAGE12, + GX_TEVSTAGE13, + GX_TEVSTAGE14, + GX_TEVSTAGE15, + GX_MAX_TEVSTAGE, +} GXTevStageID; + +typedef enum { + GX_MODULATE, + GX_DECAL, + GX_BLEND, + GX_REPLACE, + GX_PASSCLR, +} GXTevMode; + +typedef enum { + GX_MTX3x4, + GX_MTX2x4, +} GXTexMtxType; + +typedef enum { + GX_TG_MTX3x4, + GX_TG_MTX2x4, + GX_TG_BUMP0, + GX_TG_BUMP1, + GX_TG_BUMP2, + GX_TG_BUMP3, + GX_TG_BUMP4, + GX_TG_BUMP5, + GX_TG_BUMP6, + GX_TG_BUMP7, + GX_TG_SRTG, +} GXTexGenType; + +typedef enum { + GX_PNMTX0 = 0, + GX_PNMTX1 = 3, + GX_PNMTX2 = 6, + GX_PNMTX3 = 9, + GX_PNMTX4 = 12, + GX_PNMTX5 = 15, + GX_PNMTX6 = 18, + GX_PNMTX7 = 21, + GX_PNMTX8 = 24, + GX_PNMTX9 = 27, +} GXPosNrmMtx; + +typedef enum { + GX_TEXMTX0 = 30, + GX_TEXMTX1 = 33, + GX_TEXMTX2 = 36, + GX_TEXMTX3 = 39, + GX_TEXMTX4 = 42, + GX_TEXMTX5 = 45, + GX_TEXMTX6 = 48, + GX_TEXMTX7 = 51, + GX_TEXMTX8 = 54, + GX_TEXMTX9 = 57, + GX_IDENTITY = 60, +} GXTexMtx; + +typedef enum { + GX_COLOR0, + GX_COLOR1, + GX_ALPHA0, + GX_ALPHA1, + GX_COLOR0A0, + GX_COLOR1A1, + GX_COLOR_ZERO, + GX_ALPHA_BUMP, + GX_ALPHA_BUMPN, + GX_COLOR_NULL = 0xFF, +} GXChannelID; + +typedef enum { + GX_TG_POS, + GX_TG_NRM, + GX_TG_BINRM, + GX_TG_TANGENT, + GX_TG_TEX0, + GX_TG_TEX1, + GX_TG_TEX2, + GX_TG_TEX3, + GX_TG_TEX4, + GX_TG_TEX5, + GX_TG_TEX6, + GX_TG_TEX7, + GX_TG_TEXCOORD0, + GX_TG_TEXCOORD1, + GX_TG_TEXCOORD2, + GX_TG_TEXCOORD3, + GX_TG_TEXCOORD4, + GX_TG_TEXCOORD5, + GX_TG_TEXCOORD6, + GX_TG_COLOR0, + GX_TG_COLOR1, + GX_MAX_TEXGENSRC, +} GXTexGenSrc; + +typedef enum { + GX_BM_NONE, + GX_BM_BLEND, + GX_BM_LOGIC, + GX_BM_SUBTRACT, + GX_MAX_BLENDMODE, +} GXBlendMode; + +typedef enum { + GX_BL_ZERO, + GX_BL_ONE, + GX_BL_SRCCLR, + GX_BL_INVSRCCLR, + GX_BL_SRCALPHA, + GX_BL_INVSRCALPHA, + GX_BL_DSTALPHA, + GX_BL_INVDSTALPHA, + GX_BL_DSTCLR = GX_BL_SRCCLR, + GX_BL_INVDSTCLR = GX_BL_INVSRCCLR, +} GXBlendFactor; + +typedef enum { + GX_LO_CLEAR, + GX_LO_AND, + GX_LO_REVAND, + GX_LO_COPY, + GX_LO_INVAND, + GX_LO_NOOP, + GX_LO_XOR, + GX_LO_OR, + GX_LO_NOR, + GX_LO_EQUIV, + GX_LO_INV, + GX_LO_REVOR, + GX_LO_INVCOPY, + GX_LO_INVOR, + GX_LO_NAND, + GX_LO_SET, +} GXLogicOp; + +typedef enum { + GX_POS_XY = 0, + GX_POS_XYZ = 1, + GX_NRM_XYZ = 0, + GX_NRM_NBT = 1, + GX_NRM_NBT3 = 2, + GX_CLR_RGB = 0, + GX_CLR_RGBA = 1, + GX_TEX_S = 0, + GX_TEX_ST = 1, +} GXCompCnt; + +typedef enum { + GX_U8 = 0, + GX_S8 = 1, + GX_U16 = 2, + GX_S16 = 3, + GX_F32 = 4, + GX_RGB565 = 0, + GX_RGB8 = 1, + GX_RGBX8 = 2, + GX_RGBA4 = 3, + GX_RGBA6 = 4, + GX_RGBA8 = 5, +} GXCompType; + +typedef enum { + GX_PTTEXMTX0 = 64, + GX_PTTEXMTX1 = 67, + GX_PTTEXMTX2 = 70, + GX_PTTEXMTX3 = 73, + GX_PTTEXMTX4 = 76, + GX_PTTEXMTX5 = 79, + GX_PTTEXMTX6 = 82, + GX_PTTEXMTX7 = 85, + GX_PTTEXMTX8 = 88, + GX_PTTEXMTX9 = 91, + GX_PTTEXMTX10 = 94, + GX_PTTEXMTX11 = 97, + GX_PTTEXMTX12 = 100, + GX_PTTEXMTX13 = 103, + GX_PTTEXMTX14 = 106, + GX_PTTEXMTX15 = 109, + GX_PTTEXMTX16 = 112, + GX_PTTEXMTX17 = 115, + GX_PTTEXMTX18 = 118, + GX_PTTEXMTX19 = 121, + GX_PTIDENTITY = 125, +} GXPTTexMtx; + +typedef enum { + GX_TEVPREV, + GX_TEVREG0, + GX_TEVREG1, + GX_TEVREG2, + GX_MAX_TEVREG, +} GXTevRegID; + +typedef enum { + GX_DF_NONE, + GX_DF_SIGN, + GX_DF_CLAMP, +} GXDiffuseFn; + +typedef enum { + GX_SRC_REG, + GX_SRC_VTX, +} GXColorSrc; + +typedef enum { + GX_AF_SPEC, + GX_AF_SPOT, + GX_AF_NONE, +} GXAttnFn; + +typedef enum { + GX_LIGHT0 = 0x001, + GX_LIGHT1 = 0x002, + GX_LIGHT2 = 0x004, + GX_LIGHT3 = 0x008, + GX_LIGHT4 = 0x010, + GX_LIGHT5 = 0x020, + GX_LIGHT6 = 0x040, + GX_LIGHT7 = 0x080, + GX_MAX_LIGHT = 0x100, + GX_LIGHT_NULL = 0, +} GXLightID; + +typedef enum { + GX_TO_ZERO, + GX_TO_SIXTEENTH, + GX_TO_EIGHTH, + GX_TO_FOURTH, + GX_TO_HALF, + GX_TO_ONE, + GX_MAX_TEXOFFSET, +} GXTexOffset; + +typedef enum { + GX_SP_OFF, + GX_SP_FLAT, + GX_SP_COS, + GX_SP_COS2, + GX_SP_SHARP, + GX_SP_RING1, + GX_SP_RING2, +} GXSpotFn; + +typedef enum { + GX_DA_OFF, + GX_DA_GENTLE, + GX_DA_MEDIUM, + GX_DA_STEEP, +} GXDistAttnFn; + +typedef enum { + GX_CULL_NONE, + GX_CULL_FRONT, + GX_CULL_BACK, + GX_CULL_ALL + +} GXCullMode; + +typedef enum { GX_TEV_SWAP0 = 0, GX_TEV_SWAP1, GX_TEV_SWAP2, GX_TEV_SWAP3, GX_MAX_TEVSWAP } GXTevSwapSel; + +typedef enum { GX_CH_RED = 0, GX_CH_GREEN, GX_CH_BLUE, GX_CH_ALPHA } GXTevColorChan; + +typedef enum _GXFogType { + GX_FOG_NONE = 0, + GX_FOG_PERSP_LIN = 2, + GX_FOG_PERSP_EXP = 4, + GX_FOG_PERSP_EXP2 = 5, + GX_FOG_PERSP_REVEXP = 6, + GX_FOG_PERSP_REVEXP2 = 7, + GX_FOG_ORTHO_LIN = 10, + GX_FOG_ORTHO_EXP = 12, + GX_FOG_ORTHO_EXP2 = 13, + GX_FOG_ORTHO_REVEXP = 14, + GX_FOG_ORTHO_REVEXP2 = 15, + GX_FOG_LIN = GX_FOG_PERSP_LIN, + GX_FOG_EXP = GX_FOG_PERSP_EXP, + GX_FOG_EXP2 = GX_FOG_PERSP_EXP2, + GX_FOG_REVEXP = GX_FOG_PERSP_REVEXP, + GX_FOG_REVEXP2 = GX_FOG_PERSP_REVEXP2, +} GXFogType; + +typedef enum { + GX_CC_CPREV, + GX_CC_APREV, + GX_CC_C0, + GX_CC_A0, + GX_CC_C1, + GX_CC_A1, + GX_CC_C2, + GX_CC_A2, + GX_CC_TEXC, + GX_CC_TEXA, + GX_CC_RASC, + GX_CC_RASA, + GX_CC_ONE, + GX_CC_HALF, + GX_CC_KONST, + GX_CC_ZERO +} GXTevColorArg; + +typedef enum { + GX_CA_APREV, + GX_CA_A0, + GX_CA_A1, + GX_CA_A2, + GX_CA_TEXA, + GX_CA_RASA, + GX_CA_KONST, + GX_CA_ZERO +} GXTevAlphaArg; + +typedef enum { + GX_TEV_ADD = 0, + GX_TEV_SUB = 1, + GX_TEV_COMP_R8_GT = 8, + GX_TEV_COMP_R8_EQ = 9, + GX_TEV_COMP_GR16_GT = 10, + GX_TEV_COMP_GR16_EQ = 11, + GX_TEV_COMP_BGR24_GT = 12, + GX_TEV_COMP_BGR24_EQ = 13, + GX_TEV_COMP_RGB8_GT = 14, + GX_TEV_COMP_RGB8_EQ = 15, + GX_TEV_COMP_A8_GT = GX_TEV_COMP_RGB8_GT, + GX_TEV_COMP_A8_EQ = GX_TEV_COMP_RGB8_EQ +} GXTevOp; + +typedef enum { GX_TB_ZERO, GX_TB_ADDHALF, GX_TB_SUBHALF, GX_MAX_TEVBIAS } GXTevBias; + +typedef enum { GX_CS_SCALE_1, GX_CS_SCALE_2, GX_CS_SCALE_4, GX_CS_DIVIDE_2, GX_MAX_TEVSCALE } GXTevScale; + +typedef enum { + GX_TEV_KCSEL_8_8 = 0x00, + GX_TEV_KCSEL_7_8 = 0x01, + GX_TEV_KCSEL_6_8 = 0x02, + GX_TEV_KCSEL_5_8 = 0x03, + GX_TEV_KCSEL_4_8 = 0x04, + GX_TEV_KCSEL_3_8 = 0x05, + GX_TEV_KCSEL_2_8 = 0x06, + GX_TEV_KCSEL_1_8 = 0x07, + GX_TEV_KCSEL_1 = GX_TEV_KCSEL_8_8, + GX_TEV_KCSEL_3_4 = GX_TEV_KCSEL_6_8, + GX_TEV_KCSEL_1_2 = GX_TEV_KCSEL_4_8, + GX_TEV_KCSEL_1_4 = GX_TEV_KCSEL_2_8, + GX_TEV_KCSEL_K0 = 0x0C, + GX_TEV_KCSEL_K1 = 0x0D, + GX_TEV_KCSEL_K2 = 0x0E, + GX_TEV_KCSEL_K3 = 0x0F, + GX_TEV_KCSEL_K0_R = 0x10, + GX_TEV_KCSEL_K1_R = 0x11, + GX_TEV_KCSEL_K2_R = 0x12, + GX_TEV_KCSEL_K3_R = 0x13, + GX_TEV_KCSEL_K0_G = 0x14, + GX_TEV_KCSEL_K1_G = 0x15, + GX_TEV_KCSEL_K2_G = 0x16, + GX_TEV_KCSEL_K3_G = 0x17, + GX_TEV_KCSEL_K0_B = 0x18, + GX_TEV_KCSEL_K1_B = 0x19, + GX_TEV_KCSEL_K2_B = 0x1A, + GX_TEV_KCSEL_K3_B = 0x1B, + GX_TEV_KCSEL_K0_A = 0x1C, + GX_TEV_KCSEL_K1_A = 0x1D, + GX_TEV_KCSEL_K2_A = 0x1E, + GX_TEV_KCSEL_K3_A = 0x1F +} GXTevKColorSel; + +typedef enum { + GX_TEV_KASEL_8_8 = 0x00, + GX_TEV_KASEL_7_8 = 0x01, + GX_TEV_KASEL_6_8 = 0x02, + GX_TEV_KASEL_5_8 = 0x03, + GX_TEV_KASEL_4_8 = 0x04, + GX_TEV_KASEL_3_8 = 0x05, + GX_TEV_KASEL_2_8 = 0x06, + GX_TEV_KASEL_1_8 = 0x07, + GX_TEV_KASEL_1 = GX_TEV_KASEL_8_8, + GX_TEV_KASEL_3_4 = GX_TEV_KASEL_6_8, + GX_TEV_KASEL_1_2 = GX_TEV_KASEL_4_8, + GX_TEV_KASEL_1_4 = GX_TEV_KASEL_2_8, + GX_TEV_KASEL_K0_R = 0x10, + GX_TEV_KASEL_K1_R = 0x11, + GX_TEV_KASEL_K2_R = 0x12, + GX_TEV_KASEL_K3_R = 0x13, + GX_TEV_KASEL_K0_G = 0x14, + GX_TEV_KASEL_K1_G = 0x15, + GX_TEV_KASEL_K2_G = 0x16, + GX_TEV_KASEL_K3_G = 0x17, + GX_TEV_KASEL_K0_B = 0x18, + GX_TEV_KASEL_K1_B = 0x19, + GX_TEV_KASEL_K2_B = 0x1A, + GX_TEV_KASEL_K3_B = 0x1B, + GX_TEV_KASEL_K0_A = 0x1C, + GX_TEV_KASEL_K1_A = 0x1D, + GX_TEV_KASEL_K2_A = 0x1E, + GX_TEV_KASEL_K3_A = 0x1F +} GXTevKAlphaSel; + +typedef enum { GX_KCOLOR0 = 0, GX_KCOLOR1, GX_KCOLOR2, GX_KCOLOR3, GX_MAX_KCOLOR } GXTevKColorID; + +typedef enum { + GX_ZT_DISABLE, + GX_ZT_ADD, + GX_ZT_REPLACE, + GX_MAX_ZTEXOP, +} GXZTexOp; + +typedef enum { + GX_ITF_8, + GX_ITF_5, + GX_ITF_4, + GX_ITF_3, + GX_MAX_ITFORMAT, +} GXIndTexFormat; + +typedef enum { + GX_ITB_NONE, + GX_ITB_S, + GX_ITB_T, + GX_ITB_ST, + GX_ITB_U, + GX_ITB_SU, + GX_ITB_TU, + GX_ITB_STU, + GX_MAX_ITBIAS, +} GXIndTexBiasSel; + +typedef enum { + GX_ITBA_OFF, + GX_ITBA_S, + GX_ITBA_T, + GX_ITBA_U, + GX_MAX_ITBALPHA, +} GXIndTexAlphaSel; + +typedef enum { + GX_ITM_OFF, + GX_ITM_0, + GX_ITM_1, + GX_ITM_2, + GX_ITM_S0 = 5, + GX_ITM_S1, + GX_ITM_S2, + GX_ITM_T0 = 9, + GX_ITM_T1, + GX_ITM_T2, +} GXIndTexMtxID; + +typedef enum { + GX_ITW_OFF, + GX_ITW_256, + GX_ITW_128, + GX_ITW_64, + GX_ITW_32, + GX_ITW_16, + GX_ITW_0, + GX_MAX_ITWRAP, +} GXIndTexWrap; + +typedef enum { + GX_INDTEXSTAGE0, + GX_INDTEXSTAGE1, + GX_INDTEXSTAGE2, + GX_INDTEXSTAGE3, + GX_MAX_INDTEXSTAGE, +} GXIndTexStageID; + +typedef enum { + GX_ITS_1, + GX_ITS_2, + GX_ITS_4, + GX_ITS_8, + GX_ITS_16, + GX_ITS_32, + GX_ITS_64, + GX_ITS_128, + GX_ITS_256, + GX_MAX_ITSCALE, +} GXIndTexScale; + +typedef enum { + GX_PERF0_VERTICES, + GX_PERF0_CLIP_VTX, + GX_PERF0_CLIP_CLKS, + GX_PERF0_XF_WAIT_IN, + GX_PERF0_XF_WAIT_OUT, + GX_PERF0_XF_XFRM_CLKS, + GX_PERF0_XF_LIT_CLKS, + GX_PERF0_XF_BOT_CLKS, + GX_PERF0_XF_REGLD_CLKS, + GX_PERF0_XF_REGRD_CLKS, + GX_PERF0_CLIP_RATIO, + + GX_PERF0_TRIANGLES, + GX_PERF0_TRIANGLES_CULLED, + GX_PERF0_TRIANGLES_PASSED, + GX_PERF0_TRIANGLES_SCISSORED, + GX_PERF0_TRIANGLES_0TEX, + GX_PERF0_TRIANGLES_1TEX, + GX_PERF0_TRIANGLES_2TEX, + GX_PERF0_TRIANGLES_3TEX, + GX_PERF0_TRIANGLES_4TEX, + GX_PERF0_TRIANGLES_5TEX, + GX_PERF0_TRIANGLES_6TEX, + GX_PERF0_TRIANGLES_7TEX, + GX_PERF0_TRIANGLES_8TEX, + GX_PERF0_TRIANGLES_0CLR, + GX_PERF0_TRIANGLES_1CLR, + GX_PERF0_TRIANGLES_2CLR, + + GX_PERF0_QUAD_0CVG, + GX_PERF0_QUAD_NON0CVG, + GX_PERF0_QUAD_1CVG, + GX_PERF0_QUAD_2CVG, + GX_PERF0_QUAD_3CVG, + GX_PERF0_QUAD_4CVG, + GX_PERF0_AVG_QUAD_CNT, + + GX_PERF0_CLOCKS, + GX_PERF0_NONE, +} GXPerf0; + +typedef enum { + GX_PERF1_TEXELS, + GX_PERF1_TX_IDLE, + GX_PERF1_TX_REGS, + GX_PERF1_TX_MEMSTALL, + GX_PERF1_TC_CHECK1_2, + GX_PERF1_TC_CHECK3_4, + GX_PERF1_TC_CHECK5_6, + GX_PERF1_TC_CHECK7_8, + GX_PERF1_TC_MISS, + + GX_PERF1_VC_ELEMQ_FULL, + GX_PERF1_VC_MISSQ_FULL, + GX_PERF1_VC_MEMREQ_FULL, + GX_PERF1_VC_STATUS7, + GX_PERF1_VC_MISSREP_FULL, + GX_PERF1_VC_STREAMBUF_LOW, + GX_PERF1_VC_ALL_STALLS, + GX_PERF1_VERTICES, + + GX_PERF1_FIFO_REQ, + GX_PERF1_CALL_REQ, + GX_PERF1_VC_MISS_REQ, + GX_PERF1_CP_ALL_REQ, + + GX_PERF1_CLOCKS, + GX_PERF1_NONE, +} GXPerf1; + +typedef enum { + GX_VC_POS = 0, + GX_VC_NRM = 1, + GX_VC_CLR0 = 2, + GX_VC_CLR1 = 3, + GX_VC_TEX0 = 4, + GX_VC_TEX1 = 5, + GX_VC_TEX2 = 6, + GX_VC_TEX3 = 7, + GX_VC_TEX4 = 8, + GX_VC_TEX5 = 9, + GX_VC_TEX6 = 10, + GX_VC_TEX7 = 11, + GX_VC_ALL = 15, +} GXVCachePerf; + +typedef enum { + GX_CLIP_ENABLE = 0, + GX_CLIP_DISABLE = 1, +} GXClipMode; + +typedef enum { + GX_CLAMP_NONE = 0, + GX_CLAMP_TOP = 1, + GX_CLAMP_BOTTOM = 2, +} GXFBClamp; + +typedef enum { + GX_TLUT0 = 0, + GX_TLUT1 = 1, + GX_TLUT2 = 2, + GX_TLUT3 = 3, + GX_TLUT4 = 4, + GX_TLUT5 = 5, + GX_TLUT6 = 6, + GX_TLUT7 = 7, + GX_TLUT8 = 8, + GX_TLUT9 = 9, + GX_TLUT10 = 10, + GX_TLUT11 = 11, + GX_TLUT12 = 12, + GX_TLUT13 = 13, + GX_TLUT14 = 14, + GX_TLUT15 = 15, + GX_BIGTLUT0 = 16, + GX_BIGTLUT1 = 17, + GX_BIGTLUT2 = 18, + GX_BIGTLUT3 = 19, +} GXTlut; + +typedef enum { + GX_TL_IA8, + GX_TL_RGB565, + GX_TL_RGB5A3, + GX_MAX_TLUTFMT, +} GXTlutFmt; + +typedef enum { + GX_TLUT_16 = 1, + GX_TLUT_32 = 2, + GX_TLUT_64 = 4, + GX_TLUT_128 = 8, + GX_TLUT_256 = 16, + GX_TLUT_512 = 32, + GX_TLUT_1K = 64, + GX_TLUT_2K = 128, + GX_TLUT_4K = 256, + GX_TLUT_8K = 512, + GX_TLUT_16K = 1024, +} GXTlutSize; + +typedef enum { + GX_READ_00, + GX_READ_FF, + GX_READ_NONE, +} GXAlphaReadMode; + +typedef enum { + GX_TEXCACHE_32K, + GX_TEXCACHE_128K, + GX_TEXCACHE_512K, + GX_TEXCACHE_NONE, +} GXTexCacheSize; + +typedef enum { + GX_MT_XF_FLUSH = 1, + GX_MT_DL_SAVE_CONTEXT = 2, + GX_MT_NULL = 0, +} GXMiscToken; + +typedef enum { + GX_CMD_LOAD_INDX_A = 0x20, + GX_CMD_LOAD_INDX_B = 0x28, + GX_CMD_LOAD_INDX_C = 0x30, + GX_CMD_LOAD_INDX_D = 0x38, + + GX_CMD_LOAD_CP_REG = 0x08, + GX_CMD_LOAD_XF_REG = 0x10, + GX_CMD_LOAD_BP_REG = 0x61, +} GXCommand; + +typedef enum { + GX_CP_REG_MTXIDXA = 0x30, // Matrix index A + GX_CP_REG_MTXIDXB = 0x40, // Matrix index B + GX_CP_REG_VCD_LO = 0x50, // Vertex descriptor (lo) + GX_CP_REG_VCD_HI = 0x60, // Vertex descriptor (hi) + GX_CP_REG_VAT_GRP0 = 0x70, // Vertex attribute table (group 0) + GX_CP_REG_VAT_GRP1 = 0x80, // Vertex attribute table (group 1) + GX_CP_REG_VAT_GRP2 = 0x90, // Vertex attribute table (group 2) + GX_CP_REG_ARRAYBASE = 0xA0, // Vertex array start/base + GX_CP_REG_ARRAYSTRIDE = 0xB0, // Vertex array stride +} GXCPRegs; + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gx/GXExtra.h b/aurora-main/include/dolphin/gx/GXExtra.h new file mode 100644 index 0000000..27713aa --- /dev/null +++ b/aurora-main/include/dolphin/gx/GXExtra.h @@ -0,0 +1,30 @@ +#ifndef DOLPHIN_GXEXTRA_H +#define DOLPHIN_GXEXTRA_H +// Extra types for PC +#ifdef TARGET_PC +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + float r; + float g; + float b; + float a; +} GXColorF32; + +void GXDestroyTexObj(GXTexObj* obj); +void GXDestroyTlutObj(GXTlutObj* obj); +void GXDestroyCopyTex(void* dest); + +void GXColor4f32(float r, float g, float b, float a); + +#ifdef __cplusplus +} +#endif +#endif + +#endif diff --git a/aurora-main/include/dolphin/gx/GXFifo.h b/aurora-main/include/dolphin/gx/GXFifo.h new file mode 100644 index 0000000..8edbf04 --- /dev/null +++ b/aurora-main/include/dolphin/gx/GXFifo.h @@ -0,0 +1,37 @@ +#ifndef DOLPHIN_GXFIFO_H +#define DOLPHIN_GXFIFO_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + u8 pad[128]; +} GXFifoObj; + +typedef struct OSThread OSThread; + +void GXInitFifoBase(GXFifoObj* fifo, void* base, u32 size); +void GXInitFifoPtrs(GXFifoObj* fifo, void* readPtr, void* writePtr); +void GXGetFifoPtrs(GXFifoObj* fifo, void** readPtr, void** writePtr); +OSThread *GXSetCurrentGXThread(void); +OSThread *GXGetCurrentGXThread(void); +GXFifoObj* GXGetCPUFifo(void); +GXFifoObj* GXGetGPFifo(void); +void GXSetCPUFifo(GXFifoObj* fifo); +void GXSetGPFifo(GXFifoObj* fifo); +void GXSaveCPUFifo(GXFifoObj* fifo); +void GXGetFifoStatus(GXFifoObj* fifo, GXBool* overhi, GXBool* underlow, u32* fifoCount, GXBool* cpu_write, + GXBool* gp_read, GXBool* fifowrap); +void GXGetGPStatus(GXBool* overhi, GXBool* underlow, GXBool* readIdle, GXBool* cmdIdle, GXBool* brkpt); +void GXInitFifoLimits(GXFifoObj* fifo, u32 hiWaterMark, u32 loWaterMark); +void* GXGetFifoBase(const GXFifoObj* fifo); +u32 GXGetFifoSize(const GXFifoObj* fifo); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gx/GXFrameBuffer.h b/aurora-main/include/dolphin/gx/GXFrameBuffer.h new file mode 100644 index 0000000..c289cd3 --- /dev/null +++ b/aurora-main/include/dolphin/gx/GXFrameBuffer.h @@ -0,0 +1,60 @@ +#ifndef DOLPHIN_GXFRAMEBUFFER_H +#define DOLPHIN_GXFRAMEBUFFER_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define GX_MAX_Z24 0x00FFFFFF + +extern GXRenderModeObj GXNtsc240Ds; +extern GXRenderModeObj GXNtsc240DsAa; +extern GXRenderModeObj GXNtsc240Int; +extern GXRenderModeObj GXNtsc240IntAa; +extern GXRenderModeObj GXNtsc480IntDf; +extern GXRenderModeObj GXNtsc480Int; +extern GXRenderModeObj GXNtsc480IntAa; +extern GXRenderModeObj GXNtsc480Prog; +extern GXRenderModeObj GXNtsc480ProgAa; +extern GXRenderModeObj GXMpal240Ds; +extern GXRenderModeObj GXMpal240DsAa; +extern GXRenderModeObj GXMpal240Int; +extern GXRenderModeObj GXMpal240IntAa; +extern GXRenderModeObj GXMpal480IntDf; +extern GXRenderModeObj GXMpal480Int; +extern GXRenderModeObj GXMpal480IntAa; +extern GXRenderModeObj GXPal264Ds; +extern GXRenderModeObj GXPal264DsAa; +extern GXRenderModeObj GXPal264Int; +extern GXRenderModeObj GXPal264IntAa; +extern GXRenderModeObj GXPal528IntDf; +extern GXRenderModeObj GXPal528Int; +extern GXRenderModeObj GXPal528IntAa; + +void GXSetCopyClear(GXColor clear_clr, u32 clear_z); +void GXAdjustForOverscan(GXRenderModeObj* rmin, GXRenderModeObj* rmout, u16 hor, u16 ver); +void GXCopyDisp(void* dest, GXBool clear); +void GXSetDispCopyGamma(GXGamma gamma); +void GXSetDispCopySrc(u16 left, u16 top, u16 wd, u16 ht); +void GXSetDispCopyDst(u16 wd, u16 ht); +void GXSetCopyClamp(GXFBClamp clamp); +void GXSetDispCopyFrame2Field(u32 mode); +u32 GXSetDispCopyYScale(f32 vscale); +void GXSetCopyFilter(GXBool aa, u8 sample_pattern[12][2], GXBool vf, u8 vfilter[7]); +void GXSetPixelFmt(GXPixelFmt pix_fmt, GXZFmt16 z_fmt); +void GXSetTexCopySrc(u16 left, u16 top, u16 wd, u16 ht); +void GXSetTexCopyDst(u16 wd, u16 ht, GXTexFmt fmt, GXBool mipmap); +void GXCopyTex(void* dest, GXBool clear); +void GXClearBoundingBox(void); +void GXReadBoundingBox(u16* left, u16* right, u16* top, u16* bottom); +u16 GXGetNumXfbLines(u16 efbHeight, f32 yScale); +f32 GXGetYScaleFactor(u16 efbHeight, u16 xfbHeight); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gx/GXGeometry.h b/aurora-main/include/dolphin/gx/GXGeometry.h new file mode 100644 index 0000000..a71eb74 --- /dev/null +++ b/aurora-main/include/dolphin/gx/GXGeometry.h @@ -0,0 +1,43 @@ +#ifndef DOLPHIN_GXGEOMETRY_H +#define DOLPHIN_GXGEOMETRY_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void GXSetVtxDesc(GXAttr attr, GXAttrType type); +void GXSetSourceVtxDesc(GXAttr attr, GXAttrType type); +void GXSetVtxDescv(GXVtxDescList* list); +void GXClearVtxDesc(void); +void GXSetVtxAttrFmtv(GXVtxFmt vtxfmt, const GXVtxAttrFmtList* list); +void GXSetVtxAttrFmt(GXVtxFmt vtxfmt, GXAttr attr, GXCompCnt cnt, GXCompType type, u8 frac); +void GXSetNumTexGens(u8 nTexGens); +void GXBegin(GXPrimitive type, GXVtxFmt vtxfmt, u16 nverts); +void GXSetTexCoordGen2(GXTexCoordID dst_coord, GXTexGenType func, GXTexGenSrc src_param, u32 mtx, GXBool normalize, + u32 postmtx); +void GXSetLineWidth(u8 width, GXTexOffset texOffsets); +void GXSetPointSize(u8 pointSize, GXTexOffset texOffsets); +void GXEnableTexOffsets(GXTexCoordID coord, GXBool line_enable, GXBool point_enable); +#ifdef TARGET_PC +void GXSetArray(GXAttr attr, const void* data, u32 size, u8 stride, bool le); +static inline void GXSetArray(GXAttr attr, const void* data, u32 size, u8 stride) { + GXSetArray(attr, data, size, stride, false); +} +#define GXSETARRAY(attr, data, size, stride, le) GXSetArray((attr), (data), (size), (stride), (le)) +#else +void GXSetArray(GXAttr attr, const void* data, u8 stride); +#define GXSETARRAY(attr, data, size, stride, le) GXSetArray((attr), (data), (stride)) +#endif +void GXInvalidateVtxCache(void); + +static inline void GXSetTexCoordGen(GXTexCoordID dst_coord, GXTexGenType func, GXTexGenSrc src_param, u32 mtx) { + GXSetTexCoordGen2(dst_coord, func, src_param, mtx, GX_FALSE, GX_PTIDENTITY); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gx/GXGet.h b/aurora-main/include/dolphin/gx/GXGet.h new file mode 100644 index 0000000..a1b7e1c --- /dev/null +++ b/aurora-main/include/dolphin/gx/GXGet.h @@ -0,0 +1,40 @@ +#ifndef DOLPHIN_GXGET_H +#define DOLPHIN_GXGET_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void GXGetVtxDesc(GXAttr attr, GXAttrType* type); +void GXGetVtxDescv(GXVtxDescList* vcd); +void GXGetVtxAttrFmtv(GXVtxFmt fmt, GXVtxAttrFmtList* vat); +void GXGetLineWidth(u8* width, GXTexOffset* texOffsets); +void GXGetPointSize(u8* pointSize, GXTexOffset* texOffsets); +void GXGetCullMode(GXCullMode* mode); +GXBool GXGetTexObjMipMap(GXTexObj* tex_obj); +GXTexFmt GXGetTexObjFmt(GXTexObj* tex_obj); +u16 GXGetTexObjHeight(GXTexObj* tex_obj); +u16 GXGetTexObjWidth(GXTexObj* tex_obj); +GXTexWrapMode GXGetTexObjWrapS(GXTexObj* tex_obj); +GXTexWrapMode GXGetTexObjWrapT(GXTexObj* tex_obj); +void* GXGetTexObjData(GXTexObj* tex_obj); +void GXGetProjectionv(f32* p); +void GXGetLightAttnA(GXLightObj* lt_obj, f32* a0, f32* a1, f32* a2); +void GXGetLightAttnK(GXLightObj* lt_obj, f32* k0, f32* k1, f32* k2); +void GXGetLightPos(GXLightObj* lt_obj, f32* x, f32* y, f32* z); +void GXGetLightDir(GXLightObj* lt_obj, f32* nx, f32* ny, f32* nz); +void GXGetLightColor(GXLightObj* lt_obj, GXColor* color); +void GXGetVtxAttrFmt(GXVtxFmt idx, GXAttr attr, GXCompCnt* compCnt, GXCompType* compType, u8* shift); +u32 GXGetTexObjTlut(const GXTexObj* tex_obj); + +void GXGetViewportv(f32* vp); +void GXGetScissor(u32* left, u32* top, u32* wd, u32* ht); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gx/GXLighting.h b/aurora-main/include/dolphin/gx/GXLighting.h new file mode 100644 index 0000000..2e7ff19 --- /dev/null +++ b/aurora-main/include/dolphin/gx/GXLighting.h @@ -0,0 +1,32 @@ +#ifndef DOLPHIN_GXLIGHTING_H +#define DOLPHIN_GXLIGHTING_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void GXSetNumChans(u8 nChans); +void GXSetChanCtrl(GXChannelID chan, GXBool enable, GXColorSrc amb_src, GXColorSrc mat_src, u32 light_mask, + GXDiffuseFn diff_fn, GXAttnFn attn_fn); +void GXSetChanAmbColor(GXChannelID chan, GXColor amb_color); +void GXSetChanMatColor(GXChannelID chan, GXColor mat_color); + +void GXInitLightSpot(GXLightObj* lt_obj, f32 cutoff, GXSpotFn spot_func); +void GXInitLightDistAttn(GXLightObj* lt_obj, f32 ref_distance, f32 ref_brightness, GXDistAttnFn dist_func); +void GXInitLightPos(GXLightObj* lt_obj, f32 x, f32 y, f32 z); +void GXInitLightDir(GXLightObj* lt_obj, f32 nx, f32 ny, f32 nz); +void GXInitSpecularDir(GXLightObj* lt_obj, f32 nx, f32 ny, f32 nz); +void GXInitLightColor(GXLightObj* lt_obj, GXColor color); +void GXInitLightAttn(GXLightObj* lt_obj, f32 a0, f32 a1, f32 a2, f32 k0, f32 k1, f32 k2); +void GXInitLightAttnA(GXLightObj* lt_obj, f32 a0, f32 a1, f32 a2); +void GXInitLightAttnK(GXLightObj* lt_obj, f32 k0, f32 k1, f32 k2); +void GXLoadLightObjImm(GXLightObj* lt_obj, GXLightID light); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gx/GXManage.h b/aurora-main/include/dolphin/gx/GXManage.h new file mode 100644 index 0000000..a4d7d72 --- /dev/null +++ b/aurora-main/include/dolphin/gx/GXManage.h @@ -0,0 +1,28 @@ +#ifndef DOLPHIN_GXMANAGE_H +#define DOLPHIN_GXMANAGE_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*GXDrawSyncCallback)(u16 token); +typedef void (*GXDrawDoneCallback)(void); + +GXFifoObj* GXInit(void* base, u32 size); +GXDrawSyncCallback GXSetDrawSyncCallback(GXDrawSyncCallback cb); +GXDrawDoneCallback GXSetDrawDoneCallback(GXDrawDoneCallback cb); +void GXDrawDone(void); +void GXSetDrawDone(void); +void GXFlush(void); +void GXPixModeSync(void); +void GXTexModeSync(void); +void GXSetMisc(GXMiscToken token, u32 val); +void GXAbortFrame(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gx/GXPerf.h b/aurora-main/include/dolphin/gx/GXPerf.h new file mode 100644 index 0000000..7859e18 --- /dev/null +++ b/aurora-main/include/dolphin/gx/GXPerf.h @@ -0,0 +1,16 @@ +#ifndef DOLPHIN_GXPERF_H +#define DOLPHIN_GXPERF_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void GXReadXfRasMetric(u32* xf_wait_in, u32* xf_wait_out, u32* ras_busy, u32* clocks); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gx/GXPixel.h b/aurora-main/include/dolphin/gx/GXPixel.h new file mode 100644 index 0000000..db6d9d0 --- /dev/null +++ b/aurora-main/include/dolphin/gx/GXPixel.h @@ -0,0 +1,28 @@ +#ifndef DOLPHIN_GXPIXEL_H +#define DOLPHIN_GXPIXEL_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void GXSetFog(GXFogType type, f32 startz, f32 endz, f32 nearz, f32 farz, GXColor color); +void GXSetFogColor(GXColor color); +void GXSetFogRangeAdj(GXBool enable, u16 center, GXFogAdjTable *table); +void GXSetBlendMode(GXBlendMode type, GXBlendFactor src_factor, GXBlendFactor dst_factor, GXLogicOp op); +void GXSetColorUpdate(GXBool update_enable); +void GXSetAlphaUpdate(GXBool update_enable); +void GXSetZMode(GXBool compare_enable, GXCompare func, GXBool update_enable); +void GXSetZCompLoc(GXBool before_tex); +void GXSetPixelFmt(GXPixelFmt pix_fmt, GXZFmt16 z_fmt); +void GXSetDither(GXBool dither); +void GXSetDstAlpha(GXBool enable, u8 alpha); +void GXSetFieldMask(GXBool odd_mask, GXBool even_mask); +void GXSetFieldMode(GXBool field_mode, GXBool half_aspect_ratio); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gx/GXStruct.h b/aurora-main/include/dolphin/gx/GXStruct.h new file mode 100644 index 0000000..fc7b16a --- /dev/null +++ b/aurora-main/include/dolphin/gx/GXStruct.h @@ -0,0 +1,120 @@ +#ifndef DOLPHIN_GXSTRUCT_H +#define DOLPHIN_GXSTRUCT_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define VI_TVMODE(format, interlace) (((format) << 2) + (interlace)) + +#define VI_INTERLACE 0 +#define VI_NON_INTERLACE 1 +#define VI_PROGRESSIVE 2 + +#define VI_NTSC 0 +#define VI_PAL 1 +#define VI_MPAL 2 +#define VI_DEBUG 3 +#define VI_DEBUG_PAL 4 +#define VI_EURGB60 5 + +typedef enum { + VI_TVMODE_NTSC_INT = VI_TVMODE(VI_NTSC, VI_INTERLACE), + VI_TVMODE_NTSC_DS = VI_TVMODE(VI_NTSC, VI_NON_INTERLACE), + VI_TVMODE_NTSC_PROG = VI_TVMODE(VI_NTSC, VI_PROGRESSIVE), + VI_TVMODE_PAL_INT = VI_TVMODE(VI_PAL, VI_INTERLACE), + VI_TVMODE_PAL_DS = VI_TVMODE(VI_PAL, VI_NON_INTERLACE), + VI_TVMODE_EURGB60_INT = VI_TVMODE(VI_EURGB60, VI_INTERLACE), + VI_TVMODE_EURGB60_DS = VI_TVMODE(VI_EURGB60, VI_NON_INTERLACE), + VI_TVMODE_MPAL_INT = VI_TVMODE(VI_MPAL, VI_INTERLACE), + VI_TVMODE_MPAL_DS = VI_TVMODE(VI_MPAL, VI_NON_INTERLACE), + VI_TVMODE_DEBUG_INT = VI_TVMODE(VI_DEBUG, VI_INTERLACE), + VI_TVMODE_DEBUG_PAL_INT = VI_TVMODE(VI_DEBUG_PAL, VI_INTERLACE), + VI_TVMODE_DEBUG_PAL_DS = VI_TVMODE(VI_DEBUG_PAL, VI_NON_INTERLACE) +} VITVMode; + +typedef enum { VI_XFBMODE_SF = 0, VI_XFBMODE_DF } VIXFBMode; + +typedef struct { + /*0x00*/ VITVMode viTVmode; + /*0x04*/ u16 fbWidth; + /*0x06*/ u16 efbHeight; + /*0x08*/ u16 xfbHeight; + /*0x0A*/ u16 viXOrigin; + /*0x0C*/ u16 viYOrigin; + /*0x0E*/ u16 viWidth; + /*0x10*/ u16 viHeight; + /*0x14*/ VIXFBMode xFBmode; + /*0x18*/ u8 field_rendering; + u8 aa; + u8 sample_pattern[12][2]; + u8 vfilter[7]; +} GXRenderModeObj; + +typedef struct { + u8 r; + u8 g; + u8 b; + u8 a; +} GXColor; + +typedef struct { +#ifdef TARGET_PC + u32 dummy[24]; +#else + u32 dummy[8]; +#endif +} GXTexObj; + +typedef struct { +#ifdef TARGET_PC + u32 dummy[12]; +#else + u32 dummy[3]; +#endif +} GXTlutObj; + +typedef struct { + u32 dummy[16]; +} GXLightObj; + +typedef struct { + GXAttr attr; + GXAttrType type; +} GXVtxDescList; + +typedef struct { + GXAttr attr; + GXCompCnt cnt; + GXCompType type; + u8 frac; +} GXVtxAttrFmtList; + +typedef struct { + s16 r; + s16 g; + s16 b; + s16 a; +} GXColorS10; + +typedef struct _GXTexRegion { + u32 dummy[4]; +} GXTexRegion; + +typedef struct _GXTlutRegion { + u32 dummy[4]; +} GXTlutRegion; + +typedef struct _GXFogAdjTable +{ + u16 r[10]; +} GXFogAdjTable; + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gx/GXTev.h b/aurora-main/include/dolphin/gx/GXTev.h new file mode 100644 index 0000000..dccd4d5 --- /dev/null +++ b/aurora-main/include/dolphin/gx/GXTev.h @@ -0,0 +1,35 @@ +#ifndef DOLPHIN_GXTEV_H +#define DOLPHIN_GXTEV_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void GXSetTevOp(GXTevStageID id, GXTevMode mode); +void GXSetTevColorIn(GXTevStageID stage, GXTevColorArg a, GXTevColorArg b, GXTevColorArg c, GXTevColorArg d); +void GXSetTevAlphaIn(GXTevStageID stage, GXTevAlphaArg a, GXTevAlphaArg b, GXTevAlphaArg c, GXTevAlphaArg d); +void GXSetTevColorOp(GXTevStageID stage, GXTevOp op, GXTevBias bias, GXTevScale scale, GXBool clamp, + GXTevRegID out_reg); +void GXSetTevAlphaOp(GXTevStageID stage, GXTevOp op, GXTevBias bias, GXTevScale scale, GXBool clamp, + GXTevRegID out_reg); +void GXSetTevColor(GXTevRegID id, GXColor color); +void GXSetTevColorS10(GXTevRegID id, GXColorS10 color); +void GXSetTevKColor(GXTevKColorID id, GXColor color); +void GXSetTevKColorSel(GXTevStageID stage, GXTevKColorSel sel); +void GXSetTevKAlphaSel(GXTevStageID stage, GXTevKAlphaSel sel); +void GXSetTevSwapMode(GXTevStageID stage, GXTevSwapSel ras_sel, GXTevSwapSel tex_sel); +void GXSetTevSwapModeTable(GXTevSwapSel table, GXTevColorChan red, GXTevColorChan green, GXTevColorChan blue, + GXTevColorChan alpha); +void GXSetAlphaCompare(GXCompare comp0, u8 ref0, GXAlphaOp op, GXCompare comp1, u8 ref1); +void GXSetZTexture(GXZTexOp op, GXTexFmt fmt, u32 bias); +void GXSetTevOrder(GXTevStageID stage, GXTexCoordID coord, GXTexMapID map, GXChannelID color); +void GXSetNumTevStages(u8 nStages); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gx/GXTexture.h b/aurora-main/include/dolphin/gx/GXTexture.h new file mode 100644 index 0000000..5e7afc8 --- /dev/null +++ b/aurora-main/include/dolphin/gx/GXTexture.h @@ -0,0 +1,49 @@ +#ifndef DOLPHIN_GXTEXTURE_H +#define DOLPHIN_GXTEXTURE_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef GXTexRegion* (*GXTexRegionCallback)(const GXTexObj* obj, GXTexMapID id); + +void GXInitTexObj(GXTexObj* obj, const void* data, u16 width, u16 height, GXTexFmt format, GXTexWrapMode wrapS, + GXTexWrapMode wrapT, GXBool mipmap); +void GXInitTexObjCI(GXTexObj* obj, const void* data, u16 width, u16 height, GXCITexFmt format, GXTexWrapMode wrapS, + GXTexWrapMode wrapT, GXBool mipmap, u32 tlut); +void GXInitTexObjData(GXTexObj* obj, const void* data); +void GXInitTexObjLOD(GXTexObj* obj, GXTexFilter min_filt, GXTexFilter mag_filt, f32 min_lod, f32 max_lod, f32 lod_bias, + GXBool bias_clamp, GXBool do_edge_lod, GXAnisotropy max_aniso); +void GXInitTexObjFilter(GXTexObj* obj, GXTexFilter min_filt, GXTexFilter mag_filt); +void GXInitTexObjMaxLOD(GXTexObj* obj, f32 max_lod); +void GXInitTexObjMinLOD(GXTexObj* obj, f32 min_lod); +void GXInitTexObjLODBias(GXTexObj* obj, f32 lod_bias); +void GXInitTexObjBiasClamp(GXTexObj* obj, GXBool bias_clamp); +void GXInitTexObjEdgeLOD(GXTexObj* obj, GXBool do_edge_lod); +void GXInitTexObjMaxAniso(GXTexObj* obj, GXAnisotropy max_aniso); +void GXInitTexObjUserData(GXTexObj* obj, void* user_data); +void* GXGetTexObjUserData(const GXTexObj* obj); +void GXInitTexObjTlut(GXTexObj* obj_, u32 tlut); +void GXLoadTexObj(GXTexObj* obj, GXTexMapID id); +u32 GXGetTexBufferSize(u16 width, u16 height, u32 format, GXBool mipmap, u8 max_lod); +void GXInvalidateTexAll(); +void GXInitTexObjWrapMode(GXTexObj* obj, GXTexWrapMode s, GXTexWrapMode t); +void GXInitTlutObj(GXTlutObj* obj, const void* data, GXTlutFmt format, u16 entries); +void GXInitTlutObjData(GXTlutObj* obj, const void* data); +void GXLoadTlut(const GXTlutObj* obj, u32 idx); +void GXSetTexCoordScaleManually(GXTexCoordID coord, GXBool enable, u16 ss, u16 ts); +void GXSetTexCoordCylWrap(GXTexCoordID coord, GXBool s_enable, GXBool t_enable); +void GXSetTexCoordBias(GXTexCoordID coord, GXBool s_enable, GXBool t_enable); +void GXInitTexCacheRegion(GXTexRegion* region, GXBool is_32b_mipmap, u32 tmem_even, GXTexCacheSize size_even, + u32 tmem_odd, GXTexCacheSize size_odd); +GXTexRegionCallback GXSetTexRegionCallback(GXTexRegionCallback callback); +void GXInvalidateTexRegion(const GXTexRegion* region); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gx/GXTransform.h b/aurora-main/include/dolphin/gx/GXTransform.h new file mode 100644 index 0000000..d961e5a --- /dev/null +++ b/aurora-main/include/dolphin/gx/GXTransform.h @@ -0,0 +1,37 @@ +#ifndef DOLPHIN_GXTRANSFORM_H +#define DOLPHIN_GXTRANSFORM_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define GX_PROJECTION_SZ 7 + +void GXProject(f32 x, f32 y, f32 z, const f32 mtx[3][4], const f32* pm, const f32* vp, f32* sx, f32* sy, f32* sz); +void GXSetProjectionv(const f32* ptr); + +#ifdef TARGET_PC +void GXSetProjection(const void* mtx, GXProjectionType type); +void GXLoadPosMtxImm(const void* mtx, u32 id); +void GXLoadNrmMtxImm(const void* mtx, u32 id); +void GXLoadTexMtxImm(const void* mtx, u32 id, GXTexMtxType type); +#else +void GXSetProjection(f32 mtx[4][4], GXProjectionType type); +void GXLoadPosMtxImm(f32 mtx[3][4], u32 id); +void GXLoadNrmMtxImm(f32 mtx[3][4], u32 id); +void GXLoadTexMtxImm(f32 mtx[][4], u32 id, GXTexMtxType type); +#endif +void GXSetViewport(f32 left, f32 top, f32 wd, f32 ht, f32 nearz, f32 farz); +void GXSetCurrentMtx(u32 id); +void GXSetViewportJitter(f32 left, f32 top, f32 wd, f32 ht, f32 nearz, f32 farz, u32 field); +void GXSetZScaleOffset(f32 scale, f32 offset); +void GXSetScissorBoxOffset(s32 x_off, s32 y_off); +void GXSetClipMode(GXClipMode mode); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/gx/GXVert.h b/aurora-main/include/dolphin/gx/GXVert.h new file mode 100644 index 0000000..355231c --- /dev/null +++ b/aurora-main/include/dolphin/gx/GXVert.h @@ -0,0 +1,150 @@ +#ifndef DOLPHIN_GXVERT_H +#define DOLPHIN_GXVERT_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define GXFIFO_ADDR 0xCC008000 + +typedef union { + u8 u8; + u16 u16; + u32 u32; + u64 u64; + s8 s8; + s16 s16; + s32 s32; + s64 s64; + f32 f32; + f64 f64; +} PPCWGPipe; + +#ifdef __MWERKS__ +volatile PPCWGPipe GXWGFifo : GXFIFO_ADDR; +#else +#define GXWGFifo (*(volatile PPCWGPipe*)GXFIFO_ADDR) +#endif + +#ifdef TARGET_PC + +void GXPosition3f32(f32 x, f32 y, f32 z); +void GXPosition3u16(u16 x, u16 y, u16 z); +void GXPosition3s16(s16 x, s16 y, s16 z); +void GXPosition3u8(u8 x, u8 y, u8 z); +void GXPosition3s8(s8 x, s8 y, s8 z); + +void GXPosition2f32(f32 x, f32 y); +void GXPosition2u16(u16 x, u16 y); +void GXPosition2s16(s16 x, s16 y); +void GXPosition2u8(u8 x, u8 y); +void GXPosition2s8(s8 x, s8 y); + +void GXPosition1x16(u16 index); +void GXPosition1x8(u8 index); + +void GXNormal3f32(f32 x, f32 y, f32 z); +void GXNormal3u16(u16 x, u16 y, u16 z); +void GXNormal3s16(s16 x, s16 y, s16 z); +void GXNormal3u8(u8 x, u8 y, u8 z); +void GXNormal3s8(s8 x, s8 y, s8 z); + +void GXNormal1x16(u16 index); +void GXNormal1x8(u8 index); + +void GXColor4u8(u8 r, u8 g, u8 b, u8 a); + +void GXColor3u8(u8 r, u8 g, u8 b); + +void GXColor1u32(u32 clr); +void GXColor1u16(u16 clr); + +void GXColor1x16(u16 index); +void GXColor1x8(u8 index); + +void GXTexCoord2f32(f32 s, f32 t); +void GXTexCoord2u16(u16 s, u16 t); +void GXTexCoord2s16(s16 s, s16 t); +void GXTexCoord2u8(u8 s, u8 t); +void GXTexCoord2s8(s8 s, s8 t); + +void GXTexCoord1f32(f32 s); +void GXTexCoord1u16(u16 s); +void GXTexCoord1s16(s16 s); +void GXTexCoord1u8(u8 s); +void GXTexCoord1s8(s8 s); + +void GXTexCoord1x16(u16 index); +void GXTexCoord1x8(u8 index); + +extern void GXEnd(void); + +#else + +static inline void GXPosition2f32(const f32 x, const f32 y) { + GXWGFifo.f32 = x; + GXWGFifo.f32 = y; +} + +static inline void GXPosition3s16(const s16 x, const s16 y, const s16 z) { + GXWGFifo.s16 = x; + GXWGFifo.s16 = y; + GXWGFifo.s16 = z; +} + +static inline void GXPosition3f32(const f32 x, const f32 y, const f32 z) { + GXWGFifo.f32 = x; + GXWGFifo.f32 = y; + GXWGFifo.f32 = z; +} + +static inline void GXNormal3f32(const f32 x, const f32 y, const f32 z) { + GXWGFifo.f32 = x; + GXWGFifo.f32 = y; + GXWGFifo.f32 = z; +} + +static inline void GXColor4u8(const u8 r, const u8 g, const u8 b, const u8 a) { + GXWGFifo.u8 = r; + GXWGFifo.u8 = g; + GXWGFifo.u8 = b; + GXWGFifo.u8 = a; +} + +static inline void GXTexCoord2s16(const s16 u, const s16 v) { + GXWGFifo.s16 = u; + GXWGFifo.s16 = v; +} + +static inline void GXTexCoord2f32(const f32 u, const f32 v) { + GXWGFifo.f32 = u; + GXWGFifo.f32 = v; +} + +static inline void GXEnd(void) {} + +#endif + +void GXCmd1u8(const u8 x); +void GXCmd1u16(const u16 x); +void GXCmd1u32(const u32 x); +void GXCmd1u64(const u64 x); + +void GXParam1u8(const u8 x); +void GXParam1u16(const u16 x); +void GXParam1u32(const u32 x); +void GXParam1s8(const s8 x); +void GXParam1s16(const s16 x); +void GXParam1s32(const s32 x); +void GXParam1f32(const f32 x); +void GXParam3f32(const f32 x, const f32 y, const f32 z); +void GXParam4f32(const f32 x, const f32 y, const f32 z, const f32 w); +void GXMatrixIndex1u8(GXAttr attr, u8 index); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/hio.h b/aurora-main/include/dolphin/hio.h new file mode 100644 index 0000000..dc83ee1 --- /dev/null +++ b/aurora-main/include/dolphin/hio.h @@ -0,0 +1,28 @@ +#ifndef _DOLPHIN_HIO_H_ +#define _DOLPHIN_HIO_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*HIOCallback)(void); +typedef BOOL (*HIOEnumCallback)(s32 chan); + +BOOL HIOEnumDevices(HIOEnumCallback callback); +BOOL HIOInit(s32 chan, HIOCallback callback); +BOOL HIOInitEx(s32 chan, u32 dev, HIOCallback callback); +BOOL HIOReadMailbox(u32* word); +BOOL HIOWriteMailbox(u32 word); +BOOL HIORead(u32 addr, void* buffer, s32 size); +BOOL HIOWrite(u32 addr, void* buffer, s32 size); +BOOL HIOReadAsync(u32 addr, void* buffer, s32 size, HIOCallback callback); +BOOL HIOWriteAsync(u32 addr, void* buffer, s32 size, HIOCallback callback); +BOOL HIOReadStatus(u32* status); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/hio2.h b/aurora-main/include/dolphin/hio2.h new file mode 100644 index 0000000..46b873c --- /dev/null +++ b/aurora-main/include/dolphin/hio2.h @@ -0,0 +1,45 @@ +#ifndef _REVOLUTION_HIO2_H_ +#define _REVOLUTION_HIO2_H_ + +// Actually from revolution but who cares? +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + HIO2_DEVICE_INVALID = -1, + HIO2_DEVICE_UNK_0 = 0, + HIO2_DEVICE_UNK_1 = 1, + HIO2_DEVICE_UNK_2 = 2, +} HIO2DeviceType; + +typedef BOOL (*HIO2EnumCallback)(HIO2DeviceType); +typedef void (*HIO2UnkCallback)(s32); +typedef void (*HIO2DisconnectCallback)(s32); + +typedef struct HIO2Control { + /* 0x00 */ HIO2DeviceType type; + /* 0x04 */ s32 chan; + /* 0x08 */ u32 _0x8; + /* 0x0C */ HIO2UnkCallback exiCallback; + /* 0x10 */ HIO2UnkCallback _0x10; + /* 0x14 */ HIO2UnkCallback _0x14; + /* 0x18 */ HIO2DisconnectCallback disconnectCb; +} HIO2Control; // size 0x1C + + +BOOL HIO2Init(void); +BOOL HIO2EnumDevices(HIO2EnumCallback callback); +s32 HIO2Open(HIO2DeviceType type, HIO2UnkCallback exiCb, HIO2DisconnectCallback disconnectCb); +BOOL HIO2Close(s32 handle); +BOOL HIO2Read(s32 handle, u32 addr, void* buffer, s32 size); +BOOL HIO2Write(s32 handle, u32 addr, void* buffer, s32 size); +void HIO2Exit(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/hw_regs.h b/aurora-main/include/dolphin/hw_regs.h new file mode 100644 index 0000000..1f5700a --- /dev/null +++ b/aurora-main/include/dolphin/hw_regs.h @@ -0,0 +1,116 @@ +#ifndef _DOLPHIN_HW_REGS_H_ +#define _DOLPHIN_HW_REGS_H_ + +#include + +#ifdef __MWERKS__ +volatile u16 __VIRegs[59] AT_ADDRESS(0xCC002000); +volatile u32 __PIRegs[12] AT_ADDRESS(0xCC003000); +volatile u16 __MEMRegs[64] AT_ADDRESS(0xCC004000); +volatile u16 __DSPRegs[] AT_ADDRESS(0xCC005000); +volatile u32 __DIRegs[] AT_ADDRESS(0xCC006000); +volatile u32 __SIRegs[0x100] AT_ADDRESS(0xCC006400); +volatile u32 __EXIRegs[0x40] AT_ADDRESS(0xCC006800); +volatile u32 __AIRegs[8] AT_ADDRESS(0xCC006C00); +#else +#define __VIRegs ((volatile u16 *)0xCC002000) +#define __PIRegs ((volatile u32 *)0xCC003000) +#define __MEMRegs ((volatile u16 *)0xCC004000) +#define __DSPRegs ((volatile u16 *)0xCC005000) +#define __DIRegs ((volatile u32 *)0xCC006000) +#define __SIRegs ((volatile u32 *)0xCC006400) +#define __EXIRegs ((volatile u32 *)0xCC006800) +#define __AIRegs ((volatile u32 *)0xCC006C00) +#endif + +// Offsets for __VIRegs + +// offsets for __VIRegs[i] +#define VI_VERT_TIMING (0) +#define VI_DISP_CONFIG (1) +#define VI_HORIZ_TIMING_0L (2) +#define VI_HORIZ_TIMING_0U (3) +#define VI_HORIZ_TIMING_1L (4) +#define VI_HORIZ_TIMING_1U (5) +#define VI_VERT_TIMING_ODD (6) +#define VI_VERT_TIMING_ODD_U (7) +#define VI_VERT_TIMING_EVEN (8) +#define VI_VERT_TIMING_EVEN_U (9) + +#define VI_BBI_ODD (10) // burst blanking interval +#define VI_BBI_ODD_U (11) // burst blanking interval +#define VI_BBI_EVEN (12) // burst blanking interval +#define VI_BBI_EVEN_U (13) // burst blanking interval + +#define VI_TOP_FIELD_BASE_LEFT (14) // top in 2d, top of left pic in 3d +#define VI_TOP_FIELD_BASE_LEFT_U (15) // top in 2d, top of left pic in 3d + +#define VI_TOP_FIELD_BASE_RIGHT (16) // top of right pic in 3d +#define VI_TOP_FIELD_BASE_RIGHT_U (17) // top of right pic in 3d + +#define VI_BTTM_FIELD_BASE_LEFT (18) // bottom in 2d, bottom of left pic in 3d +#define VI_BTTM_FIELD_BASE_LEFT_U (19) // bottom in 2d, bottom of left pic in 3d + +#define VI_BTTM_FIELD_BASE_RIGHT (20) // bottom of right pic in 3d +#define VI_BTTM_FIELD_BASE_RIGHT_U (21) // bottom of right pic in 3d + +#define VI_VERT_COUNT (22) // vertical display position +#define VI_HORIZ_COUNT (23) // horizontal display position + +#define VI_DISP_INT_0 (24) // display interrupt 0L +#define VI_DISP_INT_0U (25) // display interrupt 0U +#define VI_DISP_INT_1 (26) // display interrupt 1L +#define VI_DISP_INT_1U (27) // display interrupt 1U +#define VI_DISP_INT_2 (28) // display interrupt 2L +#define VI_DISP_INT_2U (29) // display interrupt 2U +#define VI_DISP_INT_3 (30) // display interrupt 3L +#define VI_DISP_INT_3U (31) // display interrupt 3U + +#define VI_HSW (36) // horizontal scaling width +#define VI_HSR (37) // horizontal scaling register + +#define VI_FCT_0 (38) // filter coefficient table 0L +#define VI_FCT_0U (39) // filter coefficient table 0U +#define VI_FCT_1 (40) // filter coefficient table 1L +#define VI_FCT_1U (41) // filter coefficient table 1U +#define VI_FCT_2 (42) // filter coefficient table 2L +#define VI_FCT_2U (43) // filter coefficient table 2U +#define VI_FCT_3 (44) // filter coefficient table 3L +#define VI_FCT_3U (45) // filter coefficient table 3U +#define VI_FCT_4 (46) // filter coefficient table 4L +#define VI_FCT_4U (47) // filter coefficient table 4U +#define VI_FCT_5 (48) // filter coefficient table 5L +#define VI_FCT_5U (49) // filter coefficient table 5U +#define VI_FCT_6 (50) // filter coefficient table 6L +#define VI_FCT_6U (51) // filter coefficient table 6U + +#define VI_CLOCK_SEL (54) // clock select +#define VI_DTV_STAT (55) // DTV status + +#define VI_WIDTH (56) + +// offsets for __DSPRegs[i] +#define DSP_MAILBOX_IN_HI (0) +#define DSP_MAILBOX_IN_LO (1) +#define DSP_MAILBOX_OUT_HI (2) +#define DSP_MAILBOX_OUT_LO (3) +#define DSP_CONTROL_STATUS (5) + +#define DSP_ARAM_SIZE (9) +#define DSP_ARAM_MODE (11) +#define DSP_ARAM_REFRESH (13) +#define DSP_ARAM_DMA_MM_HI (16) // Main mem address +#define DSP_ARAM_DMA_MM_LO (17) +#define DSP_ARAM_DMA_ARAM_HI (18) // ARAM address +#define DSP_ARAM_DMA_ARAM_LO (19) +#define DSP_ARAM_DMA_SIZE_HI (20) // DMA buffer size +#define DSP_ARAM_DMA_SIZE_LO (21) + +#define DSP_DMA_START_HI (24) // DMA start address +#define DSP_DMA_START_LO (25) +#define DSP_DMA_CONTROL_LEN (27) +#define DSP_DMA_BYTES_LEFT (29) + +#define DSP_DMA_START_FLAG (0x8000) // set to start DSP + +#endif diff --git a/aurora-main/include/dolphin/ms.h b/aurora-main/include/dolphin/ms.h new file mode 100644 index 0000000..0cd4c55 --- /dev/null +++ b/aurora-main/include/dolphin/ms.h @@ -0,0 +1,35 @@ +// Aurora exclusive API extension +// While the GC and Wii could both technically support mice, they never received official libraries +// This library is a theoretical implementation based loosely on PAD +#ifndef DOLPHIN_MS_H +#define DOLPHIN_MS_H +#include "types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct MSStatus { + f32 x; + f32 y; + f32 xrel; + f32 yrel; + u32 buttons; + f32 scrollX; + f32 scrollY; +} MSStatus; + +#define MS_BUTTON_LEFT (1 << 0) +#define MS_BUTTON_MIDDLE (1 << 1) +#define MS_BUTTON_RIGHT (1 << 2) +#define MS_BUTTON_X1 (1 << 3) +#define MS_BUTTON_X2 (1 << 4) + +void MSPoll(); +void MSRead(MSStatus* status); + +#ifdef __cplusplus +} +#endif + +#endif // DUSK_MS_H diff --git a/aurora-main/include/dolphin/mtx.h b/aurora-main/include/dolphin/mtx.h new file mode 100644 index 0000000..7172615 --- /dev/null +++ b/aurora-main/include/dolphin/mtx.h @@ -0,0 +1,385 @@ +#ifndef _DOLPHIN_MTX +#define _DOLPHIN_MTX + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#if !defined(MTX_USE_PS) && !defined(MTX_USE_C) && !defined(GEKKO) +#define MTX_USE_C +#endif + +#if !defined(MTX_USE_PS) && !defined(MTX_USE_C) && defined(GEKKO) +#ifndef _DEBUG +#define MTX_USE_PS +#endif +#endif + +#define MTX_PTR_OFFSET 3 + +#define MTX44_PTR_OFFSET 4 + +typedef struct { + u32 numMtx; + MtxPtr stackBase; + MtxPtr stackPtr; +} MTXStack, MtxStack, *MtxStackPtr; + +#define MTXDegToRad(a) ((a)*0.01745329252f) +#define MTXRadToDeg(a) ((a)*57.29577951f) +#define MTXRowCol(m, r, c) ((m)[(r)][(c)]) + +void C_MTXIdentity(Mtx m); +void C_MTXCopy(const Mtx src, Mtx dst); +void C_MTXConcat(const Mtx a, const Mtx b, Mtx ab); +void C_MTXConcatArray(const Mtx a, const Mtx* srcBase, Mtx* dstBase, u32 count); +void C_MTXTranspose(const Mtx src, Mtx xPose); +u32 C_MTXInverse(const Mtx src, Mtx inv); +u32 C_MTXInvXpose(const Mtx src, Mtx invX); + +#ifdef GEKKO +void PSMTXIdentity(Mtx m); +void PSMTXCopy(const Mtx src, Mtx dst); +void PSMTXConcat(const Mtx a, const Mtx b, Mtx ab); +void PSMTXConcatArray(const Mtx a, const Mtx* srcBase, Mtx* dstBase, u32 count); +void PSMTXTranspose(const Mtx src, Mtx xPose); +u32 PSMTXInverse(const Mtx src, Mtx inv); +u32 PSMTXInvXpose(const Mtx src, Mtx invX); +#else +#define PSMTXIdentity MTXIdentity +#define PSMTXCopy MTXCopy +#define PSMTXConcat MTXConcat +#define PSMTXConcatArray MTXConcatArray +#define PSMTXTranspose MTXTranspose +#define PSMTXInverse MTXInverse +#define PSMTXInvXpose MTXInvXpose +#endif + +#ifdef MTX_USE_PS +#define MTXIdentity C_MTXIdentity +#define MTXCopy C_MTXCopy +#define MTXConcat C_MTXConcat +#define MTXConcatArray C_MTXConcatArray +#define MTXTranspose C_MTXTranspose +#define MTXInverse C_MTXInverse +#define MTXInvXpose C_MTXInvXpose +#else +#define MTXIdentity C_MTXIdentity +#define MTXCopy C_MTXCopy +#define MTXConcat C_MTXConcat +#define MTXConcatArray C_MTXConcatArray +#define MTXTranspose C_MTXTranspose +#define MTXInverse C_MTXInverse +#define MTXInvXpose C_MTXInvXpose +#endif + +void C_MTXMultVec(const Mtx m, const Vec* src, Vec* dst); +void C_MTXMultVecArray(const Mtx m, const Vec* srcBase, Vec* dstBase, u32 count); +void C_MTXMultVecSR(const Mtx m, const Vec* src, Vec* dst); +void C_MTXMultVecArraySR(const Mtx m, const Vec* srcBase, Vec* dstBase, u32 count); +void C_MTXROMultVecArray(const ROMtx m, const Vec *srcBase, Vec *dstBase, u32 count); + +#ifdef GEKKO +void PSMTXMultVec(const Mtx m, const Vec* src, Vec* dst); +void PSMTXMultVecArray(const Mtx m, const Vec* srcBase, Vec* dstBase, u32 count); +void PSMTXMultVecSR(const Mtx m, const Vec* src, Vec* dst); +void PSMTXMultVecArraySR(const Mtx m, const Vec* srcBase, Vec* dstBase, u32 count); +void PSMTXMultS16VecArray(const Mtx m, const S16Vec* srcBase, Vec* dstBase, u32 count); + +void PSMTXROMultVecArray(const ROMtx m, const Vec *srcBase, Vec *dstBase, u32 count); +void PSMTXROSkin2VecArray(const ROMtx m0, const ROMtx m1, const f32* wtBase, const Vec* srcBase, + Vec* dstBase, u32 count); +void PSMTXROMultS16VecArray(const ROMtx m, const S16Vec* srcBase, Vec* dstBase, u32 count); +#else +#define PSMTXMultVec MTXMultVec +#define PSMTXMultVecArray MTXMultVecArray +#define PSMTXMultVecSR MTXMultVecSR +#define PSMTXMultVecArraySR MTXMultVecArraySR +#define PSMTXMultS16VecArray MTXMultS16VecArray + +#define PSMTXROMultVecArray MTXROMultVecArray +#define PSMTXROSkin2VecArray MTXROSkin2VecArray +#define PSMTXROMultS16VecArray MTXROMultS16VecArray +#endif + +#ifdef MTX_USE_PS +#define MTXMultVec C_MTXMultVec +#define MTXMultVecArray C_MTXMultVecArray +#define MTXMultVecSR C_MTXMultVecSR +#define MTXMultVecArraySR C_MTXMultVecArraySR +#define MTXMultS16VecArray PSMTXMultS16VecArray + +#define MTXROMultVecArray C_MTXROMultVecArray +#define MTXROSkin2VecArray PSMTXROSkin2VecArray +#define MTXROMultS16VecArray PSMTXROMultS16VecArray +#else // MTX_USE_C +#define MTXMultVec C_MTXMultVec +#define MTXMultVecArray C_MTXMultVecArray +#define MTXMultVecSR C_MTXMultVecSR +#define MTXMultVecArraySR C_MTXMultVecArraySR + +#define MTXROMultVecArray C_MTXROMultVecArray +#endif + +void C_MTXQuat(Mtx m, const Quaternion* q); +void C_MTXReflect(Mtx m, const Vec* p, const Vec* n); + +void C_MTXTrans(Mtx m, f32 xT, f32 yT, f32 zT); +void C_MTXTransApply(const Mtx src, Mtx dst, f32 xT, f32 yT, f32 zT); +void C_MTXScale(Mtx m, f32 xS, f32 yS, f32 zS); +void C_MTXScaleApply(const Mtx src, Mtx dst, f32 xS, f32 yS, f32 zS); + +void C_MTXRotRad(Mtx m, char axis, f32 rad); +void C_MTXRotTrig(Mtx m, char axis, f32 sinA, f32 cosA); +void C_MTXRotAxisRad(Mtx m, const Vec* axis, f32 rad); + +void C_MTXReorder(const Mtx src, ROMtx dst); + +#ifdef GEKKO +void PSMTXQuat(Mtx m, const Quaternion* q); +void PSMTXReflect(Mtx m, const Vec* p, const Vec* n); + +void PSMTXTrans(Mtx m, f32 xT, f32 yT, f32 zT); +void PSMTXTransApply(const Mtx src, Mtx dst, f32 xT, f32 yT, f32 zT); +void PSMTXScale(Mtx m, f32 xS, f32 yS, f32 zS); +void PSMTXScaleApply(const Mtx src, Mtx dst, f32 xS, f32 yS, f32 zS); + +void PSMTXRotRad(Mtx m, char axis, f32 rad); +void PSMTXRotTrig(Mtx m, char axis, f32 sinA, f32 cosA); +void PSMTXRotAxisRad(Mtx m, const Vec* axis, f32 rad); + +void PSMTXReorder(const Mtx src, ROMtx dest); +#else +#define PSMTXQuat MTXQuat +#define PSMTXReflect MTXReflect +#define PSMTXTrans MTXTrans +#define PSMTXTransApply MTXTransApply +#define PSMTXScale MTXScale +#define PSMTXScaleApply MTXScaleApply +#define PSMTXRotRad MTXRotRad +#define PSMTXRotTrig MTXRotTrig +#define PSMTXRotAxisRad MTXRotAxisRad + +#define PSMTXRotRad MTXRotRad +#define PSMTXRotAxisRad MTXRotAxisRad + +#define PSMTXReorder MTXReorder +#endif + +#ifdef MTX_USE_PS +#define MTXQuat C_MTXQuat +#define MTXReflect C_MTXReflect +#define MTXTrans C_MTXTrans +#define MTXTransApply C_MTXTransApply +#define MTXScale C_MTXScale +#define MTXScaleApply C_MTXScaleApply +#define MTXRotRad C_MTXRotRad +#define MTXRotTrig C_MTXRotTrig +#define MTXRotAxisRad C_MTXRotAxisRad + +#define MTXRotDeg(m, axis, deg) C_MTXRotRad(m, axis, MTXDegToRad(deg)) +#define MTXRotAxisDeg(m, axis, deg) C_MTXRotAxisRad(m, axis, MTXDegToRad(deg)) + +#define MTXReorder C_MTXReorder + +#else // MTX_USE_C +#define MTXQuat C_MTXQuat +#define MTXReflect C_MTXReflect +#define MTXTrans C_MTXTrans +#define MTXTransApply C_MTXTransApply +#define MTXScale C_MTXScale +#define MTXScaleApply C_MTXScaleApply +#define MTXRotRad C_MTXRotRad +#define MTXRotTrig C_MTXRotTrig +#define MTXRotAxisRad C_MTXRotAxisRad + +#define MTXRotDeg(m, axis, deg) C_MTXRotRad(m, axis, MTXDegToRad(deg)) +#define MTXRotAxisDeg(m, axis, deg) C_MTXRotAxisRad(m, axis, MTXDegToRad(deg)) + +#define MTXReorder C_MTXReorder + +#endif + +void C_MTXLookAt(Mtx m, const Point3d* camPos, const Vec* camUp, const Point3d* target); + +#define MTXLookAt C_MTXLookAt + +void C_MTXFrustum(Mtx44 m, f32 t, f32 b, f32 l, f32 r, f32 n, f32 f); +void C_MTXPerspective(Mtx44 m, f32 fovY, f32 aspect, f32 n, f32 f); +void C_MTXOrtho(Mtx44 m, f32 t, f32 b, f32 l, f32 r, f32 n, f32 f); + +#define MTXFrustum C_MTXFrustum +#define MTXPerspective C_MTXPerspective +#define MTXOrtho C_MTXOrtho + +void C_MTXLightFrustum(Mtx m, f32 t, f32 b, f32 l, f32 r, f32 n, f32 scaleS, f32 scaleT, f32 transS, + f32 transT); + +void C_MTXLightPerspective(Mtx m, f32 fovY, f32 aspect, f32 scaleS, f32 scaleT, f32 transS, + f32 transT); + +void C_MTXLightOrtho(Mtx m, f32 t, f32 b, f32 l, f32 r, f32 scaleS, f32 scaleT, f32 transS, + f32 transT); + +#define MTXLightFrustum C_MTXLightFrustum +#define MTXLightPerspective C_MTXLightPerspective +#define MTXLightOrtho C_MTXLightOrtho + +void C_VECAdd(const Vec* a, const Vec* b, Vec* ab); +void C_VECSubtract(const Vec* a, const Vec* b, Vec* a_b); +void C_VECScale(const Vec* src, Vec* dst, f32 scale); +void C_VECNormalize(const Vec* src, Vec* unit); +f32 C_VECSquareMag(const Vec* v); +f32 C_VECMag(const Vec* v); +f32 C_VECDotProduct(const Vec* a, const Vec* b); +void C_VECCrossProduct(const Vec* a, const Vec* b, Vec* axb); +f32 C_VECSquareDistance(const Vec* a, const Vec* b); +f32 C_VECDistance(const Vec* a, const Vec* b); +void C_VECReflect(const Vec* src, const Vec* normal, Vec* dst); +void C_VECHalfAngle(const Vec* a, const Vec* b, Vec* half); + +void PSVECNormalize(const Vec* src, Vec* unit); +f32 PSVECSquareMag(const Vec* v); +f32 PSVECMag(const Vec* v); +f32 PSVECSquareDistance(const Vec* a, const Vec* b); +f32 PSVECDistance(const Vec* a, const Vec* b); + +#ifdef GEKKO +void PSVECAdd(const Vec* a, const Vec* b, Vec* ab); +void PSVECSubtract(const Vec* a, const Vec* b, Vec* a_b); +void PSVECScale(const Vec* src, Vec* dst, f32 scale); +void PSVECNormalize(const Vec* src, Vec* unit); +f32 PSVECSquareMag(const Vec* v); +f32 PSVECMag(const Vec* v); +f32 PSVECDotProduct(const Vec* a, const Vec* b); +void PSVECCrossProduct(const Vec* a, const Vec* b, Vec* axb); +f32 PSVECSquareDistance(const Vec* a, const Vec* b); +f32 PSVECDistance(const Vec* a, const Vec* b); +#else +#define PSVECAdd VECAdd +#define PSVECSubtract VECSubtract +#define PSVECScale VECScale +#define PSVECDotProduct VECDotProduct +#define PSVECCrossProduct VECCrossProduct +#endif + +#if defined(MTX_USE_PS) +#define VECAdd C_VECAdd +#define VECSubtract C_VECSubtract +#define VECScale C_VECScale +#define VECNormalize PSVECNormalize +#define VECSquareMag PSVECSquareMag +#define VECMag PSVECMag +#define VECDotProduct C_VECDotProduct +#define VECCrossProduct C_VECCrossProduct +#define VECSquareDistance PSVECSquareDistance +#define VECDistance PSVECDistance +#else // MTX_USE_C +#define VECAdd C_VECAdd +#define VECSubtract C_VECSubtract +#define VECScale C_VECScale +#define VECNormalize C_VECNormalize +#define VECSquareMag C_VECSquareMag +#define VECMag C_VECMag +#define VECDotProduct C_VECDotProduct +#define VECCrossProduct C_VECCrossProduct +#define VECSquareDistance C_VECSquareDistance +#define VECDistance C_VECDistance +#endif + +#define VECReflect C_VECReflect +#define VECHalfAngle C_VECHalfAngle + +void C_QUATAdd(const Quaternion* p, const Quaternion* q, Quaternion* r); +void C_QUATSubtract(const Quaternion* p, const Quaternion* q, Quaternion* r); +void C_QUATMultiply(const Quaternion* p, const Quaternion* q, Quaternion* pq); +void C_QUATDivide(const Quaternion* p, const Quaternion* q, Quaternion* r); +void C_QUATScale(const Quaternion* q, Quaternion* r, f32 scale); +f32 C_QUATDotProduct(const Quaternion* p, const Quaternion* q); +void C_QUATNormalize(const Quaternion* src, Quaternion* unit); +void C_QUATInverse(const Quaternion* src, Quaternion* inv); +void C_QUATExp(const Quaternion* q, Quaternion* r); +void C_QUATLogN(const Quaternion* q, Quaternion* r); + +void C_QUATMakeClosest(const Quaternion* q, const Quaternion* qto, Quaternion* r); +void C_QUATRotAxisRad(Quaternion* r, const Vec* axis, f32 rad); +void C_QUATMtx(Quaternion* r, const Mtx m); + +void C_QUATLerp(const Quaternion* p, const Quaternion* q, Quaternion* r, f32 t); +void C_QUATSlerp(const Quaternion* p, const Quaternion* q, Quaternion* r, f32 t); +void C_QUATSquad(const Quaternion* p, const Quaternion* a, const Quaternion* b, const Quaternion* q, + Quaternion* r, f32 t); +void C_QUATCompA(const Quaternion* qprev, const Quaternion* q, const Quaternion* qnext, + Quaternion* a); + +#ifdef GEKKO +void PSQUATAdd(const Quaternion* p, const Quaternion* q, Quaternion* r); +void PSQUATSubtract(const Quaternion* p, const Quaternion* q, Quaternion* r); +void PSQUATMultiply(const Quaternion* p, const Quaternion* q, Quaternion* pq); +void PSQUATDivide(const Quaternion* p, const Quaternion* q, Quaternion* r); +void PSQUATScale(const Quaternion* q, Quaternion* r, f32 scale); +f32 PSQUATDotProduct(const Quaternion* p, const Quaternion* q); +void PSQUATNormalize(const Quaternion* src, Quaternion* unit); +void PSQUATInverse(const Quaternion* src, Quaternion* inv); +#else +#define PSQUATAdd QUATAdd +#define PSQUATSubtract QUATSubtract +#define PSQUATMultiply QUATMultiply +#define PSQUATDivide QUATDivide +#define PSQUATScale QUATScale +#define PSQUATDotProduct QUATDotProduct +#define PSQUATNormalize QUATNormalize +#define PSQUATInverse QUATInverse +#endif + +#ifdef MTX_USE_PS +#define QUATAdd C_QUATAdd +#define QUATSubtract C_QUATSubtract +#define QUATMultiply C_QUATMultiply +#define QUATDivide C_QUATDivide +#define QUATScale C_QUATScale +#define QUATDotProduct C_QUATDotProduct +#define QUATNormalize C_QUATNormalize +#define QUATInverse C_QUATInverse +#else // MTX_USE_C +#define QUATAdd C_QUATAdd +#define QUATSubtract C_QUATSubtract +#define QUATMultiply C_QUATMultiply +#define QUATDivide C_QUATDivide +#define QUATScale C_QUATScale +#define QUATDotProduct C_QUATDotProduct +#define QUATNormalize C_QUATNormalize +#define QUATInverse C_QUATInverse +#endif + +#define QUATExp C_QUATExp +#define QUATLogN C_QUATLogN +#define QUATMakeClosest C_QUATMakeClosest +#define QUATRotAxisRad C_QUATRotAxisRad +#define QUATMtx C_QUATMtx +#define QUATLerp C_QUATLerp +#define QUATSlerp C_QUATSlerp +#define QUATSquad C_QUATSquad +#define QUATCompA C_QUATCompA + +void MTXInitStack(MtxStack* sPtr, u32 numMtx); +MtxPtr MTXPush(MtxStack* sPtr, const Mtx m); +MtxPtr MTXPushFwd(MtxStack* sPtr, const Mtx m); +MtxPtr MTXPushInv(MtxStack* sPtr, const Mtx m); +MtxPtr MTXPushInvXpose(MtxStack* sPtr, const Mtx m); +MtxPtr MTXPop(MtxStack* sPtr); +MtxPtr MTXGetStackPtr(const MtxStack* sPtr); + +#define MTXAllocStack(sPtr, numMtx) \ + (((MtxStackPtr)(sPtr))->stackBase = (MtxPtr)OSAlloc(((numMtx) * sizeof(Mtx)))) + +#define MTXFreeStack(sPtr) (OSFree((void*)(((MtxStackPtr)(sPtr))->stackBase))) + +#ifdef __cplusplus +} +#endif + +#endif // _DOLPHIN_MTX diff --git a/aurora-main/include/dolphin/mtx/GeoTypes.h b/aurora-main/include/dolphin/mtx/GeoTypes.h new file mode 100644 index 0000000..985c2d3 --- /dev/null +++ b/aurora-main/include/dolphin/mtx/GeoTypes.h @@ -0,0 +1,40 @@ +#ifndef _DOLPHIN_GEOTYPES +#define _DOLPHIN_GEOTYPES + +#include + +#ifdef __cplusplus +extern "C" { +#endif + + typedef struct Vec { + f32 x, y, z; + } Vec, *VecPtr, Point3d, *Point3dPtr; + + typedef struct { + s16 x; + s16 y; + s16 z; + } S16Vec, *S16VecPtr; + + typedef struct { + f32 x, y, z, w; + } Quaternion, *QuaternionPtr, Qtrn, *QtrnPtr; + + typedef f32 Mtx[3][4]; + + typedef f32 (*MtxPtr)[4]; + + typedef f32 ROMtx[4][3]; + + typedef f32 (*ROMtxPtr)[3]; + + typedef f32 Mtx44[4][4]; + + typedef f32 (*Mtx44Ptr)[4]; + +#ifdef __cplusplus +} +#endif + +#endif // _DOLPHIN_GEOTYPES diff --git a/aurora-main/include/dolphin/mtx/mtx44ext.h b/aurora-main/include/dolphin/mtx/mtx44ext.h new file mode 100644 index 0000000..391ea57 --- /dev/null +++ b/aurora-main/include/dolphin/mtx/mtx44ext.h @@ -0,0 +1,106 @@ +#include + +#ifndef _DOLPHIN_MTX44EXT +#define _DOLPHIN_MTX44EXT + +#ifdef __cplusplus +extern "C" { +#endif + +void C_MTX44Identity(Mtx44 m); +void C_MTX44Copy(const Mtx44 src, Mtx44 dst); +void C_MTX44Concat(const Mtx44 a, const Mtx44 b, Mtx44 ab); +void C_MTX44Transpose(const Mtx44 src, Mtx44 xPose); +u32 C_MTX44Inverse(const Mtx44 src, Mtx44 inv); + +#ifdef GEKKO +void PSMTX44Identity(Mtx44 m); +void PSMTX44Copy(const Mtx44 src, Mtx44 dst); +void PSMTX44Concat(const Mtx44 a, const Mtx44 b, Mtx44 ab); +void PSMTX44Transpose(const Mtx44 src, Mtx44 xPose); +#endif + +#ifdef MTX_USE_PS +#define MTX44Identity PSMTX44Identity +#define MTX44Copy PSMTX44Copy +#define MTX44Concat PSMTX44Concat +#define MTX44Transpose PSMTX44Transpose +#else +#define MTX44Identity C_MTX44Identity +#define MTX44Copy C_MTX44Copy +#define MTX44Concat C_MTX44Concat +#define MTX44Transpose C_MTX44Transpose +#endif + +#define MTX44Inverse C_MTX44Inverse + +void C_MTX44Trans(Mtx44 m, f32 xT, f32 yT, f32 zT); +void C_MTX44TransApply(const Mtx44 src, Mtx44 dst, f32 xT, f32 yT, f32 zT); +void C_MTX44Scale(Mtx44 m, f32 xS, f32 yS, f32 zS); +void C_MTX44ScaleApply(const Mtx44 src, Mtx44 dst, f32 xS, f32 yS, f32 zS); + +void C_MTX44RotRad(Mtx44 m, char axis, f32 rad); +void C_MTX44RotTrig(Mtx44 m, char axis, f32 sinA, f32 cosA); +void C_MTX44RotAxisRad(Mtx44 m, const Vec* axis, f32 rad); + +#ifdef GEKKO +void PSMTX44Trans(Mtx44 m, f32 xT, f32 yT, f32 zT); +void PSMTX44TransApply(const Mtx44 src, Mtx44 dst, f32 xT, f32 yT, f32 zT); +void PSMTX44Scale(Mtx44 m, f32 xS, f32 yS, f32 zS); +void PSMTX44ScaleApply(const Mtx44 src, Mtx44 dst, f32 xS, f32 yS, f32 zS); + +void PSMTX44RotRad(Mtx44 m, char axis, f32 rad); +void PSMTX44RotTrig(Mtx44 m, char axis, f32 sinA, f32 cosA); +void PSMTX44RotAxisRad(Mtx44 m, const Vec* axis, f32 rad); +#endif + +#ifdef MTX_USE_PS +#define MTX44Trans PSMTX44Trans +#define MTX44TransApply PSMTX44TransApply +#define MTX44Scale PSMTX44Scale +#define MTX44ScaleApply PSMTX44ScaleApply + +#define MTX44RotRad PSMTX44RotRad +#define MTX44RotTrig PSMTX44RotTrig +#define MTX44RotAxisRad PSMTX44RotAxisRad + +#else +#define MTX44Trans C_MTX44Trans +#define MTX44TransApply C_MTX44TransApply +#define MTX44Scale C_MTX44Scale +#define MTX44ScaleApply C_MTX44ScaleApply + +#define MTX44RotRad C_MTX44RotRad +#define MTX44RotTrig C_MTX44RotTrig +#define MTX44RotAxisRad C_MTX44RotAxisRad +#endif + +void C_MTX44MultVec(const Mtx44 m, const Vec* src, Vec* dst); +void C_MTX44MultVecArray(const Mtx44 m, const Vec* srcBase, Vec* dstBase, u32 count); +void C_MTX44MultVecSR(const Mtx44 m, const Vec* src, Vec* dst); +void C_MTX44MultVecArraySR(const Mtx44 m, const Vec* srcBase, Vec* dstBase, u32 count); + +#ifdef GEKKO +void PSMTX44MultVec(const Mtx44 m, const Vec* src, Vec* dst); +void PSMTX44MultVecArray(const Mtx44 m, const Vec* srcBase, Vec* dstBase, u32 count); +void PSMTX44MultVecSR(const Mtx44 m, const Vec* src, Vec* dst); +void PSMTX44MultVecArraySR(const Mtx44 m, const Vec* srcBase, Vec* dstBase, u32 count); +#endif + +#ifdef MTX_USE_PS +#define MTX44MultVec PSMTX44MultVec +#define MTX44MultVecArray PSMTX44MultVecArray +#define MTX44MultVecSR PSMTX44MultVecSR +#define MTX44MultVecArraySR PSMTX44MultVecArraySR +#else +#define MTX44MultVec C_MTX44MultVec +#define MTX44MultVecArray C_MTX44MultVecArray +#define MTX44MultVecSR C_MTX44MultVecSR +#define MTX44MultVecArraySR C_MTX44MultVecArraySR +#endif + +#ifdef __cplusplus +} +#endif + +#endif // _DOLPHIN_MTX44EXT diff --git a/aurora-main/include/dolphin/os.h b/aurora-main/include/dolphin/os.h new file mode 100644 index 0000000..18f3637 --- /dev/null +++ b/aurora-main/include/dolphin/os.h @@ -0,0 +1,311 @@ +#ifndef _DOLPHIN_OS_H_ +#define _DOLPHIN_OS_H_ + +#ifdef __cplusplus +#include +#else +#include +#endif + +#include +#include + +void OSReportInit(void); +void OSSwitchFiberEx(u32, u32, u32, u32, u32, u32); +void OSVAttention(const char* fmt, va_list args); + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __MWERKS__ +#include +#endif +#include +#include +#include +#include + +// private macro, maybe shouldn't be defined here? +#define OFFSET(addr, align) (((u32)(addr) & ((align)-1))) + +#ifndef __MWERKS__ +typedef struct { + BOOL valid; + u32 restartCode; + u32 bootDol; + void* regionStart; + void* regionEnd; + int argsUseDefault; + void* argsAddr; +} OSExecParams; +#endif + +#define DOLPHIN_ALIGNMENT 32 + +// Upper words of the masks, since UIMM is only 16 bits +#define OS_CACHED_REGION_PREFIX 0x8000 +#define OS_UNCACHED_REGION_PREFIX 0xC000 +#define OS_PHYSICAL_MASK 0x3FFF + +extern uintptr_t OSBaseAddress; + +#define OS_BASE_CACHED (OSBaseAddress) +#define OS_BASE_UNCACHED (OSBaseAddress) + +#ifdef __MWERKS__ +u32 __OSPhysicalMemSize AT_ADDRESS(OS_BASE_CACHED | 0x0028); +volatile int __OSTVMode AT_ADDRESS(OS_BASE_CACHED | 0x00CC); +OSThreadQueue __OSActiveThreadQueue AT_ADDRESS(OS_BASE_CACHED | 0x00DC); +OSThread* __OSCurrentThread AT_ADDRESS(OS_BASE_CACHED | 0x00E4); +u32 __OSSimulatedMemSize AT_ADDRESS(OS_BASE_CACHED | 0x00F0); +u32 __OSBusClock AT_ADDRESS(OS_BASE_CACHED | 0x00F8); +u32 __OSCoreClock AT_ADDRESS(OS_BASE_CACHED | 0x00FC); +volatile u16 __OSDeviceCode AT_ADDRESS(OS_BASE_CACHED | 0x30E6); +u16 __OSWirelessPadFixMode AT_ADDRESS(OS_BASE_CACHED | 0x30E0); + +// unknowns +OSThread* __gUnkThread1 AT_ADDRESS(OS_BASE_CACHED | 0x00D8); +int __gUnknown800030C0[2] AT_ADDRESS(OS_BASE_CACHED | 0x30C0); +u8 __gUnknown800030E3 AT_ADDRESS(OS_BASE_CACHED | 0x30E3); +#else +#define __OSBusClock (*(u32 *)(OS_BASE_CACHED + 0x00F8)) +#define __OSCoreClock (*(u32 *)(OS_BASE_CACHED + 0x00FC)) +#endif + +#define OS_TIMER_CLOCK_DIVIDER 4 +#define OS_BUS_CLOCK 162000000 +#define OS_CORE_CLOCK 162000000 +#define OS_TIMER_CLOCK (OS_BUS_CLOCK/OS_TIMER_CLOCK_DIVIDER) + +#define OSTicksToSeconds(ticks) ((ticks) / (OS_TIMER_CLOCK)) +#define OSTicksToMilliseconds(ticks) ((ticks) / (OS_TIMER_CLOCK/1000)) +#define OSTicksToMicroseconds(ticks) ((ticks) * 8 / (OS_TIMER_CLOCK/125000)) +#define OSSecondsToTicks(sec) ((sec) * (OS_TIMER_CLOCK)) +#define OSMillisecondsToTicks(msec) ((msec) * (OS_TIMER_CLOCK / 1000)) +#define OSNanosecondsToTicks(nsec) (((nsec) * (OS_TIMER_CLOCK / 125000)) / 8000) +#define OSMicrosecondsToTicks(usec) (((usec) * (OS_TIMER_CLOCK / 125000)) / 8) + +u32 OSGetConsoleType(void); +void OSInit(void); +void OSRegisterVersion(const char* id); + +void* OSGetArenaHi(void); +void* OSGetArenaLo(void); +void OSSetArenaHi(void* newHi); +void OSSetArenaLo(void* newLo); +void* OSAllocFromArenaLo(u32 size, u32 align); +void* OSAllocFromArenaHi(u32 size, u32 align); + +u32 OSGetPhysicalMemSize(void); + +void __OSPSInit(void); +void __OSFPRInit(void); +u32 __OSGetDIConfig(void); + +#if 0 +void OSDefaultExceptionHandler(__OSException exception, OSContext* context); +#endif + +typedef struct OSCalendarTime { + /* 0x00 */ int sec; + /* 0x04 */ int min; + /* 0x08 */ int hour; + /* 0x0C */ int mday; + /* 0x10 */ int mon; + /* 0x14 */ int year; + /* 0x18 */ int wday; + /* 0x1C */ int yday; + /* 0x20 */ int msec; + /* 0x24 */ int usec; +} OSCalendarTime; + +#include +typedef struct OSBootInfo_s { + DVDDiskID DVDDiskID; + u32 magic; + u32 version; + u32 memorySize; + u32 consoleType; + void* arenaLo; + void* arenaHi; + void* FSTLocation; + u32 FSTMaxLength; +} OSBootInfo; + +typedef struct OSStopwatch { + char* name; + u32 hits; + OSTime total; + OSTime min; + OSTime max; + OSTime last; + BOOL running; + u32 _padding; +} OSStopwatch; + +void OSInitStopwatch(OSStopwatch* sw, char* name); +void OSStartStopwatch(OSStopwatch* sw); +void OSStopStopwatch(OSStopwatch* sw); +OSTime OSCheckStopwatch(OSStopwatch* sw); +void OSResetStopwatch(OSStopwatch* sw); +void OSDumpStopwatch(OSStopwatch* sw); + +OSTick OSGetTick(void); +OSTime OSGetTime(void); +void OSTicksToCalendarTime(OSTime ticks, OSCalendarTime* td); +OSTime OSCalendarTimeToTicks(OSCalendarTime* td); +BOOL OSEnableInterrupts(void); +BOOL OSDisableInterrupts(void); +BOOL OSRestoreInterrupts(BOOL level); + +#define OS_CONSOLE_MASK 0xF0000000 +#define OS_CONSOLE_RETAIL 0x00000000 +#define OS_CONSOLE_DEVELOPMENT 0x10000000 +#define OS_CONSOLE_TDEV 0x20000000 + +#define OS_CONSOLE_RETAIL4 0x00000004 +#define OS_CONSOLE_RETAIL3 0x00000003 +#define OS_CONSOLE_RETAIL2 0x00000002 +#define OS_CONSOLE_RETAIL1 0x00000001 +#define OS_CONSOLE_TDEVHW4 0x20000007 +#define OS_CONSOLE_TDEVHW3 0x20000006 +#define OS_CONSOLE_TDEVHW2 0x20000005 +#define OS_CONSOLE_TDEVHW1 0x20000004 +#define OS_CONSOLE_DEVHW4 0x10000007 +#define OS_CONSOLE_DEVHW3 0x10000006 +#define OS_CONSOLE_DEVHW2 0x10000005 +#define OS_CONSOLE_DEVHW1 0x10000004 +#define OS_CONSOLE_MINNOW 0x10000003 +#define OS_CONSOLE_ARTHUR 0x10000002 +#define OS_CONSOLE_PC_EMULATOR 0x10000001 +#define OS_CONSOLE_EMULATOR 0x10000000 + +#define OS_SOUND_MODE_MONO 0 +#define OS_SOUND_MODE_STEREO 1 + +u32 OSGetSoundMode(void); +void OSSetSoundMode(u32 mode); + +DECL_WEAK void OSReport(const char* msg, ...); +DECL_WEAK void OSVReport(const char* msg, va_list list); +DECL_WEAK void OSPanic NORETURN(const char* file, int line, const char* msg, ...); +void OSFatal NORETURN(GXColor fg, GXColor bg, const char* msg); + +#define OSRoundUp32B(x) (((uintptr_t)(x) + 32 - 1) & ~(32 - 1)) +#define OSRoundDown32B(x) (((uintptr_t)(x)) & ~(32 - 1)) + +void* OSPhysicalToCached(u32 paddr); +void* OSPhysicalToUncached(u32 paddr); +u32 OSCachedToPhysical(void* caddr); +u32 OSUncachedToPhysical(void* ucaddr); +void* OSCachedToUncached(void* caddr); +void* OSUncachedToCached(void* ucaddr); + +#if !DEBUG && !defined(TARGET_PC) +#define OSPhysicalToCached(paddr) ((void*) ((u32)(OS_BASE_CACHED + (u32)(paddr)))) +#define OSPhysicalToUncached(paddr) ((void*) ((u32)(OS_BASE_UNCACHED + (u32)(paddr)))) +#define OSCachedToPhysical(caddr) ((u32) ((u32)(caddr) - OS_BASE_CACHED)) +#define OSUncachedToPhysical(ucaddr) ((u32) ((u32)(ucaddr) - OS_BASE_UNCACHED)) +#define OSCachedToUncached(caddr) ((void*) ((u8*)(caddr) + (OS_BASE_UNCACHED - OS_BASE_CACHED))) +#define OSUncachedToCached(ucaddr) ((void*) ((u8*)(ucaddr) - (OS_BASE_UNCACHED - OS_BASE_CACHED))) +#endif + +// unsorted externs +extern OSTime __OSGetSystemTime(void); +DECL_WEAK extern int __OSIsGcam; +extern OSExecParams __OSRebootParams; +extern OSTime __OSStartTime; +extern int __OSInIPL; + +// helper for assert line numbers in different revisions +#ifndef SDK_REVISION +#define SDK_REVISION 0 +#endif + +#if SDK_REVISION < 1 + #define LINE(l0, l1, l2) (l0) +#elif SDK_REVISION < 2 + #define LINE(l0, l1, l2) (l1) +#else + #define LINE(l0, l1, l2) (l2) +#endif + +#if DEBUG +#if __MWERKS__ + +#define ASSERTLINE(line, cond) \ + ((cond) || (OSPanic(__FILE__, line, "Failed assertion " #cond), 0)) + +#define ASSERTMSGLINE(line, cond, msg) \ + ((cond) || (OSPanic(__FILE__, line, msg), 0)) + +// This is dumb but we dont have a Metrowerks way to do variadic macros in the macro to make this done in a not scrubby way. +#define ASSERTMSG1LINE(line, cond, msg, arg1) \ + ((cond) || (OSPanic(__FILE__, line, msg, arg1), 0)) + +#define ASSERTMSG2LINE(line, cond, msg, arg1, arg2) \ + ((cond) || (OSPanic(__FILE__, line, msg, arg1, arg2), 0)) + +#define ASSERTMSGLINEV(line, cond, ...) \ + ((cond) || (OSPanic(__FILE__, line, __VA_ARGS__), 0)) + +#else + +#define ASSERTLINE(line, cond) \ + ((cond) || (OSPanic(__FILE__, __LINE__, "Failed assertion " #cond), 0)) + +#define ASSERTMSGLINE(line, cond, msg) \ + ((cond) || (OSPanic(__FILE__, __LINE__, msg), 0)) + + // This is dumb but we dont have a Metrowerks way to do variadic macros in the macro to make this done in a not scrubby way. +#define ASSERTMSG1LINE(line, cond, msg, arg1) \ + ((cond) || (OSPanic(__FILE__, __LINE__, msg, arg1), 0)) +#define ASSERTMSG2LINE(line, cond, msg, arg1, arg2) \ + ((cond) || (OSPanic(__FILE__, __LINE__, msg, arg1, arg2), 0)) + +#define ASSERTMSGLINEV(line, cond, ...) \ + ((cond) || (OSPanic(__FILE__, __LINE__, __VA_ARGS__), 0)) + +#endif + +#else +#define ASSERTLINE(line, cond) (void)0 +#define ASSERTMSGLINE(line, cond, msg) (void)0 +#define ASSERTMSG1LINE(line, cond, msg, arg1) (void)0 +#define ASSERTMSG2LINE(line, cond, msg, arg1, arg2) (void)0 +#define ASSERTMSGLINEV(line, cond, ...) (void)0 +#endif + +#ifdef ASSERT +#undef ASSERT +#endif +#define ASSERT(cond) ASSERTLINE(__LINE__, cond) + +#ifdef __cplusplus +} +#endif +#endif diff --git a/aurora-main/include/dolphin/os/OSAlarm.h b/aurora-main/include/dolphin/os/OSAlarm.h new file mode 100644 index 0000000..3105039 --- /dev/null +++ b/aurora-main/include/dolphin/os/OSAlarm.h @@ -0,0 +1,36 @@ +#ifndef _DOLPHIN_OSALARM_H_ +#define _DOLPHIN_OSALARM_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct OSAlarm OSAlarm; +typedef void (*OSAlarmHandler)(OSAlarm* alarm, OSContext* context); +struct OSAlarm { + OSAlarmHandler handler; + u32 tag; + OSTime fire; + OSAlarm* prev; + OSAlarm* next; + OSTime period; + OSTime start; +}; + +BOOL OSCheckAlarmQueue(void); +void OSInitAlarm(void); +void OSCreateAlarm(OSAlarm* alarm); +void OSSetAlarm(OSAlarm* alarm, OSTime tick, OSAlarmHandler handler); +void OSSetAbsAlarm(OSAlarm* alarm, OSTime time, OSAlarmHandler handler); +void OSSetPeriodicAlarm(OSAlarm* alarm, OSTime start, OSTime period, OSAlarmHandler handler); +void OSCancelAlarm(OSAlarm *alarm); +void OSSetAlarmTag(OSAlarm* alarm, u32 tag); +void OSCancelAlarms(u32 tag); + +#ifdef __cplusplus +} +#endif + +#endif // _DOLPHIN_OSALARM_H_ diff --git a/aurora-main/include/dolphin/os/OSAlloc.h b/aurora-main/include/dolphin/os/OSAlloc.h new file mode 100644 index 0000000..e2cecd9 --- /dev/null +++ b/aurora-main/include/dolphin/os/OSAlloc.h @@ -0,0 +1,34 @@ +#ifndef _DOLPHIN_OSALLOC_H_ +#define _DOLPHIN_OSALLOC_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef int OSHeapHandle; + +extern volatile OSHeapHandle __OSCurrHeap; + +void* OSAllocFromHeap(OSHeapHandle heap, u32 size); +void* OSAllocFixed(void* rstart, void* rend); +void OSFreeToHeap(OSHeapHandle heap, void* ptr); +OSHeapHandle OSSetCurrentHeap(OSHeapHandle heap); +void* OSInitAlloc(void* arenaStart, void* arenaEnd, int maxHeaps); +OSHeapHandle OSCreateHeap(void* start, void* end); +void OSDestroyHeap(OSHeapHandle heap); +void OSAddToHeap(OSHeapHandle heap, void* start, void* end); +s32 OSCheckHeap(OSHeapHandle heap); +u32 OSReferentSize(void* ptr); +void OSDumpHeap(OSHeapHandle heap); +void OSVisitAllocated(void (*visitor)(void*, u32)); + +#define OSAlloc(size) OSAllocFromHeap(__OSCurrHeap, (size)) +#define OSFree(ptr) OSFreeToHeap(__OSCurrHeap, (ptr)) + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/os/OSCache.h b/aurora-main/include/dolphin/os/OSCache.h new file mode 100644 index 0000000..89e7ce9 --- /dev/null +++ b/aurora-main/include/dolphin/os/OSCache.h @@ -0,0 +1,38 @@ +#ifndef _DOLPHIN_OSCACHE_H_ +#define _DOLPHIN_OSCACHE_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void DCInvalidateRange(void* addr, u32 nBytes); +void DCFlushRange(void* addr, u32 nBytes); +void DCStoreRange(void* addr, u32 nBytes); +void DCFlushRangeNoSync(void* addr, u32 nBytes); +void DCStoreRangeNoSync(void* addr, u32 nBytes); +void DCZeroRange(void* addr, u32 nBytes); +void DCTouchRange(void* addr, u32 nBytes); +void ICInvalidateRange(void* addr, u32 nBytes); + +#define LC_BASE_PREFIX 0xE000 +#define LC_BASE (LC_BASE_PREFIX << 16) +#define LCGetBase() ((void*)LC_BASE) + +void LCEnable(void); +void LCDisable(void); +void LCLoadBlocks(void* destTag, void* srcAddr, u32 numBlocks); +void LCStoreBlocks(void* destAddr, void* srcTag, u32 numBlocks); +u32 LCLoadData(void* destAddr, void* srcAddr, u32 nBytes); +u32 LCStoreData(void* destAddr, void* srcAddr, u32 nBytes); +u32 LCQueueLength(void); +void LCQueueWait(u32 len); +void LCFlushQueue(void); +void __OSCacheInit(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/os/OSContext.h b/aurora-main/include/dolphin/os/OSContext.h new file mode 100644 index 0000000..bf06efc --- /dev/null +++ b/aurora-main/include/dolphin/os/OSContext.h @@ -0,0 +1,177 @@ +#ifndef _DOLPHIN_OSCONTEXT_H_ +#define _DOLPHIN_OSCONTEXT_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define __OS_CONTEXT_FRAME 768 + +#define OS_CONTEXT_R0 0 +#define OS_CONTEXT_R1 4 +#define OS_CONTEXT_R2 8 +#define OS_CONTEXT_R3 12 +#define OS_CONTEXT_R4 16 +#define OS_CONTEXT_R5 20 +#define OS_CONTEXT_R6 24 +#define OS_CONTEXT_R7 28 +#define OS_CONTEXT_R8 32 +#define OS_CONTEXT_R9 36 +#define OS_CONTEXT_R10 40 +#define OS_CONTEXT_R11 44 +#define OS_CONTEXT_R12 48 +#define OS_CONTEXT_R13 52 +#define OS_CONTEXT_R14 56 +#define OS_CONTEXT_R15 60 +#define OS_CONTEXT_R16 64 +#define OS_CONTEXT_R17 68 +#define OS_CONTEXT_R18 72 +#define OS_CONTEXT_R19 76 +#define OS_CONTEXT_R20 80 +#define OS_CONTEXT_R21 84 +#define OS_CONTEXT_R22 88 +#define OS_CONTEXT_R23 92 +#define OS_CONTEXT_R24 96 +#define OS_CONTEXT_R25 100 +#define OS_CONTEXT_R26 104 +#define OS_CONTEXT_R27 108 +#define OS_CONTEXT_R28 112 +#define OS_CONTEXT_R29 116 +#define OS_CONTEXT_R30 120 +#define OS_CONTEXT_R31 124 + +#define OS_CONTEXT_CR 128 +#define OS_CONTEXT_LR 132 +#define OS_CONTEXT_CTR 136 +#define OS_CONTEXT_XER 140 + +#define OS_CONTEXT_FPR0 144 +#define OS_CONTEXT_FPR1 152 +#define OS_CONTEXT_FPR2 160 +#define OS_CONTEXT_FPR3 168 +#define OS_CONTEXT_FPR4 176 +#define OS_CONTEXT_FPR5 184 +#define OS_CONTEXT_FPR6 192 +#define OS_CONTEXT_FPR7 200 +#define OS_CONTEXT_FPR8 208 +#define OS_CONTEXT_FPR9 216 +#define OS_CONTEXT_FPR10 224 +#define OS_CONTEXT_FPR11 232 +#define OS_CONTEXT_FPR12 240 +#define OS_CONTEXT_FPR13 248 +#define OS_CONTEXT_FPR14 256 +#define OS_CONTEXT_FPR15 264 +#define OS_CONTEXT_FPR16 272 +#define OS_CONTEXT_FPR17 280 +#define OS_CONTEXT_FPR18 288 +#define OS_CONTEXT_FPR19 296 +#define OS_CONTEXT_FPR20 304 +#define OS_CONTEXT_FPR21 312 +#define OS_CONTEXT_FPR22 320 +#define OS_CONTEXT_FPR23 328 +#define OS_CONTEXT_FPR24 336 +#define OS_CONTEXT_FPR25 344 +#define OS_CONTEXT_FPR26 352 +#define OS_CONTEXT_FPR27 360 +#define OS_CONTEXT_FPR28 368 +#define OS_CONTEXT_FPR29 376 +#define OS_CONTEXT_FPR30 384 +#define OS_CONTEXT_FPR31 392 + +#define OS_CONTEXT_FPSCR 400 + +#define OS_CONTEXT_SRR0 408 +#define OS_CONTEXT_SRR1 412 + +#define OS_CONTEXT_MODE 416 +#define OS_CONTEXT_STATE 418 + +#define OS_CONTEXT_GQR0 420 +#define OS_CONTEXT_GQR1 424 +#define OS_CONTEXT_GQR2 428 +#define OS_CONTEXT_GQR3 432 +#define OS_CONTEXT_GQR4 436 +#define OS_CONTEXT_GQR5 440 +#define OS_CONTEXT_GQR6 444 +#define OS_CONTEXT_GQR7 448 +#define __OSCONTEXT_PADDING 452 + +#define OS_CONTEXT_PSF0 456 +#define OS_CONTEXT_PSF1 464 +#define OS_CONTEXT_PSF2 472 +#define OS_CONTEXT_PSF3 480 +#define OS_CONTEXT_PSF4 488 +#define OS_CONTEXT_PSF5 496 +#define OS_CONTEXT_PSF6 504 +#define OS_CONTEXT_PSF7 512 +#define OS_CONTEXT_PSF8 520 +#define OS_CONTEXT_PSF9 528 +#define OS_CONTEXT_PSF10 536 +#define OS_CONTEXT_PSF11 544 +#define OS_CONTEXT_PSF12 552 +#define OS_CONTEXT_PSF13 560 +#define OS_CONTEXT_PSF14 568 +#define OS_CONTEXT_PSF15 576 +#define OS_CONTEXT_PSF16 584 +#define OS_CONTEXT_PSF17 592 +#define OS_CONTEXT_PSF18 600 +#define OS_CONTEXT_PSF19 608 +#define OS_CONTEXT_PSF20 616 +#define OS_CONTEXT_PSF21 624 +#define OS_CONTEXT_PSF22 632 +#define OS_CONTEXT_PSF23 640 +#define OS_CONTEXT_PSF24 648 +#define OS_CONTEXT_PSF25 656 +#define OS_CONTEXT_PSF26 664 +#define OS_CONTEXT_PSF27 672 +#define OS_CONTEXT_PSF28 680 +#define OS_CONTEXT_PSF29 688 +#define OS_CONTEXT_PSF30 696 +#define OS_CONTEXT_PSF31 704 +#define OS_CONTEXT_STATE_EXC 0x02u + +#define OS_CONTEXT_STATE_FPSAVED 0x01u + +typedef struct OSContext { +#ifndef TARGET_PC + /* 0x000 */ u32 gpr[32]; + /* 0x080 */ u32 cr; + /* 0x084 */ u32 lr; + /* 0x088 */ u32 ctr; + /* 0x08C */ u32 xer; + /* 0x090 */ f64 fpr[32]; + /* 0x190 */ u32 fpscr_pad; + /* 0x194 */ u32 fpscr; + /* 0x198 */ u32 srr0; + /* 0x19C */ u32 srr1; + /* 0x1A0 */ u16 mode; + /* 0x1A2 */ u16 state; + /* 0x1A4 */ u32 gqr[8]; + /* 0x1C4 */ u32 psf_pad; + /* 0x1C8 */ f64 psf[32]; +#else + char storage[0x2C8]; +#endif +} OSContext; + +u32 OSGetStackPointer(void); +void OSDumpContext(OSContext* context); +void OSLoadContext(OSContext* context); +u32 OSSaveContext(OSContext* context); +void OSClearContext(OSContext* context); +OSContext* OSGetCurrentContext(void); +void OSSetCurrentContext(OSContext* context); +void OSLoadFPUContext(OSContext* fpucontext); +void OSSaveFPUContext(OSContext* fpucontext); +u32 OSSwitchStack(u32 newsp); +int OSSwitchFiber(u32 pc, u32 newsp); +void OSInitContext(OSContext* context, u32 pc, u32 newsp); +void OSFillFPUContext(OSContext* context); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/os/OSDC.h b/aurora-main/include/dolphin/os/OSDC.h new file mode 100644 index 0000000..bab2d68 --- /dev/null +++ b/aurora-main/include/dolphin/os/OSDC.h @@ -0,0 +1,23 @@ +#ifndef _DOLPHIN_OSDC_H_ +#define _DOLPHIN_OSDC_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +void DCFlashInvalidate(void); +void DCEnable(void); +void DCDisable(void); +void DCFreeze(void); +void DCUnfreeze(void); +void DCTouchLoad(void* addr); +void DCBlockZero(void* addr); +void DCBlockStore(void* addr); +void DCBlockFlush(void* addr); +void DCBlockInvalidate(void* addr); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/os/OSError.h b/aurora-main/include/dolphin/os/OSError.h new file mode 100644 index 0000000..32278eb --- /dev/null +++ b/aurora-main/include/dolphin/os/OSError.h @@ -0,0 +1,39 @@ +#ifndef _DOLPHIN_OSERROR_H_ +#define _DOLPHIN_OSERROR_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef u16 OSError; +typedef void (*OSErrorHandler)(OSError error, OSContext* context, ...); + +#define OS_ERROR_SYSTEM_RESET 0 +#define OS_ERROR_MACHINE_CHECK 1 +#define OS_ERROR_DSI 2 +#define OS_ERROR_ISI 3 +#define OS_ERROR_EXTERNAL_INTERRUPT 4 +#define OS_ERROR_ALIGNMENT 5 +#define OS_ERROR_PROGRAM 6 +#define OS_ERROR_FLOATING_POINT 7 +#define OS_ERROR_DECREMENTER 8 +#define OS_ERROR_SYSTEM_CALL 9 +#define OS_ERROR_TRACE 10 +#define OS_ERROR_PERFORMACE_MONITOR 11 +#define OS_ERROR_BREAKPOINT 12 +#define OS_ERROR_SYSTEM_INTERRUPT 13 +#define OS_ERROR_THERMAL_INTERRUPT 14 +#define OS_ERROR_PROTECTION 15 +#define OS_ERROR_MAX (OS_ERROR_PROTECTION + 1) + +OSErrorHandler OSSetErrorHandler(OSError error, OSErrorHandler handler); +extern u32 __OSFpscrEnableBits; +extern OSErrorHandler __OSErrorTable[17]; + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/os/OSException.h b/aurora-main/include/dolphin/os/OSException.h new file mode 100644 index 0000000..e9a2f22 --- /dev/null +++ b/aurora-main/include/dolphin/os/OSException.h @@ -0,0 +1,61 @@ +#ifndef _DOLPHIN_OSEXCEPTION_H_ +#define _DOLPHIN_OSEXCEPTION_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define __OS_EXCEPTION_SYSTEM_RESET 0 +#define __OS_EXCEPTION_MACHINE_CHECK 1 +#define __OS_EXCEPTION_DSI 2 +#define __OS_EXCEPTION_ISI 3 +#define __OS_EXCEPTION_EXTERNAL_INTERRUPT 4 +#define __OS_EXCEPTION_ALIGNMENT 5 +#define __OS_EXCEPTION_PROGRAM 6 +#define __OS_EXCEPTION_FLOATING_POINT 7 +#define __OS_EXCEPTION_DECREMENTER 8 +#define __OS_EXCEPTION_SYSTEM_CALL 9 +#define __OS_EXCEPTION_TRACE 10 +#define __OS_EXCEPTION_PERFORMACE_MONITOR 11 +#define __OS_EXCEPTION_BREAKPOINT 12 +#define __OS_EXCEPTION_SYSTEM_INTERRUPT 13 +#define __OS_EXCEPTION_THERMAL_INTERRUPT 14 +#define __OS_EXCEPTION_MEMORY_PROTECTION 15 +#define __OS_EXCEPTION_FLOATING_POINT_EXCEPTION 16 +#define __OS_EXCEPTION_MAX \ + (__OS_EXCEPTION_THERMAL_INTERRUPT+1) + +typedef u8 __OSException; +typedef void (*__OSExceptionHandler)(__OSException exception, OSContext* context); + +__OSExceptionHandler __OSSetExceptionHandler(__OSException exception, __OSExceptionHandler handler); +__OSExceptionHandler __OSGetExceptionHandler(__OSException exception); + +#define OS_EXCEPTION_SAVE_GPRS(context) \ + stw r0, OS_CONTEXT_R0(context); \ + stw r1, OS_CONTEXT_R1(context); \ + stw r2, OS_CONTEXT_R2(context); \ + stmw r6, OS_CONTEXT_R6(context); \ + mfspr r0, GQR1; \ + stw r0, OS_CONTEXT_GQR1(context); \ + mfspr r0, GQR2; \ + stw r0, OS_CONTEXT_GQR2(context); \ + mfspr r0, GQR3; \ + stw r0, OS_CONTEXT_GQR3(context); \ + mfspr r0, GQR4; \ + stw r0, OS_CONTEXT_GQR4(context); \ + mfspr r0, GQR5; \ + stw r0, OS_CONTEXT_GQR5(context); \ + mfspr r0, GQR6; \ + stw r0, OS_CONTEXT_GQR6(context); \ + mfspr r0, GQR7; \ + stw r0, OS_CONTEXT_GQR7(context); + +#ifdef __cplusplus +} +#endif + +#endif // _DOLPHIN_OSEXCEPTION_H_ diff --git a/aurora-main/include/dolphin/os/OSExec.h b/aurora-main/include/dolphin/os/OSExec.h new file mode 100644 index 0000000..af0dc4d --- /dev/null +++ b/aurora-main/include/dolphin/os/OSExec.h @@ -0,0 +1,35 @@ +#ifndef _DOLPHIN_OSEXEC_H_ +#define _DOLPHIN_OSEXEC_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + BOOL valid; + u32 restartCode; + u32 bootDol; + void* regionStart; + void* regionEnd; + int argsUseDefault; + void* argsAddr; +} OSExecParams; + +typedef int (*appGetNextCallback)(void*, u32*, u32*); +typedef void (*appInitCallback)(void (*)(char*)); +typedef void* (*appGetEntryCallback)(); +typedef void (*AppLoaderCallback)(appInitCallback*, appGetNextCallback*, appGetEntryCallback*); + +OSExecParams* __OSExecParams AT_ADDRESS(0x800030F0); +s32 __OSAppLoaderOffset AT_ADDRESS(0x800030F4); + +void OSExecv(const char* dolfile, const char** argv); +void OSExecl(const char* dolfile, const char* arg0, ...); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/os/OSFastCast.h b/aurora-main/include/dolphin/os/OSFastCast.h new file mode 100644 index 0000000..6c57394 --- /dev/null +++ b/aurora-main/include/dolphin/os/OSFastCast.h @@ -0,0 +1,40 @@ +#ifndef _DOLPHIN_OSFASTCAST_H_ +#define _DOLPHIN_OSFASTCAST_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +static inline void OSInitFastCast(void) {} + +static inline s16 __OSf32tos16(f32 inF) { return (s16) inF; } +static inline void OSf32tos16(f32 *f, s16 *out) { *out = __OSf32tos16(*f); } + +static inline u8 __OSf32tou8(f32 inF) { return (u8)inF; } +static inline void OSf32tou8(f32 *f, u8 *out) { *out = __OSf32tou8(*f); } + +static inline s8 __OSf32tos8(f32 inF) { return (s8) inF; } +static inline void OSf32tos8(f32 *f, s8 *out) { *out = __OSf32tos8(*f); } + +static inline u16 __OSf32tou16(f32 inF) { return (u16) inF; } +static inline void OSf32tou16(f32 *f, u16 *out) { *out = __OSf32tou16(*f); } + +static inline f32 __OSs8tof32(const s8* arg) { return (f32)*arg; } +static inline void OSs8tof32(const s8* in, f32* out) { *out = __OSs8tof32(in); } + +static inline f32 __OSs16tof32(const s16* arg) { return (f32)*arg; } +static inline void OSs16tof32(const s16* in, f32* out) { *out = __OSs16tof32(in); } + +static inline f32 __OSu8tof32(const u8* arg) { return (f32)*arg; } +static inline void OSu8tof32(const u8* in, f32* out) { *out = __OSu8tof32(in); } + +static inline f32 __OSu16tof32(const u16* arg) { return (f32)*arg; } +static inline void OSu16tof32(const u16* in, f32* out) { *out = __OSu16tof32(in); } + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/os/OSFont.h b/aurora-main/include/dolphin/os/OSFont.h new file mode 100644 index 0000000..1fff41a --- /dev/null +++ b/aurora-main/include/dolphin/os/OSFont.h @@ -0,0 +1,58 @@ +#ifndef _DOLPHIN_OSFONT_H_ +#define _DOLPHIN_OSFONT_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define OS_FONT_ENCODE_ANSI 0u +#define OS_FONT_ENCODE_SJIS 1u +#define OS_FONT_ENCODE_MAX 5u +#define OS_FONT_SIZE_ANSI (288 + 131072) // 9 sheets +#define OS_FONT_SIZE_SJIS (3840 + 1179648) // 1 sheet +#define OS_FONT_ROM_SIZE_ANSI 0x03000 +#define OS_FONT_ROM_SIZE_SJIS 0x4D000 + +typedef struct OSFontHeader { + u16 fontType; + u16 firstChar; + u16 lastChar; + u16 invalChar; + u16 ascent; + u16 descent; + u16 width; + u16 leading; + u16 cellWidth; + u16 cellHeight; + u32 sheetSize; + u16 sheetFormat; + u16 sheetColumn; + u16 sheetRow; + u16 sheetWidth; + u16 sheetHeight; + u16 widthTable; + u32 sheetImage; + u32 sheetFullSize; + u8 c0; + u8 c1; + u8 c2; + u8 c3; +} OSFontHeader; + +u16 OSGetFontEncode(void); +u16 OSSetFontEncode(u16 encode); +BOOL OSInitFont(OSFontHeader* fontData); +u32 OSLoadFont(OSFontHeader* fontData, void* tmp); +char* OSGetFontTexture(const char* string, void** image, s32* x, s32* y, s32* width); +char* OSGetFontWidth(const char* string, s32* width); +char* OSGetFontTexel(const char* string, void* image, s32 pos, s32 stride, s32* width); +int OSSetFontWidth(int fixed); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/os/OSIC.h b/aurora-main/include/dolphin/os/OSIC.h new file mode 100644 index 0000000..3309d3a --- /dev/null +++ b/aurora-main/include/dolphin/os/OSIC.h @@ -0,0 +1,20 @@ +#ifndef _DOLPHIN_OSIC_H_ +#define _DOLPHIN_OSIC_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +void ICFlashInvalidate(void); +void ICEnable(void); +void ICDisable(void); +void ICFreeze(void); +void ICUnfreeze(void); +void ICBlockInvalidate(void *addr); +void ICSync(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/os/OSInterrupt.h b/aurora-main/include/dolphin/os/OSInterrupt.h new file mode 100644 index 0000000..8fa27d2 --- /dev/null +++ b/aurora-main/include/dolphin/os/OSInterrupt.h @@ -0,0 +1,119 @@ +#ifndef _DOLPHIN_OSINTERRUPT_H_ +#define _DOLPHIN_OSINTERRUPT_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef s16 __OSInterrupt; +typedef u32 OSInterruptMask; + +#define __OS_INTERRUPT_MEM_0 0 +#define __OS_INTERRUPT_MEM_1 1 +#define __OS_INTERRUPT_MEM_2 2 +#define __OS_INTERRUPT_MEM_3 3 +#define __OS_INTERRUPT_MEM_ADDRESS 4 +#define __OS_INTERRUPT_DSP_AI 5 +#define __OS_INTERRUPT_DSP_ARAM 6 +#define __OS_INTERRUPT_DSP_DSP 7 +#define __OS_INTERRUPT_AI_AI 8 +#define __OS_INTERRUPT_EXI_0_EXI 9 +#define __OS_INTERRUPT_EXI_0_TC 10 +#define __OS_INTERRUPT_EXI_0_EXT 11 +#define __OS_INTERRUPT_EXI_1_EXI 12 +#define __OS_INTERRUPT_EXI_1_TC 13 +#define __OS_INTERRUPT_EXI_1_EXT 14 +#define __OS_INTERRUPT_EXI_2_EXI 15 +#define __OS_INTERRUPT_EXI_2_TC 16 +#define __OS_INTERRUPT_PI_CP 17 +#define __OS_INTERRUPT_PI_PE_TOKEN 18 +#define __OS_INTERRUPT_PI_PE_FINISH 19 +#define __OS_INTERRUPT_PI_SI 20 +#define __OS_INTERRUPT_PI_DI 21 +#define __OS_INTERRUPT_PI_RSW 22 +#define __OS_INTERRUPT_PI_ERROR 23 +#define __OS_INTERRUPT_PI_VI 24 +#define __OS_INTERRUPT_PI_DEBUG 25 +#define __OS_INTERRUPT_PI_HSP 26 +#define __OS_INTERRUPT_MAX 32 + +#define OS_INTERRUPTMASK(interrupt) (0x80000000u >> (interrupt)) + +#define OS_INTERRUPTMASK_MEM_0 OS_INTERRUPTMASK(__OS_INTERRUPT_MEM_0) +#define OS_INTERRUPTMASK_MEM_1 OS_INTERRUPTMASK(__OS_INTERRUPT_MEM_1) +#define OS_INTERRUPTMASK_MEM_2 OS_INTERRUPTMASK(__OS_INTERRUPT_MEM_2) +#define OS_INTERRUPTMASK_MEM_3 OS_INTERRUPTMASK(__OS_INTERRUPT_MEM_3) +#define OS_INTERRUPTMASK_MEM_ADDRESS OS_INTERRUPTMASK(__OS_INTERRUPT_MEM_ADDRESS) +#define OS_INTERRUPTMASK_MEM_RESET \ + (OS_INTERRUPTMASK_MEM_0 | OS_INTERRUPTMASK_MEM_1 | OS_INTERRUPTMASK_MEM_2 | \ + OS_INTERRUPTMASK_MEM_3) +#define OS_INTERRUPTMASK_MEM \ + (OS_INTERRUPTMASK_MEM_0 | OS_INTERRUPTMASK_MEM_1 | OS_INTERRUPTMASK_MEM_2 | \ + OS_INTERRUPTMASK_MEM_3 | OS_INTERRUPTMASK_MEM_ADDRESS) +#define OS_INTERRUPTMASK_DSP_AI OS_INTERRUPTMASK(__OS_INTERRUPT_DSP_AI) +#define OS_INTERRUPTMASK_DSP_ARAM OS_INTERRUPTMASK(__OS_INTERRUPT_DSP_ARAM) +#define OS_INTERRUPTMASK_DSP_DSP OS_INTERRUPTMASK(__OS_INTERRUPT_DSP_DSP) +#define OS_INTERRUPTMASK_DSP \ + (OS_INTERRUPTMASK_DSP_AI | OS_INTERRUPTMASK_DSP_ARAM | OS_INTERRUPTMASK_DSP_DSP) +#define OS_INTERRUPTMASK_AI_AI OS_INTERRUPTMASK(__OS_INTERRUPT_AI_AI) +#define OS_INTERRUPTMASK_AI (OS_INTERRUPTMASK_AI_AI) +#define OS_INTERRUPTMASK_EXI_0_EXI OS_INTERRUPTMASK(__OS_INTERRUPT_EXI_0_EXI) +#define OS_INTERRUPTMASK_EXI_0_TC OS_INTERRUPTMASK(__OS_INTERRUPT_EXI_0_TC) +#define OS_INTERRUPTMASK_EXI_0_EXT OS_INTERRUPTMASK(__OS_INTERRUPT_EXI_0_EXT) +#define OS_INTERRUPTMASK_EXI_0 \ + (OS_INTERRUPTMASK_EXI_0_EXI | OS_INTERRUPTMASK_EXI_0_TC | OS_INTERRUPTMASK_EXI_0_EXT) +#define OS_INTERRUPTMASK_EXI_1_EXI OS_INTERRUPTMASK(__OS_INTERRUPT_EXI_1_EXI) +#define OS_INTERRUPTMASK_EXI_1_TC OS_INTERRUPTMASK(__OS_INTERRUPT_EXI_1_TC) +#define OS_INTERRUPTMASK_EXI_1_EXT OS_INTERRUPTMASK(__OS_INTERRUPT_EXI_1_EXT) +#define OS_INTERRUPTMASK_EXI_1 \ + (OS_INTERRUPTMASK_EXI_1_EXI | OS_INTERRUPTMASK_EXI_1_TC | OS_INTERRUPTMASK_EXI_1_EXT) +#define OS_INTERRUPTMASK_EXI_2_EXI OS_INTERRUPTMASK(__OS_INTERRUPT_EXI_2_EXI) +#define OS_INTERRUPTMASK_EXI_2_TC OS_INTERRUPTMASK(__OS_INTERRUPT_EXI_2_TC) +#define OS_INTERRUPTMASK_EXI_2 (OS_INTERRUPTMASK_EXI_2_EXI | OS_INTERRUPTMASK_EXI_2_TC) +#define OS_INTERRUPTMASK_EXI \ + (OS_INTERRUPTMASK_EXI_0_EXI | OS_INTERRUPTMASK_EXI_0_TC | OS_INTERRUPTMASK_EXI_0_EXT | \ + OS_INTERRUPTMASK_EXI_1_EXI | OS_INTERRUPTMASK_EXI_1_TC | OS_INTERRUPTMASK_EXI_1_EXT | \ + OS_INTERRUPTMASK_EXI_2_EXI | OS_INTERRUPTMASK_EXI_2_TC) +#define OS_INTERRUPTMASK_PI_PE_TOKEN OS_INTERRUPTMASK(__OS_INTERRUPT_PI_PE_TOKEN) +#define OS_INTERRUPTMASK_PI_PE_FINISH OS_INTERRUPTMASK(__OS_INTERRUPT_PI_PE_FINISH) +#define OS_INTERRUPTMASK_PI_PE (OS_INTERRUPTMASK_PI_PE_TOKEN | OS_INTERRUPTMASK_PI_PE_FINISH) +#define OS_INTERRUPTMASK_PI_CP OS_INTERRUPTMASK(__OS_INTERRUPT_PI_CP) +#define OS_INTERRUPTMASK_PI_SI OS_INTERRUPTMASK(__OS_INTERRUPT_PI_SI) +#define OS_INTERRUPTMASK_PI_DI OS_INTERRUPTMASK(__OS_INTERRUPT_PI_DI) +#define OS_INTERRUPTMASK_PI_RSW OS_INTERRUPTMASK(__OS_INTERRUPT_PI_RSW) +#define OS_INTERRUPTMASK_PI_ERROR OS_INTERRUPTMASK(__OS_INTERRUPT_PI_ERROR) +#define OS_INTERRUPTMASK_PI_VI OS_INTERRUPTMASK(__OS_INTERRUPT_PI_VI) +#define OS_INTERRUPTMASK_PI_DEBUG OS_INTERRUPTMASK(__OS_INTERRUPT_PI_DEBUG) +#define OS_INTERRUPTMASK_PI_HSP OS_INTERRUPTMASK(__OS_INTERRUPT_PI_HSP) +#define OS_INTERRUPTMASK_PI \ + (OS_INTERRUPTMASK_PI_CP | OS_INTERRUPTMASK_PI_SI | OS_INTERRUPTMASK_PI_DI | \ + OS_INTERRUPTMASK_PI_RSW | OS_INTERRUPTMASK_PI_ERROR | OS_INTERRUPTMASK_PI_VI | \ + OS_INTERRUPTMASK_PI_PE_TOKEN | OS_INTERRUPTMASK_PI_PE_FINISH | OS_INTERRUPTMASK_PI_DEBUG | \ + OS_INTERRUPTMASK_PI_HSP) + +typedef void (*__OSInterruptHandler)(__OSInterrupt interrupt, OSContext* context); + +extern volatile __OSInterrupt __OSLastInterrupt; +extern volatile u32 __OSLastInterruptSrr0; +extern volatile OSTime __OSLastInterruptTime; + +__OSInterruptHandler __OSSetInterruptHandler(__OSInterrupt interrupt, __OSInterruptHandler handler); + +__OSInterruptHandler __OSGetInterruptHandler(__OSInterrupt interrupt); + +void __OSDispatchInterrupt(__OSException exception, OSContext* context); + +OSInterruptMask OSGetInterruptMask(void); +OSInterruptMask OSSetInterruptMask(OSInterruptMask mask); +OSInterruptMask __OSMaskInterrupts(OSInterruptMask mask); +OSInterruptMask __OSUnmaskInterrupts(OSInterruptMask mask); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/os/OSL2.h b/aurora-main/include/dolphin/os/OSL2.h new file mode 100644 index 0000000..01b4a62 --- /dev/null +++ b/aurora-main/include/dolphin/os/OSL2.h @@ -0,0 +1,20 @@ +#ifndef _DOLPHIN_OSL2_H_ +#define _DOLPHIN_OSL2_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void L2Enable(void); +void L2Disable(void); +void L2GlobalInvalidate(void); +void L2SetDataOnly(BOOL dataOnly); +void L2SetWriteThrough(BOOL writeThrough); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/os/OSLC.h b/aurora-main/include/dolphin/os/OSLC.h new file mode 100644 index 0000000..af49708 --- /dev/null +++ b/aurora-main/include/dolphin/os/OSLC.h @@ -0,0 +1,19 @@ +#ifndef _DOLPHIN_OSLC_H_ +#define _DOLPHIN_OSLC_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void LCAllocOneTag(BOOL invalidate, void *tag); +void LCAllocTags(BOOL invalidate, void *startTag, u32 numBlocks); +void LCAlloc(void *addr, u32 nBytes); +void LCAllocNoInvalidate(void *addr, u32 nBytes); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/os/OSMemory.h b/aurora-main/include/dolphin/os/OSMemory.h new file mode 100644 index 0000000..a5debeb --- /dev/null +++ b/aurora-main/include/dolphin/os/OSMemory.h @@ -0,0 +1,28 @@ +#ifndef _DOLPHIN_OSMEMORY_H_ +#define _DOLPHIN_OSMEMORY_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define OS_PROTECT_CHAN0 0 +#define OS_PROTECT_CHAN1 1 +#define OS_PROTECT_CHAN2 2 +#define OS_PROTECT_CHAN3 3 + +#define OS_PROTECT_CONTROL_NONE 0x00 +#define OS_PROTECT_CONTROL_READ 0x01 +#define OS_PROTECT_CONTROL_WRITE 0x02 +#define OS_PROTECT_CONTROL_RDWR (OS_PROTECT_CONTROL_READ | OS_PROTECT_CONTROL_WRITE) + +void OSProtectRange(u32 chan, void* addr, u32 nBytes, u32 control); +u32 OSGetPhysicalMemSize(void); +u32 OSGetConsoleSimulatedMemSize(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/os/OSMessage.h b/aurora-main/include/dolphin/os/OSMessage.h new file mode 100644 index 0000000..19743b8 --- /dev/null +++ b/aurora-main/include/dolphin/os/OSMessage.h @@ -0,0 +1,33 @@ +#ifndef _DOLPHIN_OSMESSAGE_H_ +#define _DOLPHIN_OSMESSAGE_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void* OSMessage; + +#define OS_MESSAGE_NOBLOCK 0 +#define OS_MESSAGE_BLOCK 1 + +typedef struct { + OSThreadQueue queueSend; + OSThreadQueue queueReceive; + OSMessage* msgArray; + s32 msgCount; + s32 firstIndex; + s32 usedCount; +} OSMessageQueue; + +void OSInitMessageQueue(OSMessageQueue* mq, OSMessage* msgArray, s32 msgCount); +BOOL OSSendMessage(OSMessageQueue* mq, OSMessage msg, s32 flags); +BOOL OSReceiveMessage(OSMessageQueue* mq, OSMessage* msg, s32 flags); +BOOL OSJamMessage(OSMessageQueue* mq, OSMessage msg, s32 flags); + +#ifdef __cplusplus +} +#endif + +#endif // _DOLPHIN_OSMESSAGE_H_ diff --git a/aurora-main/include/dolphin/os/OSModule.h b/aurora-main/include/dolphin/os/OSModule.h new file mode 100644 index 0000000..6e66e9b --- /dev/null +++ b/aurora-main/include/dolphin/os/OSModule.h @@ -0,0 +1,117 @@ +#ifndef _DOLPHIN_OSMODULE_H_ +#define _DOLPHIN_OSMODULE_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define OS_MODULE_VERSION 3 +typedef struct OSModuleHeader OSModuleHeader; + +typedef u32 OSModuleID; +typedef struct OSModuleQueue OSModuleQueue; +typedef struct OSModuleLink OSModuleLink; +typedef struct OSModuleInfo OSModuleInfo; +typedef struct OSSectionInfo OSSectionInfo; +typedef struct OSImportInfo OSImportInfo; +typedef struct OSRel OSRel; + +struct OSModuleQueue { + OSModuleInfo* head; + OSModuleInfo* tail; +}; + +struct OSModuleLink { + OSModuleInfo* next; + OSModuleInfo* prev; +}; + +struct OSModuleInfo { + OSModuleID id; // unique identifier for the module + OSModuleLink link; // doubly linked list of modules + u32 numSections; // # of sections + u32 sectionInfoOffset; // offset to section info table + u32 nameOffset; // offset to module name + u32 nameSize; // size of module name + u32 version; // version number +}; + +struct OSModuleHeader { + // CAUTION: info must be the 1st member + OSModuleInfo info; + + // OS_MODULE_VERSION == 1 + u32 bssSize; // total size of bss sections in bytes + u32 relOffset; + u32 impOffset; + u32 impSize; // size in bytes + u8 prologSection; // section # for prolog function + u8 epilogSection; // section # for epilog function + u8 unresolvedSection; // section # for unresolved function + u8 bssSection; // section # for bss section (set at run-time) + u32 prolog; // prolog function offset + u32 epilog; // epilog function offset + u32 unresolved; // unresolved function offset + + // OS_MODULE_VERSION == 2 +#if (2 <= OS_MODULE_VERSION) + u32 align; // module alignment constraint + u32 bssAlign; // bss alignment constraint +#endif + + // OS_MODULE_VERSION == 3 +#if (3 <= OS_MODULE_VERSION) + u32 fixSize; +#endif +}; + +#define OSGetSectionInfo(module) ((OSSectionInfo*)(((OSModuleInfo*)(module))->sectionInfoOffset)) + +struct OSSectionInfo { + u32 offset; + u32 size; +}; + +// OSSectionInfo.offset bit +#define OS_SECTIONINFO_EXEC 0x1 +#define OS_SECTIONINFO_OFFSET(offset) ((offset) & ~0x1) + +struct OSImportInfo { + OSModuleID id; // external module id + u32 offset; // offset to OSRel instructions +}; + +struct OSRel { + u16 offset; // byte offset from the previous entry + u8 type; + u8 section; + u32 addend; +}; + +#define R_DOLPHIN_NOP 201 // C9h current offset += OSRel.offset +#define R_DOLPHIN_SECTION 202 // CAh current section = OSRel.section +#define R_DOLPHIN_END 203 // CBh +#define R_DOLPHIN_MRKREF 204 // CCh + +void OSSetStringTable(void* stringTable); +BOOL OSLink(OSModuleInfo* newModule, void* bss); + +#if (3 <= OS_MODULE_VERSION) +BOOL OSLinkFixed(OSModuleInfo* newModule, void* bss); +#endif + +BOOL OSUnlink(OSModuleInfo* oldModule); + +OSModuleInfo* OSSearchModule(void* ptr, u32* section, u32* offset); + +// debugger notification +void OSNotifyLink(OSModuleInfo* module); +void OSNotifyUnlink(OSModuleInfo* module); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/os/OSMutex.h b/aurora-main/include/dolphin/os/OSMutex.h new file mode 100644 index 0000000..ef5565e --- /dev/null +++ b/aurora-main/include/dolphin/os/OSMutex.h @@ -0,0 +1,33 @@ +#ifndef _DOLPHIN_OSMUTEX_H_ +#define _DOLPHIN_OSMUTEX_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct OSMutex { + /* 0x00 */ OSThreadQueue queue; + /* 0x08 */ OSThread* thread; + /* 0x0C */ s32 count; + /* 0x10 */ OSMutexLink link; +}; + +typedef struct OSCond { + OSThreadQueue queue; +} OSCond; + +void OSInitMutex(OSMutex* mutex); +void OSLockMutex(OSMutex* mutex); +void OSUnlockMutex(OSMutex* mutex); +BOOL OSTryLockMutex(OSMutex* mutex); +void OSInitCond(OSCond* cond); +void OSWaitCond(OSCond* cond, OSMutex* mutex); +void OSSignalCond(OSCond* cond); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/os/OSReboot.h b/aurora-main/include/dolphin/os/OSReboot.h new file mode 100644 index 0000000..6509534 --- /dev/null +++ b/aurora-main/include/dolphin/os/OSReboot.h @@ -0,0 +1,19 @@ +#ifndef _DOLPHIN_OSREBOOT_H_ +#define _DOLPHIN_OSREBOOT_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void OSSetSaveRegion(void* start, void* end); +void OSGetSaveRegion(void** start, void** end); +void OSGetSavedRegion(void** start, void** end); +void __OSReboot(u32 resetCode, u32 bootDol); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/os/OSReset.h b/aurora-main/include/dolphin/os/OSReset.h new file mode 100644 index 0000000..8c6b27d --- /dev/null +++ b/aurora-main/include/dolphin/os/OSReset.h @@ -0,0 +1,44 @@ +#ifndef _DOLPHIN_OSRESET_H_ +#define _DOLPHIN_OSRESET_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define OS_RESET_RESTART 0 +#define OS_RESET_HOTRESET 1 +#define OS_RESET_SHUTDOWN 2 + +typedef struct OSResetFunctionInfo OSResetFunctionInfo; +typedef struct OSResetFunctionQueue { + OSResetFunctionInfo* head; + OSResetFunctionInfo* tail; +} OSResetFunctionQueue; + +typedef BOOL (*OSResetFunction)(BOOL); + +struct OSResetFunctionInfo { + OSResetFunction func; + u32 priority; + OSResetFunctionInfo* next; + OSResetFunctionInfo* prev; +}; + +#define OS_RESET_RESTART 0 +#define OS_RESET_HOTRESET 1 +#define OS_RESET_SHUTDOWN 2 +#define OS_RESETCODE_RESTART 0x80000000 + +void OSRegisterResetFunction(OSResetFunctionInfo* info); +void OSUnregisterResetFunction(OSResetFunctionInfo* info); +void OSResetSystem(int reset, u32 resetCode, BOOL forceMenu); +u32 OSGetResetCode(); +u32 OSSetBootDol(u32 dolOffset); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/os/OSResetSW.h b/aurora-main/include/dolphin/os/OSResetSW.h new file mode 100644 index 0000000..3c630b8 --- /dev/null +++ b/aurora-main/include/dolphin/os/OSResetSW.h @@ -0,0 +1,20 @@ +#ifndef _DOLPHIN_OSRESETSW_H_ +#define _DOLPHIN_OSRESETSW_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*OSResetCallback)(void); + +OSResetCallback OSSetResetCallback(OSResetCallback callback); +BOOL OSGetResetSwitchState(void); +BOOL OSGetResetButtonState(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/os/OSRtc.h b/aurora-main/include/dolphin/os/OSRtc.h new file mode 100644 index 0000000..583043f --- /dev/null +++ b/aurora-main/include/dolphin/os/OSRtc.h @@ -0,0 +1,84 @@ +#ifndef _DOLPHIN_OSRTC_H_ +#define _DOLPHIN_OSRTC_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// make the assert happy +#define OS_SOUND_MODE_MONO 0 +#define OS_SOUND_MODE_STEREO 1 + +// make the asserts happy +#define OS_VIDEO_MODE_NTSC 0 +#define OS_VIDEO_MODE_MPAL 2 + +#define OS_PROGRESSIVE_MODE_OFF 0 +#define OS_PROGRESSIVE_MODE_ON 1 + +#define OS_EURGB60_OFF 0 +#define OS_EURGB60_ON 1 + +#define OS_LANGUAGE_ENGLISH 0 +#define OS_LANGUAGE_GERMAN 1 +#define OS_LANGUAGE_FRENCH 2 +#define OS_LANGUAGE_SPANISH 3 +#define OS_LANGUAGE_ITALIAN 4 +#define OS_LANGUAGE_DUTCH 5 + +typedef struct OSSram { + u16 checkSum; + u16 checkSumInv; + u32 ead0; + u32 ead1; + u32 counterBias; + s8 displayOffsetH; + u8 ntd; + u8 language; + u8 flags; +} OSSram; + +typedef struct OSSramEx { + u8 flashID[2][12]; + u32 wirelessKeyboardID; + u16 wirelessPadID[4]; + u8 dvdErrorCode; + u8 _padding0; + u8 flashIDCheckSum[2]; + u16 gbs; + u8 _padding1[2]; +} OSSramEx; + +#define SRAM_SIZE (sizeof(OSSram) + sizeof(OSSramEx)) + +typedef struct SramControl { + u8 sram[SRAM_SIZE]; // dummy for OSSram + OSSramEx + u32 offset; + BOOL enabled; + BOOL locked; + int sync; + void (*callback)(); +} SramControl; + +u32 OSGetSoundMode(void); +void OSSetSoundMode(u32 mode); +u32 OSGetVideoMode(void); +void OSSetVideoMode(u32 mode); +u8 OSGetLanguage(void); +void OSSetLanguage(u8 language); +u16 OSGetGbsMode(void); +void OSSetGbsMode(u16 mode); +u32 OSGetProgressiveMode(void); +void OSSetProgressiveMode(u32 on); +u32 OSGetEuRgb60Mode(void); +void OSSetEuRgb60Mode(u32 on); +u16 OSGetWirelessID(s32 chan); +void OSSetWirelessID(s32 chan, u16 id); + +#ifdef __cplusplus +} +#endif + +#endif // _DOLPHIN_OSRTC_H_ diff --git a/aurora-main/include/dolphin/os/OSSemaphore.h b/aurora-main/include/dolphin/os/OSSemaphore.h new file mode 100644 index 0000000..aac2381 --- /dev/null +++ b/aurora-main/include/dolphin/os/OSSemaphore.h @@ -0,0 +1,25 @@ +#ifndef _DOLPHIN_OSSEMAPHORE_H_ +#define _DOLPHIN_OSSEMAPHORE_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct OSSemaphore { + s32 count; + OSThreadQueue queue; +} OSSemaphore; + +void OSInitSemaphore(OSSemaphore* sem, s32 count); +s32 OSWaitSemaphore(OSSemaphore* sem); +s32 OSTryWaitSemaphore(OSSemaphore* sem); +s32 OSSignalSemaphore(OSSemaphore* sem); +s32 OSGetSemaphoreCount(OSSemaphore* sem); + +#ifdef __cplusplus +} +#endif + +#endif // _DOLPHIN_OSSEMAPHORE_H_ diff --git a/aurora-main/include/dolphin/os/OSSerial.h b/aurora-main/include/dolphin/os/OSSerial.h new file mode 100644 index 0000000..7c38395 --- /dev/null +++ b/aurora-main/include/dolphin/os/OSSerial.h @@ -0,0 +1,6 @@ +#ifndef _DOLPHIN_OSSERIAL_H +#define _DOLPHIN_OSSERIAL_H + +#include + +#endif // _DOLPHIN_OSSERIAL_H diff --git a/aurora-main/include/dolphin/os/OSThread.h b/aurora-main/include/dolphin/os/OSThread.h new file mode 100644 index 0000000..2053ff5 --- /dev/null +++ b/aurora-main/include/dolphin/os/OSThread.h @@ -0,0 +1,113 @@ +#ifndef _DOLPHIN_OSTHREAD_H_ +#define _DOLPHIN_OSTHREAD_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef s32 OSPriority; + +typedef struct OSThread OSThread; +typedef struct OSMutex OSMutex; +typedef struct OSThreadQueue OSThreadQueue; +typedef struct OSMutexQueue OSMutexQueue; +typedef struct OSThreadLink OSThreadLink; +typedef struct OSMutexLink OSMutexLink; + +struct OSThreadQueue { + OSThread* head; + OSThread* tail; +}; + +struct OSThreadLink { + OSThread* next; + OSThread* prev; +}; + +struct OSMutexQueue { + OSMutex* head; + OSMutex* tail; +}; + +struct OSMutexLink { + OSMutex* next; + OSMutex* prev; +}; + +struct OSThread { + /* 0x000 */ OSContext context; + /* 0x2C8 */ u16 state; + /* 0x2CA */ u16 attr; + /* 0x2CC */ s32 suspend; + /* 0x2D0 */ OSPriority priority; + /* 0x2D4 */ OSPriority base; + /* 0x2D8 */ void* val; + /* 0x2DC */ OSThreadQueue* queue; + /* 0x2E0 */ OSThreadLink link; + /* 0x2E8 */ OSThreadQueue queueJoin; + /* 0x2F0 */ OSMutex* mutex; + /* 0x2F4 */ OSMutexQueue queueMutex; + /* 0x2FC */ OSThreadLink linkActive; + /* 0x304 */ u8* stackBase; + /* 0x308 */ u8* stackEnd; + /* 0x30C */ s32 error; + /* 0x310 */ void* specific[2]; +}; + +enum OS_THREAD_STATE { + OS_THREAD_STATE_READY = 1, + OS_THREAD_STATE_RUNNING = 2, + OS_THREAD_STATE_WAITING = 4, + OS_THREAD_STATE_MORIBUND = 8, +}; + +#define OS_PRIORITY_MIN 0 // highest +#define OS_PRIORITY_MAX 31 // lowest +#define OS_PRIORITY_IDLE OS_PRIORITY_MAX + +#define OS_THREAD_SPECIFIC_MAX 2 + +#define OS_THREAD_ATTR_DETACH 0x0001u + +#define OS_THREAD_STACK_MAGIC 0xDEADBABE + +typedef void (*OSSwitchThreadCallback)(OSThread*, OSThread*); +typedef void (*OSIdleFunction)(void*); + +void OSInitThreadQueue(OSThreadQueue* queue); +void OSSleepThread(OSThreadQueue* queue); +void OSWakeupThread(OSThreadQueue* queue); +s32 OSSuspendThread(OSThread* thread); +s32 OSResumeThread(OSThread* thread); +OSThread* OSGetCurrentThread(void); +s32 OSEnableScheduler(void); +s32 OSDisableScheduler(void); +void OSCancelThread(OSThread* thread); +void OSClearStack(u8 val); +BOOL OSIsThreadSuspended(OSThread* thread); +BOOL OSIsThreadTerminated(OSThread* thread); +void OSYieldThread(void); +BOOL OSCreateThread(OSThread* thread, void* (*func)(void*), void* param, void* stack, u32 stackSize, + OSPriority priority, u16 attr); +void OSExitThread(void* val); +BOOL OSJoinThread(OSThread* thread, void** val); +void OSDetachThread(OSThread* thread); +BOOL OSSetThreadPriority(OSThread* thread, OSPriority priority); +s32 OSGetThreadPriority(OSThread* thread); +OSThread* OSSetIdleFunction(OSIdleFunction idleFunction, void* param, void* stack, u32 stackSize); +OSThread* OSGetIdleFunction(void); +s32 OSCheckActiveThreads(void); +void OSSetThreadSpecific(s32 index, void* ptr); +void* OSGetThreadSpecific(s32 index); + +OSSwitchThreadCallback OSSetSwitchThreadCallback(OSSwitchThreadCallback callback); + +#define IsSuspended(suspend) (suspend > 0) + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/os/OSTime.h b/aurora-main/include/dolphin/os/OSTime.h new file mode 100644 index 0000000..2dc1cf2 --- /dev/null +++ b/aurora-main/include/dolphin/os/OSTime.h @@ -0,0 +1,49 @@ +#ifndef _DOLPHIN_OSTIME_H_ +#define _DOLPHIN_OSTIME_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef s64 OSTime; +typedef u32 OSTick; + +#define OSDiffTick(tick1, tick0) ((s32)(tick1) - (s32)(tick0)) + +// Time base frequency = 1/4 bus clock +#define OS_TIME_SPEED (OS_BUS_CLOCK / 4) + +// OS time -> Real time +#define OS_TICKS_TO_SEC(x) ((x) / (OS_TIME_SPEED)) +#define OS_TICKS_TO_MSEC(x) ((x) / (OS_TIME_SPEED / 1000)) +#define OS_TICKS_TO_USEC(x) (((x) * 8) / (OS_TIME_SPEED / 125000)) +#define OS_TICKS_TO_NSEC(x) (((x) * 8000) / (OS_TIME_SPEED / 125000)) + +// Real time -> OS time +#define OS_SEC_TO_TICKS(x) ((x) * (OS_TIME_SPEED)) +#define OS_MSEC_TO_TICKS(x) ((x) * (OS_TIME_SPEED / 1000)) +#define OS_USEC_TO_TICKS(x) ((x) * (OS_TIME_SPEED / 125000) / 8) +#define OS_NSEC_TO_TICKS(x) ((x) * (OS_TIME_SPEED / 125000) / 8000) + +#define USEC_MAX 1000 +#define MSEC_MAX 1000 +#define MONTH_MAX 12 +#define WEEK_DAY_MAX 7 +#define YEAR_DAY_MAX 365 + +#define SECS_IN_MIN 60 +#define SECS_IN_HOUR (SECS_IN_MIN * 60) +#define SECS_IN_DAY (SECS_IN_HOUR * 24) +#define SECS_IN_YEAR (SECS_IN_DAY * 365) + +#define BIAS 0xB2575 + +#define __OSSystemTime (OSTime*)0x800030D8 + +#ifdef __cplusplus +} +#endif + +#endif // _DOLPHIN_OSTIME_H_ diff --git a/aurora-main/include/dolphin/os/OSTimer.h b/aurora-main/include/dolphin/os/OSTimer.h new file mode 100644 index 0000000..490cccb --- /dev/null +++ b/aurora-main/include/dolphin/os/OSTimer.h @@ -0,0 +1,21 @@ +#ifndef _DOLPHIN_OSTIMER_H_ +#define _DOLPHIN_OSTIMER_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*OSTimerCallback)(void); + +OSTimerCallback OSSetTimerCallback(OSTimerCallback callback); +void OSInitTimer(u32 time, u32 mode); +void OSStartTimer(void); +void OSStopTimer(void); + +#ifdef __cplusplus +} +#endif + +#endif // _DOLPHIN_OSTIMER_H_ diff --git a/aurora-main/include/dolphin/os/OSUtf.h b/aurora-main/include/dolphin/os/OSUtf.h new file mode 100644 index 0000000..2f721d5 --- /dev/null +++ b/aurora-main/include/dolphin/os/OSUtf.h @@ -0,0 +1,23 @@ +#ifndef _DOLPHIN_OSUTF_H_ +#define _DOLPHIN_OSUTF_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +char* OSUTF8to32(const char* utf8, u32* utf32); +char* OSUTF32to8(u32 utf32, char* utf8); +u16* OSUTF16to32(const u16* utf16, u32* utf32); +u16* OSUTF32to16(u32 utf32, u16* utf16); +u8 OSUTF32toANSI(u32 utf32); +u32 OSANSItoUTF32(u8 ansi); +u16 OSUTF32toSJIS(u32 utf32); +u32 OSSJIStoUTF32(u16 sjis); + +#ifdef __cplusplus +} +#endif + +#endif // _DOLPHIN_OSUTF_H_ diff --git a/aurora-main/include/dolphin/pad.h b/aurora-main/include/dolphin/pad.h new file mode 100644 index 0000000..6f821c3 --- /dev/null +++ b/aurora-main/include/dolphin/pad.h @@ -0,0 +1,300 @@ +#ifndef DOLPHIN_PAD_H +#define DOLPHIN_PAD_H + +#include + +#define PAD_SPEC_0 0 +#define PAD_SPEC_1 1 +#define PAD_SPEC_2 2 +#define PAD_SPEC_3 3 +#define PAD_SPEC_4 4 +#define PAD_SPEC_5 5 + +#define PAD_CHAN0 0 +#define PAD_CHAN1 1 +#define PAD_CHAN2 2 +#define PAD_CHAN3 3 +#define PAD_CHANMAX 4 + +#define PAD_MOTOR_STOP 0 +#define PAD_MOTOR_RUMBLE 1 +#define PAD_MOTOR_STOP_HARD 2 + +#define PAD_ERR_NONE 0 +#define PAD_ERR_NO_CONTROLLER -1 +#define PAD_ERR_NOT_READY -2 +#define PAD_ERR_TRANSFER -3 + +#define PAD_MAX_CONTROLLERS 4 + +#define PAD_BUTTON_LEFT 0x0001 +#define PAD_BUTTON_RIGHT 0x0002 +#define PAD_BUTTON_DOWN 0x0004 +#define PAD_BUTTON_UP 0x0008 +#define PAD_TRIGGER_Z 0x0010 +#define PAD_TRIGGER_R 0x0020 +#define PAD_TRIGGER_L 0x0040 +#define PAD_BUTTON_A 0x0100 +#define PAD_BUTTON_B 0x0200 +#define PAD_BUTTON_X 0x0400 +#define PAD_BUTTON_Y 0x0800 +#define PAD_BUTTON_MENU 0x1000 +#define PAD_BUTTON_START 0x1000 +#ifdef TARGET_PC +#define PAD_BUTTON_BACK 0x0002000 +#define PAD_BUTTON_GUIDE 0x0004000 +#define PAD_BUTTON_MISC1 0x0008000 +#define PAD_BUTTON_MISC2 0x0010000 +#define PAD_BUTTON_MISC3 0x0020000 +#define PAD_BUTTON_MISC4 0x0040000 +#define PAD_BUTTON_MISC5 0x0080000 +#define PAD_BUTTON_MISC6 0x0100000 +#define PAD_BUTTON_RIGHT_PADDLE1 0x0200000 +#define PAD_BUTTON_LEFT_PADDLE1 0x0400000 +#define PAD_BUTTON_RIGHT_PADDLE2 0x0800000 +#define PAD_BUTTON_LEFT_PADDLE2 0x1000000 +#define PAD_BUTTON_RIGHT_STICK 0x2000000 +#define PAD_BUTTON_LEFT_STICK 0x4000000 +#define PAD_BUTTON_TOUCHPAD 0x8000000 +#define PAD_EXT_BUTTON_COUNT 15 +#endif + +#define PAD_BUTTON_COUNT 12 + +// added by Aurora, not present in original SDK +#define PAD_AXIS_LEFT_X_POS 0 +#define PAD_AXIS_LEFT_X_NEG 1 +#define PAD_AXIS_LEFT_Y_POS 2 +#define PAD_AXIS_LEFT_Y_NEG 3 +#define PAD_AXIS_RIGHT_X_POS 4 +#define PAD_AXIS_RIGHT_X_NEG 5 +#define PAD_AXIS_RIGHT_Y_POS 6 +#define PAD_AXIS_RIGHT_Y_NEG 7 +#define PAD_AXIS_TRIGGER_L 8 +#define PAD_AXIS_TRIGGER_R 9 + +#define PAD_AXIS_COUNT 10 + +#define PAD_CHAN0_BIT 0x80000000 +#define PAD_CHAN1_BIT 0x40000000 +#define PAD_CHAN2_BIT 0x20000000 +#define PAD_CHAN3_BIT 0x10000000 + +#define PADButtonDown(buttonLast, button) (((buttonLast) ^ (button)) & (button)) +#define PADButtonUp(buttonLast, button) (((buttonLast) ^ (button)) & (buttonLast)) + +#ifdef __cplusplus +extern "C" { +#endif + +#define PAD_NATIVE_BUTTON_INVALID 0xFFFFFFFF + +typedef struct PADStatus { + u16 button; + s8 stickX; + s8 stickY; + s8 substickX; + s8 substickY; + union { + u8 triggerLeft; + u8 triggerL; + }; + union { + u8 triggerRight; + u8 triggerR; + }; + u8 analogA; + u8 analogB; + s8 err; +#ifdef TARGET_PC + u32 extButton; +#endif +} PADStatus; + +typedef enum PADAxisSign { + AXIS_SIGN_NEGATIVE = -1, + AXIS_SIGN_POSITIVE = 1, +} PADAxisSign; + +typedef struct PADSignedNativeAxis { + int32_t nativeAxis; + PADAxisSign sign; +} PADSignedNativeAxis; + +struct SDL_Gamepad; + +BOOL PADInit(); +u32 PADRead(PADStatus* status); +BOOL PADReset(u32 mask); +BOOL PADRecalibrate(u32 mask); +void PADClamp(PADStatus* status); +void PADClampCircle(PADStatus* status); +void PADControlMotor(u32 chan, u32 cmd); +void PADSetSpec(u32 spec); +void PADControlAllMotors(const u32* cmdArr); +void PADSetAnalogMode(u32 mode); + +#ifdef TARGET_PC +#define PAD_KEY_INVALID (-1) +#define PAD_KEY_MOUSE_LEFT (-2) +#define PAD_KEY_MOUSE_MIDDLE (-3) +#define PAD_KEY_MOUSE_RIGHT (-4) +#define PAD_KEY_MOUSE_X1 (-5) +#define PAD_KEY_MOUSE_X2 (-6) +typedef u16 PADButton; +typedef u32 PADExtButton; +typedef u16 PADAxis; + +typedef struct PADKeyButtonBinding { + s32 scancode; + PADButton padButton; +} PADKeyButtonBinding; + +typedef struct PADKeyAxisBinding { + s32 scancode; + PADAxis padAxis; + s16 influence; // normalized percentage between 0 and 1 +} PADKeyAxisBinding; + +/* New API to facilitate controller interactions */ +typedef struct PADDeadZones { + bool emulateTriggers; + bool useDeadzones; + u16 stickDeadZone; + u16 substickDeadZone; + u16 leftTriggerActivationZone; + u16 rightTriggerActivationZone; +} PADDeadZones; + +typedef struct PADButtonMapping { + u32 nativeButton; + PADButton padButton; +} PADButtonMapping; + +typedef struct PADAxisMapping { + PADSignedNativeAxis nativeAxis; + s32 nativeButton; + PADAxis padAxis; +} PADAxisMapping; + +typedef struct PADDefaultMapping { + PADButtonMapping buttons[PAD_BUTTON_COUNT]; + PADAxisMapping axes[PAD_AXIS_COUNT]; +} PADDefaultMapping; + +/* Returns the total number of controllers */ +u32 PADCount(); +/* Returns the controller name for the given index into the controller map */ +const char* PADGetNameForControllerIndex(u32 idx); +void PADSetPortForIndex(u32 index, u32 port); +s32 PADGetIndexForPort(u32 port); +void PADGetVidPid(u32 port, u32* vid, u32* pid); +void PADClearPort(u32 port); +const char* PADGetName(u32 port); +void PADSetButtonMapping(u32 port, PADButtonMapping mapping); +void PADSetAllButtonMappings(u32 port, PADButtonMapping buttons[PAD_BUTTON_COUNT]); +PADButtonMapping* PADGetButtonMappings(u32 port, u32* buttonCount); +void PADSetAltButtonMapping(u32 port, PADButtonMapping mapping); +PADButtonMapping* PADGetAltButtonMappings(u32 port, u32* buttonCount); +void PADSetAxisMapping(u32 port, PADAxisMapping mapping); +void PADSetAllAxisMappings(u32 port, PADAxisMapping axes[PAD_AXIS_COUNT]); +PADAxisMapping* PADGetAxisMappings(u32 port, u32* axisCount); +void PADSerializeMappings(); + +BOOL PADSetKeyButtonBinding(u32 port, PADKeyButtonBinding binding); +BOOL PADSetKeyButtonBindings(u32 port, PADKeyButtonBinding bindings[PAD_BUTTON_COUNT]); +PADKeyButtonBinding* PADGetKeyButtonBindings(u32 port, u32* buttonCount); +BOOL PADSetKeyAxisBinding(u32 port, PADKeyAxisBinding binding); +BOOL PADSetKeyAxisBindings(u32 port, PADKeyAxisBinding bindings[PAD_BUTTON_COUNT]); +PADKeyAxisBinding* PADGetKeyAxisBindings(u32 port, u32* axisCount); +void PADClearKeyBindings(u32 port); +void PADSetKeyboardActive(u32 port, BOOL active); + +PADDeadZones* PADGetDeadZones(u32 port); +const char* PADGetButtonName(PADButton); +const char* PADGetNativeButtonName(u32 button); +const char* PADGetAxisName(PADAxis); +const char* PADGetAxisDirectionLabel(PADAxis); +const char* PADGetNativeAxisName(PADSignedNativeAxis axis); + +BOOL PADIsGCAdapter(u32 port); + +/** + * Returns the SDL gamepad for the index into the controller map. + */ +struct SDL_Gamepad* PADGetSDLGamepadForIndex(u32 index); +/* Returns the first native button which is currently pressed */ +s32 PADGetNativeButtonPressed(u32 port); +/* Returns the first native axis which is currently pulled halfway or more */ +PADSignedNativeAxis PADGetNativeAxisPulled(u32 port); +void PADRestoreDefaultMapping(u32 port); +void PADBlockInput(bool block); + +/** + * Set the default controller mapping used. + * + * Must be called before PADInit. + */ + +typedef enum { + PAD_TYPE_UNKNOWN = 0, + PAD_TYPE_STANDARD, + PAD_TYPE_XBOX360, + PAD_TYPE_XBOXONE, + PAD_TYPE_PS3, + PAD_TYPE_PS4, + PAD_TYPE_PS5, + PAD_TYPE_SWITCH_PROCON, + PAD_TYPE_JOYCON_LEFT, + PAD_TYPE_JOYCON_RIGHT, + PAD_TYPE_JOYCON_PAIR, + PAD_TYPE_GAMECUBE, + PAD_TYPE_NSO_GAMECUBE, +} PADControllerType; + +void PADSetDefaultMapping(const PADDefaultMapping* mapping, PADControllerType type); + +BOOL PADSetColor(u32 port, u8 red, u8 green, u8 blue); +BOOL PADGetColor(u32 port, u8* red, u8* green, u8* blue); + +typedef enum { + PAD_SENSOR_INVALID = -1, + PAD_SENSOR_UNKNOWN, + PAD_SENSOR_ACCEL, + PAD_SENSOR_GYRO, + PAD_SENSOR_ACCEL_LEFT, + PAD_SENSOR_GYRO_LEFT, + PAD_SENSOR_ACCEL_RIGHT, + PAD_SENSOR_GYRO_RIGHT, +} PADSensorType; +BOOL PADSetSensorEnabled(u32 port, PADSensorType sensor, BOOL enabled); + +BOOL PADHasSensor(u32 port, PADSensorType sensor); + +BOOL PADGetSensorData(u32 port, PADSensorType sensor, f32* data, int nValues); + +BOOL PADSetRumbleIntensity(u32 port, u16 low, u16 high); +BOOL PADGetRumbleIntensity(u32 port, u16* low, u16* high); +BOOL PADSupportsRumbleIntensity(u32 port); + +typedef enum PADBatteryState { + PAD_BATTERYSTATE_ERROR = -1, + PAD_BATTERYSTATE_UNKNOWN, + PAD_BATTERYSTATE_ON_BATTERY, + PAD_BATTERYSTATE_NO_BATTERY, + PAD_BATTERYSTATE_CHARGING, + PAD_BATTERYSTATE_CHARGED, +} PADBatteryState; + +PADBatteryState PADGetBatteryState(u32 port, f32* perc); + +PADControllerType PADGetControllerType(u32 port); +PADControllerType PADGetControllerTypeForIndex(u32 index); + +#endif + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/perf.h b/aurora-main/include/dolphin/perf.h new file mode 100644 index 0000000..71471a5 --- /dev/null +++ b/aurora-main/include/dolphin/perf.h @@ -0,0 +1,99 @@ +#ifndef _DOLPHIN_PERF_H_ +#define _DOLPHIN_PERF_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef u8 PERFId; + +typedef enum { + PERF_CPU_EVENT, + PERF_CPU_GP_EVENT, + PERF_GP_EVENT, +} PerfType; + +typedef struct PerfSample { + /* 0x00 */ u8 id; + /* 0x04 */ u32 cpuTimeStampStart; + /* 0x08 */ u32 cpuTimeStampEnd; + /* 0x0C */ u32 gpTimeStampStart; + /* 0x10 */ u32 gpTimeStampEnd; + /* 0x14 */ int interrupted; + /* 0x18 */ u32 origcpuStart; + /* 0x1C */ u32 origgpStart; + /* 0x20 */ u32 cacheMisses[4]; + /* 0x30 */ u32 instructions[4]; + /* 0x40 */ u32 cpReq[2]; + /* 0x48 */ u32 tcReq[2]; + /* 0x50 */ u32 cpuRdReq[2]; + /* 0x58 */ u32 cpuWrReq[2]; + /* 0x60 */ u32 dspReq[2]; + /* 0x68 */ u32 ioReq[2]; + /* 0x70 */ u32 viReq[2]; + /* 0x78 */ u32 peReq[2]; + /* 0x80 */ u32 rfReq[2]; + /* 0x88 */ u32 fiReq[2]; + /* 0x90 */ u32 xfWaitIn[2]; + /* 0x98 */ u32 xfWaitOut[2]; + /* 0xA0 */ u32 rasBusy[2]; + /* 0xA8 */ u32 rasClocks[2]; +} PerfSample; + +typedef struct { + PerfSample* samples; + s32 lastSample; + u32 end; + u32 cachemisscycles; +} Frame; + +typedef struct { + char* name; + PerfType type; + s32 currSample; + GXColor color; +} PerfEvent; + +typedef void* (*PERFAllocator)(u32 size); +typedef void (*PERFDeallocator)(void* block); +typedef void (*PERFDrawCallback)(void); + +extern void (*GameDrawInit)(); + +u32 PERFInit(u32 numSamples, u32 numFramesHistory, u32 numTypes, PERFAllocator allocator, PERFDeallocator deallocator, PERFDrawCallback initDraw); +void PERFEventStart(PERFId id); +void PERFEventEnd(PERFId id); +void PERFSetEvent(PERFId id, char* name, PerfType type); +void PERFStartFrame(void); +void PERFEndFrame(void); +void PERFStartAutoSampling(f32 msInterval); +void PERFStopAutoSampling(void); + +void PERFPreDraw(void); +void PERFDumpScreen(void); +void PERFPostDraw(void); +void PERFSetDrawBWBarKey(BOOL tf); +void PERFSetDrawBWBar(BOOL tf); +void PERFSetDrawCPUBar(BOOL tf); +void PERFSetDrawXFBars(BOOL tf); +void PERFSetDrawRASBar(BOOL tf); +void PERFToggleDrawBWBarKey(void); +void PERFToggleDrawBWBar(void); +void PERFToggleDrawCPUBar(void); +void PERFToggleDrawXFBars(void); +void PERFToggleDrawRASBar(void); +void PERFShutDown(void); +void PERFSetDrawFrames(u32 frames); + +extern Frame* PERFFrames; +extern u32 PERFCurrFrame; +extern PerfEvent* PERFEvents; +extern u32 PERFNumEvents; + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/ppc_math.h b/aurora-main/include/dolphin/ppc_math.h new file mode 100644 index 0000000..c8f4cb5 --- /dev/null +++ b/aurora-main/include/dolphin/ppc_math.h @@ -0,0 +1,136 @@ +#ifndef DOLPHIN_PPC_MATH_H +#define DOLPHIN_PPC_MATH_H + +#include +#include + +// frsqrte matching courtesy of Geotale, with reference to https://achurch.org/cpu-tests/ppc750cl.s + +struct BaseAndDec32 { + uint32_t base; + int32_t dec; +}; + +struct BaseAndDec64 { + uint64_t base; + int64_t dec; +}; + +union c32 { + uint32_t u; + float f; +}; + +union c64 { + uint64_t u; + double f; +}; + +#define EXPONENT_SHIFT_F64 ((uint64_t)52) +#define MANTISSA_MASK_F64 ((uint64_t)0x000fffffffffffffULL) +#define EXPONENT_MASK_F64 ((uint64_t)0x7ff0000000000000ULL) +#define SIGN_MASK_F64 ((uint64_t)0x8000000000000000ULL) + +static const struct BaseAndDec64 RSQRTE_TABLE[32] = { + {0x69fa000000000ULL, -0x15a0000000LL}, + {0x5f2e000000000ULL, -0x13cc000000LL}, + {0x554a000000000ULL, -0x1234000000LL}, + {0x4c30000000000ULL, -0x10d4000000LL}, + {0x43c8000000000ULL, -0x0f9c000000LL}, + {0x3bfc000000000ULL, -0x0e88000000LL}, + {0x34b8000000000ULL, -0x0d94000000LL}, + {0x2df0000000000ULL, -0x0cb8000000LL}, + {0x2794000000000ULL, -0x0bf0000000LL}, + {0x219c000000000ULL, -0x0b40000000LL}, + {0x1bfc000000000ULL, -0x0aa0000000LL}, + {0x16ae000000000ULL, -0x0a0c000000LL}, + {0x11a8000000000ULL, -0x0984000000LL}, + {0x0ce6000000000ULL, -0x090c000000LL}, + {0x0862000000000ULL, -0x0898000000LL}, + {0x0416000000000ULL, -0x082c000000LL}, + {0xffe8000000000ULL, -0x1e90000000LL}, + {0xf0a4000000000ULL, -0x1c00000000LL}, + {0xe2a8000000000ULL, -0x19c0000000LL}, + {0xd5c8000000000ULL, -0x17c8000000LL}, + {0xc9e4000000000ULL, -0x1610000000LL}, + {0xbedc000000000ULL, -0x1490000000LL}, + {0xb498000000000ULL, -0x1330000000LL}, + {0xab00000000000ULL, -0x11f8000000LL}, + {0xa204000000000ULL, -0x10e8000000LL}, + {0x9994000000000ULL, -0x0fe8000000LL}, + {0x91a0000000000ULL, -0x0f08000000LL}, + {0x8a1c000000000ULL, -0x0e38000000LL}, + {0x8304000000000ULL, -0x0d78000000LL}, + {0x7c48000000000ULL, -0x0cc8000000LL}, + {0x75e4000000000ULL, -0x0c28000000LL}, + {0x6fd0000000000ULL, -0x0b98000000LL}, +}; + +#ifdef _MSC_VER +#include +static inline uint32_t ppc_clz64(uint64_t x) { + unsigned long idx; + _BitScanReverse64(&idx, x); + return 63u - (uint32_t)idx; +} +#else +static inline uint32_t ppc_clz64(uint64_t x) { + return (uint32_t)__builtin_clzll(x); +} +#endif + +static inline double frsqrte(double val) { + union c64 bits; + uint64_t mantissa; + int64_t exponent; + int sign; + uint32_t key; + uint64_t new_exp; + const struct BaseAndDec64 *entry; + union c64 result; + + bits.f = val; + mantissa = bits.u & MANTISSA_MASK_F64; + exponent = (int64_t)(bits.u & EXPONENT_MASK_F64); + sign = (bits.u & SIGN_MASK_F64) != 0; + + if (mantissa == 0 && exponent == 0) { + return copysign(INFINITY, bits.f); + } + + if ((uint64_t)exponent == EXPONENT_MASK_F64) { + if (mantissa == 0) { + return sign ? NAN : 0.0; + } + return val; + } + + if (sign) { + return NAN; + } + + if (exponent == 0) { + uint32_t shift = ppc_clz64(mantissa) - (uint32_t)(63 - EXPONENT_SHIFT_F64); + mantissa <<= shift; + mantissa &= MANTISSA_MASK_F64; + exponent -= (int64_t)(shift - 1) << EXPONENT_SHIFT_F64; + } + + key = (uint32_t)(((uint64_t)exponent | mantissa) >> 37); + new_exp = (((0xbfcULL << EXPONENT_SHIFT_F64) - (uint64_t)exponent) >> 1) & EXPONENT_MASK_F64; + + entry = &RSQRTE_TABLE[0x1fu & (key >> 11)]; + result.u = new_exp | (uint64_t)(entry->base + entry->dec * (int64_t)(key & 0x7ffu)); + return result.f; +} + +// One Newton-Raphson step +static inline float ppc_rsqrte(float x) { + double rsqrt_d = frsqrte((double)x); + float nwork0 = (float)(rsqrt_d * rsqrt_d); + float nwork1 = (float)(rsqrt_d * 0.5); + nwork0 = fmaf(-nwork0, x, 3.0f); + return nwork0 * nwork1; +} + +#endif // DOLPHIN_PPC_MATH_H diff --git a/aurora-main/include/dolphin/si.h b/aurora-main/include/dolphin/si.h new file mode 100644 index 0000000..02eb65a --- /dev/null +++ b/aurora-main/include/dolphin/si.h @@ -0,0 +1,72 @@ +#ifndef DOLPHIN_SI_H +#define DOLPHIN_SI_H + +#include + +#define SI_CHAN0 0 +#define SI_CHAN1 1 +#define SI_CHAN2 2 +#define SI_CHAN3 3 +#define SI_MAX_CHAN 4 + +#define SI_CHAN0_BIT 0x80000000 +#define SI_CHAN1_BIT 0x40000000 +#define SI_CHAN2_BIT 0x20000000 +#define SI_CHAN3_BIT 0x10000000 +#define SI_CHAN_BIT(chn) (SI_CHAN0_BIT >> (chn)) + +#define SI_ERROR_UNDER_RUN 0x0001 +#define SI_ERROR_OVER_RUN 0x0002 +#define SI_ERROR_COLLISION 0x0004 +#define SI_ERROR_NO_RESPONSE 0x0008 +#define SI_ERROR_WRST 0x0010 +#define SI_ERROR_RDST 0x0020 +#define SI_ERROR_UNKNOWN 0x0040 +#define SI_ERROR_BUSY 0x0080 + +#define SI_TYPE_MASK 0x18000000u +#define SI_TYPE_N64 0x00000000u +#define SI_TYPE_DOLPHIN 0x08000000u +#define SI_TYPE_GC SI_TYPE_DOLPHIN + +// GameCube specific +#define SI_GC_WIRELESS 0x80000000u +#define SI_GC_NOMOTOR 0x20000000u // no rumble motor +#define SI_GC_STANDARD 0x01000000u // dolphin standard controller + +// WaveBird specific +#define SI_WIRELESS_RECEIVED 0x40000000u // 0: no wireless unit +#define SI_WIRELESS_IR 0x04000000u // 0: IR 1: RF +#define SI_WIRELESS_STATE 0x02000000u // 0: variable 1: fixed +#define SI_WIRELESS_ORIGIN 0x00200000u // 0: invalid 1: valid +#define SI_WIRELESS_FIX_ID 0x00100000u // 0: not fixed 1: fixed +#define SI_WIRELESS_TYPE 0x000f0000u +#define SI_WIRELESS_LITE_MASK 0x000c0000u // 0: normal 1: lite controller +#define SI_WIRELESS_LITE 0x00040000u // 0: normal 1: lite controller +#define SI_WIRELESS_CONT_MASK 0x00080000u // 0: non-controller 1: non-controller +#define SI_WIRELESS_CONT 0x00000000u +#define SI_WIRELESS_ID 0x00c0ff00u +#define SI_WIRELESS_TYPE_ID (SI_WIRELESS_TYPE | SI_WIRELESS_ID) + +#define SI_N64_CONTROLLER (SI_TYPE_N64 | 0x05000000) +#define SI_N64_MIC (SI_TYPE_N64 | 0x00010000) +#define SI_N64_KEYBOARD (SI_TYPE_N64 | 0x00020000) +#define SI_N64_MOUSE (SI_TYPE_N64 | 0x02000000) +#define SI_GBA (SI_TYPE_N64 | 0x00040000) +#define SI_GC_CONTROLLER (SI_TYPE_GC | SI_GC_STANDARD) +#define SI_GC_RECEIVER (SI_TYPE_GC | SI_GC_WIRELESS) +#define SI_GC_WAVEBIRD (SI_TYPE_GC | SI_GC_WIRELESS | SI_GC_STANDARD | SI_WIRELESS_STATE | SI_WIRELESS_FIX_ID) +#define SI_GC_KEYBOARD (SI_TYPE_GC | 0x00200000) +#define SI_GC_STEERING (SI_TYPE_GC | 0x00000000) + +#ifdef __cplusplus +extern "C" { +#endif + +u32 SIProbe(s32 chan); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/dolphin/thp.h b/aurora-main/include/dolphin/thp.h new file mode 100644 index 0000000..455afe6 --- /dev/null +++ b/aurora-main/include/dolphin/thp.h @@ -0,0 +1,154 @@ +#ifndef _DOLPHIN_THP_H_ +#define _DOLPHIN_THP_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef u8 THPSample; +typedef s16 THPCoeff; +typedef f32 THPQuantTab[64]; + +typedef struct _THPHuffmanTab { + u8 quick[32]; + u8 increment[32]; + u8* Vij; + s32 maxCode[18]; + s32 valPtr[18]; + u8 Vij1; + u8 pad[11]; +} THPHuffmanTab; + +typedef struct _THPComponent { + u8 quantizationTableSelector; + u8 DCTableSelector; + u8 ACTableSelector; + THPCoeff predDC; +} THPComponent; + +typedef struct _THPFileInfo { + THPQuantTab quantTabs[3]; + THPHuffmanTab huffmanTabs[4]; + THPComponent components[3]; + u16 xPixelSize; + u16 yPixelSize; + u16 MCUsPerRow; + u16 decompressedY; + u8* c; + u32 currByte; + u32 cnt; + u8 validHuffmanTabs; + u8 RST; + u16 nMCU; + u16 currMCU; + u8* dLC[3]; +} THPFileInfo; + +typedef struct THPAudioRecordHeader { + u32 offsetNextChannel; + u32 sampleSize; + s16 lCoef[8][2]; + s16 rCoef[8][2]; + s16 lYn1; + s16 lYn2; + s16 rYn1; + s16 rYn2; +} THPAudioRecordHeader; + +typedef struct THPAudioDecodeInfo { + u8* encodeData; + u32 offsetNibbles; + u8 predictor; + u8 scale; + s16 yn1; + s16 yn2; +} THPAudioDecodeInfo; + +typedef struct THPTextureSet { + u8* ytexture; + u8* utexture; + u8* vtexture; + s32 frameNumber; +} THPTextureSet; + +typedef struct THPAudioBuffer { + s16* buffer; + s16* curPtr; + u32 validSample; +} THPAudioBuffer; + +typedef struct THPVideoInfo { + u32 xSize; + u32 ySize; + u32 videoType; +} THPVideoInfo; + +typedef struct THPAudioInfo { + u32 sndChannels; + u32 sndFrequency; + u32 sndNumSamples; + u32 sndNumTracks; +} THPAudioInfo; + +typedef struct THPFrameCompInfo { + u32 numComponents; + u8 frameComp[16]; +} THPFrameCompInfo; + +typedef struct THPHeader { + /* 0x00 */ char magic[4]; + /* 0x04 */ u32 version; + /* 0x08 */ u32 bufsize; + /* 0x0C */ u32 audioMaxSamples; + /* 0x10 */ f32 frameRate; + /* 0x14 */ u32 numFrames; + /* 0x18 */ u32 firstFrameSize; + /* 0x1C */ u32 movieDataSize; + /* 0x20 */ u32 compInfoDataOffsets; + /* 0x24 */ u32 offsetDataOffsets; + /* 0x28 */ u32 movieDataOffsets; + /* 0x2C */ u32 finalFrameDataOffsets; +} THPHeader; + +#define THP_AUDIO_BUFFER_COUNT 3 +#define THP_READ_BUFFER_COUNT 10 +#define THP_TEXTURE_SET_COUNT 3 + +static u32 THPAudioDecode(s16* audioBuffer, u8* audioFrame, s32 flag); +static s32 __THPAudioGetNewSample(THPAudioDecodeInfo* info); +static void __THPAudioInitialize(THPAudioDecodeInfo* info, u8* ptr); + +s32 __THPAudioGetNewSample(THPAudioDecodeInfo*); +void __THPAudioInitialize(THPAudioDecodeInfo*, u8*); + +static void __THPSetupBuffers(void); +static u8 __THPReadFrameHeader(void); +static u8 __THPReadScaneHeader(void); +static u8 __THPReadQuantizationTable(void); +static u8 __THPReadHuffmanTableSpecification(void); +static void __THPHuffGenerateSizeTable(void); +static void __THPHuffGenerateCodeTable(void); +static void __THPHuffGenerateDecoderTables(u8 tabIndex); +static void __THPRestartDefinition(void); +static void __THPPrepBitStream(void); +static void __THPDecompressYUV(void* tileY, void* tileU, void* tileV); +static void __THPGQRRestore(void); +static void __THPDecompressiMCURow512x448(void); +static void __THPDecompressiMCURow640x480(void); +static void __THPDecompressiMCURowNxN(void); +static void __THPInverseDCTNoYPos(THPCoeff* in, u32 xPos); +static void __THPHuffDecodeDCTCompY(THPFileInfo* info, THPCoeff* block); +static void __THPHuffDecodeDCTCompU(THPFileInfo* info, THPCoeff* block); +static void __THPHuffDecodeDCTCompV(THPFileInfo* info, THPCoeff* block); + +static void __THPInverseDCTY8(THPCoeff* in, u32 xPos); +static void __THPGQRSetup(); +static s32 __THPHuffDecodeTab(THPFileInfo* info, THPHuffmanTab* h); + +#ifdef __cplusplus +} +#endif + +#endif // _DOLPHIN_THP_H_ diff --git a/aurora-main/include/dolphin/types.h b/aurora-main/include/dolphin/types.h new file mode 100644 index 0000000..48c51bf --- /dev/null +++ b/aurora-main/include/dolphin/types.h @@ -0,0 +1,110 @@ +#ifndef DOLPHIN_TYPES_H +#define DOLPHIN_TYPES_H + +#if _WIN64 || __LP64__ +#define BIT_64 1 +#else +#define BIT_64 0 +#endif + +#ifdef TARGET_PC +#include +typedef int8_t s8; +typedef int16_t s16; +typedef int32_t s32; +typedef int64_t s64; +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; +#else +typedef signed char s8; +typedef signed short int s16; +typedef signed long s32; +typedef signed long long int s64; +typedef unsigned char u8; +typedef unsigned short int u16; +typedef unsigned long u32; +typedef unsigned long long int u64; +#endif + +typedef volatile u8 vu8; +typedef volatile u16 vu16; +typedef volatile u32 vu32; +typedef volatile u64 vu64; + +typedef volatile s8 vs8; +typedef volatile s16 vs16; +typedef volatile s32 vs32; +typedef volatile s64 vs64; + +typedef float f32; +typedef double f64; + +typedef volatile f32 vf32; +typedef volatile f64 vf64; + +typedef char *Ptr; + +#if defined(TARGET_PC) +#include +typedef int BOOL; +#ifndef FALSE +#define FALSE 0 +#endif +#ifndef TRUE +#define TRUE 1 +#endif +#endif + +#ifdef TARGET_PC +#include +#else +#ifndef NULL +#define NULL 0 +#endif +#endif +#ifndef __cplusplus +#ifndef nullptr +#define nullptr NULL +#endif +#endif + +#if defined(__MWERKS__) +#define AT_ADDRESS(addr) : (addr) +#define ATTRIBUTE_ALIGN(num) __attribute__((aligned(num))) +#elif defined(__GNUC__) +#define AT_ADDRESS(addr) +#define ATTRIBUTE_ALIGN(num) __attribute__((aligned(num))) +#elif defined(_MSC_VER) +#define AT_ADDRESS(addr) +#define ATTRIBUTE_ALIGN(num) +#else +#error unknown compiler +#endif + +#ifndef DECL_WEAK +#if defined(__MWERKS__) +#define DECL_WEAK __declspec(weak) +#elif defined(__GNUC__) +#define DECL_WEAK __attribute__((weak)) +#elif defined(_MSC_VER) +#define DECL_WEAK +#else +#error unknown compiler +#endif +#endif + +#if TARGET_PC && __cplusplus +#define NORETURN [[noreturn]] +#else +#define NORETURN +#endif + +#ifdef __MWERKS__ +#define __REGISTER register +#else +#define __REGISTER +#endif + +#endif diff --git a/aurora-main/include/dolphin/vi.h b/aurora-main/include/dolphin/vi.h new file mode 100644 index 0000000..377d952 --- /dev/null +++ b/aurora-main/include/dolphin/vi.h @@ -0,0 +1,66 @@ +#ifndef DOLPHIN_VI_H +#define DOLPHIN_VI_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*VIRetraceCallback)(u32 retraceCount); + +void VIInit(void); +void VIConfigure(const GXRenderModeObj *rm); +void VIConfigurePan(u16 xOrg, u16 yOrg, u16 width, u16 height); +void VIFlush(void); + +void VIWaitForRetrace(void); + +u32 VIGetTvFormat(void); +u32 VIGetRetraceCount(void); +u32 VIGetNextField(void); +u32 VIGetDTVStatus(void); + +void* VIGetCurrentFrameBuffer(void); +void* VIGetNextFrameBuffer(void); +void VISetNextFrameBuffer(void *fb); +void VISetBlack(BOOL black); + +typedef void (*VIRetraceCallback)(u32 retraceCount); + +VIRetraceCallback VISetPreRetraceCallback(VIRetraceCallback cb); +VIRetraceCallback VISetPostRetraceCallback(VIRetraceCallback cb); + +#ifdef TARGET_PC +void VISetWindowTitle(const char* title); +void VISetWindowFullscreen(bool fullscreen); +bool VIGetWindowFullscreen(); +void VISetWindowSize(uint32_t width, uint32_t height); +void VISetWindowPosition(uint32_t x, uint32_t y); +void VICenterWindow(); + +/** + * Sets the internal framebuffer resolution to a specific scale factor of the configured EFB size. + * A value of 0.0f means "Auto", which will use the underlying swapchain size (usually the window size). + */ +void VISetFrameBufferScale(float scale); + +/** + * \brief Lock the GX framebuffer to a specific aspect ratio, without changing the native framebuffer. + * + * @param width Width part of the aspect ratio fraction. + * @param height Height part of the aspect ratio fraction. + */ +void VILockAspectRatio(int width, int height); + +/** + * \brief Undoes a previous call to VILockAspectRatio. + */ +void VIUnlockAspectRatio(); +#endif + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/aurora-main/include/magic_enum.hpp b/aurora-main/include/magic_enum.hpp new file mode 100644 index 0000000..18c2804 --- /dev/null +++ b/aurora-main/include/magic_enum.hpp @@ -0,0 +1,1135 @@ +// __ __ _ ______ _____ +// | \/ | (_) | ____| / ____|_ _ +// | \ / | __ _ __ _ _ ___ | |__ _ __ _ _ _ __ ___ | | _| |_ _| |_ +// | |\/| |/ _` |/ _` | |/ __| | __| | '_ \| | | | '_ ` _ \ | | |_ _|_ _| +// | | | | (_| | (_| | | (__ | |____| | | | |_| | | | | | | | |____|_| |_| +// |_| |_|\__,_|\__, |_|\___| |______|_| |_|\__,_|_| |_| |_| \_____| +// __/ | https://github.com/Neargye/magic_enum +// |___/ version 0.7.2 +// +// Licensed under the MIT License . +// SPDX-License-Identifier: MIT +// Copyright (c) 2019 - 2021 Daniil Goncharov . +// +// 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. + +#ifndef NEARGYE_MAGIC_ENUM_HPP +#define NEARGYE_MAGIC_ENUM_HPP + +#define MAGIC_ENUM_VERSION_MAJOR 0 +#define MAGIC_ENUM_VERSION_MINOR 7 +#define MAGIC_ENUM_VERSION_PATCH 2 + +#include +#include +#include +#include +#include +#include +#include +#include + +#if !defined(MAGIC_ENUM_USING_ALIAS_OPTIONAL) +#include +#endif +#if !defined(MAGIC_ENUM_USING_ALIAS_STRING) +#include +#endif +#if !defined(MAGIC_ENUM_USING_ALIAS_STRING_VIEW) +#include +#endif + +#if defined(__clang__) +# pragma clang diagnostic push +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wmaybe-uninitialized" // May be used uninitialized 'return {};'. +#elif defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable : 26495) // Variable 'static_string::chars_' is uninitialized. +# pragma warning(disable : 28020) // Arithmetic overflow: Using operator '-' on a 4 byte value and then casting the result to a 8 byte value. +# pragma warning(disable : 26451) // The expression '0<=_Param_(1)&&_Param_(1)<=1-1' is not true at this call. +#endif + +// Checks magic_enum compiler compatibility. +#if defined(__clang__) && __clang_major__ >= 5 || defined(__GNUC__) && __GNUC__ >= 9 || defined(_MSC_VER) && _MSC_VER >= 1910 +# undef MAGIC_ENUM_SUPPORTED +# define MAGIC_ENUM_SUPPORTED 1 +#endif + +// Checks magic_enum compiler aliases compatibility. +#if defined(__clang__) && __clang_major__ >= 5 || defined(__GNUC__) && __GNUC__ >= 9 || defined(_MSC_VER) && _MSC_VER >= 1920 +# undef MAGIC_ENUM_SUPPORTED_ALIASES +# define MAGIC_ENUM_SUPPORTED_ALIASES 1 +#endif + +// Enum value must be greater or equals than MAGIC_ENUM_RANGE_MIN. By default MAGIC_ENUM_RANGE_MIN = -128. +// If need another min range for all enum types by default, redefine the macro MAGIC_ENUM_RANGE_MIN. +#if !defined(MAGIC_ENUM_RANGE_MIN) +# define MAGIC_ENUM_RANGE_MIN -128 +#endif + +// Enum value must be less or equals than MAGIC_ENUM_RANGE_MAX. By default MAGIC_ENUM_RANGE_MAX = 128. +// If need another max range for all enum types by default, redefine the macro MAGIC_ENUM_RANGE_MAX. +#if !defined(MAGIC_ENUM_RANGE_MAX) +# define MAGIC_ENUM_RANGE_MAX 128 +#endif + +namespace magic_enum { + +// If need another optional type, define the macro MAGIC_ENUM_USING_ALIAS_OPTIONAL. +#if defined(MAGIC_ENUM_USING_ALIAS_OPTIONAL) +MAGIC_ENUM_USING_ALIAS_OPTIONAL +#else +using std::optional; +#endif + +// If need another string_view type, define the macro MAGIC_ENUM_USING_ALIAS_STRING_VIEW. +#if defined(MAGIC_ENUM_USING_ALIAS_STRING_VIEW) +MAGIC_ENUM_USING_ALIAS_STRING_VIEW +#else +using std::string_view; +#endif + +// If need another string type, define the macro MAGIC_ENUM_USING_ALIAS_STRING. +#if defined(MAGIC_ENUM_USING_ALIAS_STRING) +MAGIC_ENUM_USING_ALIAS_STRING +#else +using std::string; +#endif + +namespace customize { + +// Enum value must be in range [MAGIC_ENUM_RANGE_MIN, MAGIC_ENUM_RANGE_MAX]. By default MAGIC_ENUM_RANGE_MIN = -128, MAGIC_ENUM_RANGE_MAX = 128. +// If need another range for all enum types by default, redefine the macro MAGIC_ENUM_RANGE_MIN and MAGIC_ENUM_RANGE_MAX. +// If need another range for specific enum type, add specialization enum_range for necessary enum type. +template +struct enum_range { + static_assert(std::is_enum_v, "magic_enum::customize::enum_range requires enum type."); + inline static constexpr int min = MAGIC_ENUM_RANGE_MIN; + inline static constexpr int max = MAGIC_ENUM_RANGE_MAX; + static_assert(max > min, "magic_enum::customize::enum_range requires max > min."); +}; + +static_assert(MAGIC_ENUM_RANGE_MIN <= 0, "MAGIC_ENUM_RANGE_MIN must be less or equals than 0."); +static_assert(MAGIC_ENUM_RANGE_MIN > (std::numeric_limits::min)(), "MAGIC_ENUM_RANGE_MIN must be greater than INT16_MIN."); + +static_assert(MAGIC_ENUM_RANGE_MAX > 0, "MAGIC_ENUM_RANGE_MAX must be greater than 0."); +static_assert(MAGIC_ENUM_RANGE_MAX < (std::numeric_limits::max)(), "MAGIC_ENUM_RANGE_MAX must be less than INT16_MAX."); + +static_assert(MAGIC_ENUM_RANGE_MAX > MAGIC_ENUM_RANGE_MIN, "MAGIC_ENUM_RANGE_MAX must be greater than MAGIC_ENUM_RANGE_MIN."); + +// If need custom names for enum, add specialization enum_name for necessary enum type. +template +constexpr string_view enum_name(E) noexcept { + static_assert(std::is_enum_v, "magic_enum::customize::enum_name requires enum type."); + + return {}; +} + +} // namespace magic_enum::customize + +namespace detail { + +template +struct supported +#if defined(MAGIC_ENUM_SUPPORTED) && MAGIC_ENUM_SUPPORTED || defined(MAGIC_ENUM_NO_CHECK_SUPPORT) + : std::true_type {}; +#else + : std::false_type {}; +#endif + +struct char_equal_to { + constexpr bool operator()(char lhs, char rhs) const noexcept { + return lhs == rhs; + } +}; + +template +class static_string { + public: + constexpr explicit static_string(string_view str) noexcept : static_string{str, std::make_index_sequence{}} { + assert(str.size() == N); + } + + constexpr const char* data() const noexcept { return chars_; } + + constexpr std::size_t size() const noexcept { return N; } + + constexpr operator string_view() const noexcept { return {data(), size()}; } + + private: + template + constexpr static_string(string_view str, std::index_sequence) noexcept : chars_{str[I]..., '\0'} {} + + char chars_[N + 1]; +}; + +template <> +class static_string<0> { + public: + constexpr explicit static_string(string_view) noexcept {} + + constexpr const char* data() const noexcept { return nullptr; } + + constexpr std::size_t size() const noexcept { return 0; } + + constexpr operator string_view() const noexcept { return {}; } +}; + +constexpr string_view pretty_name(string_view name) noexcept { + for (std::size_t i = name.size(); i > 0; --i) { + if (!((name[i - 1] >= '0' && name[i - 1] <= '9') || + (name[i - 1] >= 'a' && name[i - 1] <= 'z') || + (name[i - 1] >= 'A' && name[i - 1] <= 'Z') || + (name[i - 1] == '_'))) { + name.remove_prefix(i); + break; + } + } + + if (name.size() > 0 && ((name.front() >= 'a' && name.front() <= 'z') || + (name.front() >= 'A' && name.front() <= 'Z') || + (name.front() == '_'))) { + return name; + } + + return {}; // Invalid name. +} + +constexpr std::size_t find(string_view str, char c) noexcept { +#if defined(__clang__) && __clang_major__ < 9 && defined(__GLIBCXX__) || defined(_MSC_VER) && _MSC_VER < 1920 && !defined(__clang__) +// https://stackoverflow.com/questions/56484834/constexpr-stdstring-viewfind-last-of-doesnt-work-on-clang-8-with-libstdc +// https://developercommunity.visualstudio.com/content/problem/360432/vs20178-regression-c-failed-in-test.html + constexpr bool workaround = true; +#else + constexpr bool workaround = false; +#endif + if constexpr (workaround) { + for (std::size_t i = 0; i < str.size(); ++i) { + if (str[i] == c) { + return i; + } + } + + return string_view::npos; + } else { + return str.find_first_of(c); + } +} + +template +constexpr std::array, N> to_array(T (&a)[N], std::index_sequence) { + return {{a[I]...}}; +} + +template +constexpr bool cmp_equal(string_view lhs, string_view rhs, BinaryPredicate&& p) noexcept(std::is_nothrow_invocable_r_v) { +#if defined(_MSC_VER) && _MSC_VER < 1920 && !defined(__clang__) + // https://developercommunity.visualstudio.com/content/problem/360432/vs20178-regression-c-failed-in-test.html + // https://developercommunity.visualstudio.com/content/problem/232218/c-constexpr-string-view.html + constexpr bool workaround = true; +#else + constexpr bool workaround = false; +#endif + constexpr bool default_predicate = std::is_same_v, char_equal_to>; + + if constexpr (default_predicate && !workaround) { + static_cast(p); + return lhs == rhs; + } else { + if (lhs.size() != rhs.size()) { + return false; + } + + const auto size = lhs.size(); + for (std::size_t i = 0; i < size; ++i) { + if (!p(lhs[i], rhs[i])) { + return false; + } + } + + return true; + } +} + +template +constexpr bool cmp_less(L lhs, R rhs) noexcept { + static_assert(std::is_integral_v && std::is_integral_v, "magic_enum::detail::cmp_less requires integral type."); + + if constexpr (std::is_signed_v == std::is_signed_v) { + // If same signedness (both signed or both unsigned). + return lhs < rhs; + } else if constexpr (std::is_signed_v) { + // If 'right' is negative, then result is 'false', otherwise cast & compare. + return rhs > 0 && lhs < static_cast>(rhs); + } else { + // If 'left' is negative, then result is 'true', otherwise cast & compare. + return lhs < 0 || static_cast>(lhs) < rhs; + } +} + +template +constexpr I log2(I value) noexcept { + static_assert(std::is_integral_v, "magic_enum::detail::log2 requires integral type."); + + auto ret = I{0}; + for (; value > I{1}; value >>= I{1}, ++ret) {} + + return ret; +} + +template +constexpr bool is_pow2(I x) noexcept { + static_assert(std::is_integral_v, "magic_enum::detail::is_pow2 requires integral type."); + + return x != 0 && (x & (x - 1)) == 0; +} + +template +inline constexpr bool is_enum_v = std::is_enum_v && std::is_same_v>; + +template +constexpr auto n() noexcept { + static_assert(is_enum_v, "magic_enum::detail::n requires enum type."); +#if defined(MAGIC_ENUM_SUPPORTED) && MAGIC_ENUM_SUPPORTED +# if defined(__clang__) + constexpr string_view name{__PRETTY_FUNCTION__ + 34, sizeof(__PRETTY_FUNCTION__) - 36}; +# elif defined(__GNUC__) + constexpr string_view name{__PRETTY_FUNCTION__ + 49, sizeof(__PRETTY_FUNCTION__) - 51}; +# elif defined(_MSC_VER) + constexpr string_view name{__FUNCSIG__ + 40, sizeof(__FUNCSIG__) - 57}; +# endif + return static_string{name}; +#else + return string_view{}; // Unsupported compiler. +#endif +} + +template +inline constexpr auto type_name_v = n(); + +template +constexpr auto n() noexcept { + static_assert(is_enum_v, "magic_enum::detail::n requires enum type."); + constexpr auto custom_name = customize::enum_name(V); + + if constexpr (custom_name.empty()) { + static_cast(custom_name); +#if defined(MAGIC_ENUM_SUPPORTED) && MAGIC_ENUM_SUPPORTED +# if defined(__clang__) || defined(__GNUC__) + constexpr auto name = pretty_name({__PRETTY_FUNCTION__, sizeof(__PRETTY_FUNCTION__) - 2}); +# elif defined(_MSC_VER) + constexpr auto name = pretty_name({__FUNCSIG__, sizeof(__FUNCSIG__) - 17}); +# endif + return static_string{name}; +#else + return string_view{}; // Unsupported compiler. +#endif + } else { + return static_string{custom_name}; + } +} + +template +inline constexpr auto enum_name_v = n(); + +template +constexpr bool is_valid() noexcept { + static_assert(is_enum_v, "magic_enum::detail::is_valid requires enum type."); + + return n(V)>().size() != 0; +} + +template > +constexpr E value(std::size_t i) noexcept { + static_assert(is_enum_v, "magic_enum::detail::value requires enum type."); + + if constexpr (IsFlags) { + return static_cast(U{1} << static_cast(static_cast(i) + O)); + } else { + return static_cast(static_cast(i) + O); + } +} + +template > +constexpr int reflected_min() noexcept { + static_assert(is_enum_v, "magic_enum::detail::reflected_min requires enum type."); + + if constexpr (IsFlags) { + return 0; + } else { + constexpr auto lhs = customize::enum_range::min; + static_assert(lhs > (std::numeric_limits::min)(), "magic_enum::enum_range requires min must be greater than INT16_MIN."); + constexpr auto rhs = (std::numeric_limits::min)(); + + if constexpr (cmp_less(lhs, rhs)) { + return rhs; + } else { + static_assert(!is_valid(0)>(), "magic_enum::enum_range detects enum value smaller than min range size."); + return lhs; + } + } +} + +template > +constexpr int reflected_max() noexcept { + static_assert(is_enum_v, "magic_enum::detail::reflected_max requires enum type."); + + if constexpr (IsFlags) { + return std::numeric_limits::digits - 1; + } else { + constexpr auto lhs = customize::enum_range::max; + static_assert(lhs < (std::numeric_limits::max)(), "magic_enum::enum_range requires max must be less than INT16_MAX."); + constexpr auto rhs = (std::numeric_limits::max)(); + + if constexpr (cmp_less(lhs, rhs)) { + static_assert(!is_valid(0)>(), "magic_enum::enum_range detects enum value larger than max range size."); + return lhs; + } else { + return rhs; + } + } +} + +template +inline constexpr auto reflected_min_v = reflected_min(); + +template +inline constexpr auto reflected_max_v = reflected_max(); + +template +constexpr std::size_t values_count(const bool (&valid)[N]) noexcept { + auto count = std::size_t{0}; + for (std::size_t i = 0; i < N; ++i) { + if (valid[i]) { + ++count; + } + } + + return count; +} + +template +constexpr auto values(std::index_sequence) noexcept { + static_assert(is_enum_v, "magic_enum::detail::values requires enum type."); + constexpr bool valid[sizeof...(I)] = {is_valid(I)>()...}; + constexpr std::size_t count = values_count(valid); + + if constexpr (count > 0) { + E values[count] = {}; + for (std::size_t i = 0, v = 0; v < count; ++i) { + if (valid[i]) { + values[v++] = value(i); + } + } + + return to_array(values, std::make_index_sequence{}); + } else { + return std::array{}; + } +} + +template > +constexpr auto values() noexcept { + static_assert(is_enum_v, "magic_enum::detail::values requires enum type."); + constexpr auto min = reflected_min_v; + constexpr auto max = reflected_max_v; + constexpr auto range_size = max - min + 1; + static_assert(range_size > 0, "magic_enum::enum_range requires valid size."); + static_assert(range_size < (std::numeric_limits::max)(), "magic_enum::enum_range requires valid size."); + + return values>(std::make_index_sequence{}); +} + +template +inline constexpr auto values_v = values(); + +template > +using values_t = decltype((values_v)); + +template +inline constexpr auto count_v = values_v.size(); + +template > +inline constexpr auto min_v = (count_v > 0) ? static_cast(values_v.front()) : U{0}; + +template > +inline constexpr auto max_v = (count_v > 0) ? static_cast(values_v.back()) : U{0}; + +template > +constexpr std::size_t range_size() noexcept { + static_assert(is_enum_v, "magic_enum::detail::range_size requires enum type."); + constexpr auto max = IsFlags ? log2(max_v) : max_v; + constexpr auto min = IsFlags ? log2(min_v) : min_v; + constexpr auto range_size = max - min + U{1}; + static_assert(range_size > 0, "magic_enum::enum_range requires valid size."); + static_assert(range_size < (std::numeric_limits::max)(), "magic_enum::enum_range requires valid size."); + + return static_cast(range_size); +} + +template +inline constexpr auto range_size_v = range_size(); + +template +using index_t = std::conditional_t < (std::numeric_limits::max)(), std::uint8_t, std::uint16_t>; + +template +inline constexpr auto invalid_index_v = (std::numeric_limits>::max)(); + +template +constexpr auto indexes(std::index_sequence) noexcept { + static_assert(is_enum_v, "magic_enum::detail::indexes requires enum type."); + constexpr auto min = IsFlags ? log2(min_v) : min_v; + [[maybe_unused]] auto i = index_t{0}; + + return std::array{{(is_valid(I)>() ? i++ : invalid_index_v)...}}; +} + +template +inline constexpr auto indexes_v = indexes(std::make_index_sequence>{}); + +template +constexpr auto names(std::index_sequence) noexcept { + static_assert(is_enum_v, "magic_enum::detail::names requires enum type."); + + return std::array{{enum_name_v[I]>...}}; +} + +template +inline constexpr auto names_v = names(std::make_index_sequence>{}); + +template > +using names_t = decltype((names_v)); + +template +constexpr auto entries(std::index_sequence) noexcept { + static_assert(is_enum_v, "magic_enum::detail::entries requires enum type."); + + return std::array, sizeof...(I)>{{{values_v[I], enum_name_v[I]>}...}}; +} + +template +inline constexpr auto entries_v = entries(std::make_index_sequence>{}); + +template > +using entries_t = decltype((entries_v)); + +template > +constexpr bool is_sparse() noexcept { + static_assert(is_enum_v, "magic_enum::detail::is_sparse requires enum type."); + + return range_size_v != count_v; +} + +template +inline constexpr bool is_sparse_v = is_sparse(); + +template > +constexpr std::size_t undex(U value) noexcept { + static_assert(is_enum_v, "magic_enum::detail::undex requires enum type."); + + if (const auto i = static_cast(value - min_v); value >= min_v && value <= max_v) { + if constexpr (is_sparse_v) { + if (const auto idx = indexes_v[i]; idx != invalid_index_v) { + return idx; + } + } else { + return i; + } + } + + return invalid_index_v; // Value out of range. +} + +template > +constexpr std::size_t endex(E value) noexcept { + static_assert(is_enum_v, "magic_enum::detail::endex requires enum type."); + + return undex(static_cast(value)); +} + +template > +constexpr U value_ors() noexcept { + static_assert(is_enum_v, "magic_enum::detail::endex requires enum type."); + + auto value = U{0}; + for (std::size_t i = 0; i < count_v; ++i) { + value |= static_cast(values_v[i]); + } + + return value; +} + +template +struct enable_if_enum {}; + +template +struct enable_if_enum { + using type = R; + using D = std::decay_t; + static_assert(supported::value, "magic_enum unsupported compiler (https://github.com/Neargye/magic_enum#compiler-compatibility)."); +}; + +template +using enable_if_enum_t = std::enable_if_t>, R>; + +template >>> +using enum_concept = T; + +template > +struct is_scoped_enum : std::false_type {}; + +template +struct is_scoped_enum : std::bool_constant>> {}; + +template > +struct is_unscoped_enum : std::false_type {}; + +template +struct is_unscoped_enum : std::bool_constant>> {}; + +template >> +struct underlying_type {}; + +template +struct underlying_type : std::underlying_type> {}; + +} // namespace magic_enum::detail + +// Checks is magic_enum supported compiler. +inline constexpr bool is_magic_enum_supported = detail::supported::value; + +template +using Enum = detail::enum_concept; + +// Checks whether T is an Unscoped enumeration type. +// Provides the member constant value which is equal to true, if T is an [Unscoped enumeration](https://en.cppreference.com/w/cpp/language/enum#Unscoped_enumeration) type. Otherwise, value is equal to false. +template +struct is_unscoped_enum : detail::is_unscoped_enum {}; + +template +inline constexpr bool is_unscoped_enum_v = is_unscoped_enum::value; + +// Checks whether T is an Scoped enumeration type. +// Provides the member constant value which is equal to true, if T is an [Scoped enumeration](https://en.cppreference.com/w/cpp/language/enum#Scoped_enumerations) type. Otherwise, value is equal to false. +template +struct is_scoped_enum : detail::is_scoped_enum {}; + +template +inline constexpr bool is_scoped_enum_v = is_scoped_enum::value; + +// If T is a complete enumeration type, provides a member typedef type that names the underlying type of T. +// Otherwise, if T is not an enumeration type, there is no member type. Otherwise (T is an incomplete enumeration type), the program is ill-formed. +template +struct underlying_type : detail::underlying_type {}; + +template +using underlying_type_t = typename underlying_type::type; + +// Returns type name of enum. +template +[[nodiscard]] constexpr auto enum_type_name() noexcept -> detail::enable_if_enum_t { + using D = std::decay_t; + constexpr string_view name = detail::type_name_v; + static_assert(name.size() > 0, "Enum type does not have a name."); + + return name; +} + +// Returns number of enum values. +template +[[nodiscard]] constexpr auto enum_count() noexcept -> detail::enable_if_enum_t { + using D = std::decay_t; + + return detail::count_v; +} + +// Returns enum value at specified index. +// No bounds checking is performed: the behavior is undefined if index >= number of enum values. +template +[[nodiscard]] constexpr auto enum_value(std::size_t index) noexcept -> detail::enable_if_enum_t> { + using D = std::decay_t; + static_assert(detail::count_v > 0, "magic_enum requires enum implementation and valid max and min."); + + if constexpr (detail::is_sparse_v) { + return assert((index < detail::count_v)), detail::values_v[index]; + } else { + return assert((index < detail::count_v)), detail::value>(index); + } +} + +// Returns std::array with enum values, sorted by enum value. +template +[[nodiscard]] constexpr auto enum_values() noexcept -> detail::enable_if_enum_t> { + using D = std::decay_t; + static_assert(detail::count_v > 0, "magic_enum requires enum implementation and valid max and min."); + + return detail::values_v; +} + +// Returns name from static storage enum variable. +// This version is much lighter on the compile times and is not restricted to the enum_range limitation. +template +[[nodiscard]] constexpr auto enum_name() noexcept -> detail::enable_if_enum_t { + using D = std::decay_t; + constexpr string_view name = detail::enum_name_v; + static_assert(name.size() > 0, "Enum value does not have a name."); + + return name; +} + +// Returns name from enum value. +// If enum value does not have name or value out of range, returns empty string. +template +[[nodiscard]] constexpr auto enum_name(E value) noexcept -> detail::enable_if_enum_t { + using D = std::decay_t; + + if (const auto i = detail::endex(value); i != detail::invalid_index_v) { + return detail::names_v[i]; + } + + return {}; // Invalid value or out of range. +} + +// Returns std::array with names, sorted by enum value. +template +[[nodiscard]] constexpr auto enum_names() noexcept -> detail::enable_if_enum_t> { + using D = std::decay_t; + static_assert(detail::count_v > 0, "magic_enum requires enum implementation and valid max and min."); + + return detail::names_v; +} + +// Returns std::array with pairs (value, name), sorted by enum value. +template +[[nodiscard]] constexpr auto enum_entries() noexcept -> detail::enable_if_enum_t> { + using D = std::decay_t; + static_assert(detail::count_v > 0, "magic_enum requires enum implementation and valid max and min."); + + return detail::entries_v; +} + +// Obtains enum value from integer value. +// Returns optional with enum value. +template +[[nodiscard]] constexpr auto enum_cast(underlying_type_t value) noexcept -> detail::enable_if_enum_t>> { + using D = std::decay_t; + + if (detail::undex(value) != detail::invalid_index_v) { + return static_cast(value); + } + + return {}; // Invalid value or out of range. +} + +// Obtains enum value from name. +// Returns optional with enum value. +template +[[nodiscard]] constexpr auto enum_cast(string_view value, BinaryPredicate p) noexcept(std::is_nothrow_invocable_r_v) -> detail::enable_if_enum_t>> { + static_assert(std::is_invocable_r_v, "magic_enum::enum_cast requires bool(char, char) invocable predicate."); + using D = std::decay_t; + + for (std::size_t i = 0; i < detail::count_v; ++i) { + if (detail::cmp_equal(value, detail::names_v[i], p)) { + return enum_value(i); + } + } + + return {}; // Invalid value or out of range. +} + +// Obtains enum value from name. +// Returns optional with enum value. +template +[[nodiscard]] constexpr auto enum_cast(string_view value) noexcept -> detail::enable_if_enum_t>> { + using D = std::decay_t; + + return enum_cast(value, detail::char_equal_to{}); +} + +// Returns integer value from enum value. +template +[[nodiscard]] constexpr auto enum_integer(E value) noexcept -> detail::enable_if_enum_t> { + return static_cast>(value); +} + +// Obtains index in enum values from enum value. +// Returns optional with index. +template +[[nodiscard]] constexpr auto enum_index(E value) noexcept -> detail::enable_if_enum_t> { + using D = std::decay_t; + + if (const auto i = detail::endex(value); i != detail::invalid_index_v) { + return i; + } + + return {}; // Invalid value or out of range. +} + +// Checks whether enum contains enumerator with such enum value. +template +[[nodiscard]] constexpr auto enum_contains(E value) noexcept -> detail::enable_if_enum_t { + using D = std::decay_t; + + return detail::endex(value) != detail::invalid_index_v; +} + +// Checks whether enum contains enumerator with such integer value. +template +[[nodiscard]] constexpr auto enum_contains(underlying_type_t value) noexcept -> detail::enable_if_enum_t { + using D = std::decay_t; + + return detail::undex(value) != detail::invalid_index_v; +} + +// Checks whether enum contains enumerator with such name. +template +[[nodiscard]] constexpr auto enum_contains(string_view value, BinaryPredicate p) noexcept(std::is_nothrow_invocable_r_v) -> detail::enable_if_enum_t { + static_assert(std::is_invocable_r_v, "magic_enum::enum_contains requires bool(char, char) invocable predicate."); + using D = std::decay_t; + + return enum_cast(value, std::move_if_noexcept(p)).has_value(); +} + +// Checks whether enum contains enumerator with such name. +template +[[nodiscard]] constexpr auto enum_contains(string_view value) noexcept -> detail::enable_if_enum_t { + using D = std::decay_t; + + return enum_cast(value).has_value(); +} + +namespace ostream_operators { + +template , int> = 0> +std::basic_ostream& operator<<(std::basic_ostream& os, E value) { + using D = std::decay_t; + using U = underlying_type_t; +#if defined(MAGIC_ENUM_SUPPORTED) && MAGIC_ENUM_SUPPORTED + if (const auto name = magic_enum::enum_name(value); !name.empty()) { + for (const auto c : name) { + os.put(c); + } + return os; + } +#endif + return (os << static_cast(value)); +} + +template , int> = 0> +std::basic_ostream& operator<<(std::basic_ostream& os, optional value) { + return value.has_value() ? (os << value.value()) : os; +} + +} // namespace magic_enum::ostream_operators + +namespace bitwise_operators { + +template , int> = 0> +constexpr E operator~(E rhs) noexcept { + return static_cast(~static_cast>(rhs)); +} + +template , int> = 0> +constexpr E operator|(E lhs, E rhs) noexcept { + return static_cast(static_cast>(lhs) | static_cast>(rhs)); +} + +template , int> = 0> +constexpr E operator&(E lhs, E rhs) noexcept { + return static_cast(static_cast>(lhs) & static_cast>(rhs)); +} + +template , int> = 0> +constexpr E operator^(E lhs, E rhs) noexcept { + return static_cast(static_cast>(lhs) ^ static_cast>(rhs)); +} + +template , int> = 0> +constexpr E& operator|=(E& lhs, E rhs) noexcept { + return lhs = (lhs | rhs); +} + +template , int> = 0> +constexpr E& operator&=(E& lhs, E rhs) noexcept { + return lhs = (lhs & rhs); +} + +template , int> = 0> +constexpr E& operator^=(E& lhs, E rhs) noexcept { + return lhs = (lhs ^ rhs); +} + +} // namespace magic_enum::bitwise_operators + +namespace flags { + +// Returns type name of enum. +using magic_enum::enum_type_name; + +// Returns number of enum-flags values. +template +[[nodiscard]] constexpr auto enum_count() noexcept -> detail::enable_if_enum_t { + using D = std::decay_t; + + return detail::count_v; +} + +// Returns enum-flags value at specified index. +// No bounds checking is performed: the behavior is undefined if index >= number of enum-flags values. +template +[[nodiscard]] constexpr auto enum_value(std::size_t index) noexcept -> detail::enable_if_enum_t> { + using D = std::decay_t; + static_assert(detail::count_v > 0, "magic_enum::flags requires enum-flags implementation."); + + if constexpr (detail::is_sparse_v) { + return assert((index < detail::count_v)), detail::values_v[index]; + } else { + constexpr auto min = detail::log2(detail::min_v); + + return assert((index < detail::count_v)), detail::value(index); + } +} + +// Returns std::array with enum-flags values, sorted by enum-flags value. +template +[[nodiscard]] constexpr auto enum_values() noexcept -> detail::enable_if_enum_t> { + using D = std::decay_t; + static_assert(detail::count_v > 0, "magic_enum::flags requires enum-flags implementation."); + + return detail::values_v; +} + +// Returns name from enum-flags value. +// If enum-flags value does not have name or value out of range, returns empty string. +template +[[nodiscard]] auto enum_name(E value) -> detail::enable_if_enum_t { + using D = std::decay_t; + using U = underlying_type_t; + + string name; + auto check_value = U{0}; + for (std::size_t i = 0; i < detail::count_v; ++i) { + if (const auto v = static_cast(enum_value(i)); (static_cast(value) & v) != 0) { + check_value |= v; + const auto n = detail::names_v[i]; + if (!name.empty()) { + name.append(1, '|'); + } + name.append(n.data(), n.size()); + } + } + + if (check_value != 0 && check_value == static_cast(value)) { + return name; + } + + return {}; // Invalid value or out of range. +} + +// Returns std::array with string names, sorted by enum-flags value. +template +[[nodiscard]] constexpr auto enum_names() noexcept -> detail::enable_if_enum_t> { + using D = std::decay_t; + static_assert(detail::count_v > 0, "magic_enum::flags requires enum-flags implementation."); + + return detail::names_v; +} + +// Returns std::array with pairs (value, name), sorted by enum-flags value. +template +[[nodiscard]] constexpr auto enum_entries() noexcept -> detail::enable_if_enum_t> { + using D = std::decay_t; + static_assert(detail::count_v > 0, "magic_enum::flags requires enum-flags implementation."); + + return detail::entries_v; +} + +// Obtains enum-flags value from integer value. +// Returns optional with enum-flags value. +template +[[nodiscard]] constexpr auto enum_cast(underlying_type_t value) noexcept -> detail::enable_if_enum_t>> { + using D = std::decay_t; + using U = underlying_type_t; + + if constexpr (detail::is_sparse_v) { + auto check_value = U{0}; + for (std::size_t i = 0; i < detail::count_v; ++i) { + if (const auto v = static_cast(enum_value(i)); (value & v) != 0) { + check_value |= v; + } + } + + if (check_value != 0 && check_value == value) { + return static_cast(value); + } + } else { + constexpr auto min = detail::min_v; + constexpr auto max = detail::value_ors(); + + if (value >= min && value <= max) { + return static_cast(value); + } + } + + return {}; // Invalid value or out of range. +} + +// Obtains enum-flags value from name. +// Returns optional with enum-flags value. +template +[[nodiscard]] constexpr auto enum_cast(string_view value, BinaryPredicate p) noexcept(std::is_nothrow_invocable_r_v) -> detail::enable_if_enum_t>> { + static_assert(std::is_invocable_r_v, "magic_enum::flags::enum_cast requires bool(char, char) invocable predicate."); + using D = std::decay_t; + using U = underlying_type_t; + + auto result = U{0}; + while (!value.empty()) { + const auto d = detail::find(value, '|'); + const auto s = (d == string_view::npos) ? value : value.substr(0, d); + auto f = U{0}; + for (std::size_t i = 0; i < detail::count_v; ++i) { + if (detail::cmp_equal(s, detail::names_v[i], p)) { + f = static_cast(enum_value(i)); + result |= f; + break; + } + } + if (f == U{0}) { + return {}; // Invalid value or out of range. + } + value.remove_prefix((d == string_view::npos) ? value.size() : d + 1); + } + + if (result == U{0}) { + return {}; // Invalid value or out of range. + } else { + return static_cast(result); + } +} + +// Obtains enum-flags value from name. +// Returns optional with enum-flags value. +template +[[nodiscard]] constexpr auto enum_cast(string_view value) noexcept -> detail::enable_if_enum_t>> { + using D = std::decay_t; + + return enum_cast(value, detail::char_equal_to{}); +} + +// Returns integer value from enum value. +using magic_enum::enum_integer; + +// Obtains index in enum-flags values from enum-flags value. +// Returns optional with index. +template +[[nodiscard]] constexpr auto enum_index(E value) noexcept -> detail::enable_if_enum_t> { + using D = std::decay_t; + using U = underlying_type_t; + + if (detail::is_pow2(static_cast(value))) { + for (std::size_t i = 0; i < detail::count_v; ++i) { + if (enum_value(i) == value) { + return i; + } + } + } + + return {}; // Invalid value or out of range. +} + +// Checks whether enum-flags contains enumerator with such enum-flags value. +template +[[nodiscard]] constexpr auto enum_contains(E value) noexcept -> detail::enable_if_enum_t { + using D = std::decay_t; + using U = underlying_type_t; + + return enum_cast(static_cast(value)).has_value(); +} + +// Checks whether enum-flags contains enumerator with such integer value. +template +[[nodiscard]] constexpr auto enum_contains(underlying_type_t value) noexcept -> detail::enable_if_enum_t { + using D = std::decay_t; + + return enum_cast(value).has_value(); +} + +// Checks whether enum-flags contains enumerator with such name. +template +[[nodiscard]] constexpr auto enum_contains(string_view value, BinaryPredicate p) noexcept(std::is_nothrow_invocable_r_v) -> detail::enable_if_enum_t { + static_assert(std::is_invocable_r_v, "magic_enum::flags::enum_contains requires bool(char, char) invocable predicate."); + using D = std::decay_t; + + return enum_cast(value, std::move_if_noexcept(p)).has_value(); +} + +// Checks whether enum-flags contains enumerator with such name. +template +[[nodiscard]] constexpr auto enum_contains(string_view value) noexcept -> detail::enable_if_enum_t { + using D = std::decay_t; + + return enum_cast(value).has_value(); +} + +} // namespace magic_enum::flags + +namespace flags::ostream_operators { + +template , int> = 0> +std::basic_ostream& operator<<(std::basic_ostream& os, E value) { + using D = std::decay_t; + using U = underlying_type_t; +#if defined(MAGIC_ENUM_SUPPORTED) && MAGIC_ENUM_SUPPORTED + if (const auto name = magic_enum::flags::enum_name(value); !name.empty()) { + for (const auto c : name) { + os.put(c); + } + return os; + } +#endif + return (os << static_cast(value)); +} + +template , int> = 0> +std::basic_ostream& operator<<(std::basic_ostream& os, optional value) { + return value.has_value() ? (os << value.value()) : os; +} + +} // namespace magic_enum::flags::ostream_operators + +namespace flags::bitwise_operators { + +using namespace magic_enum::bitwise_operators; + +} // namespace magic_enum::flags::bitwise_operators + +} // namespace magic_enum + +#if defined(__clang__) +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#elif defined(_MSC_VER) +# pragma warning(pop) +#endif + +#endif // NEARGYE_MAGIC_ENUM_HPP diff --git a/aurora-main/lib/aurora.cpp b/aurora-main/lib/aurora.cpp new file mode 100644 index 0000000..b679e32 --- /dev/null +++ b/aurora-main/lib/aurora.cpp @@ -0,0 +1,1932 @@ +#include + +#ifdef AURORA_ENABLE_GX +#include "gfx/common.hpp" +#include "gfx/efb_ram_copy.hpp" +#include "gx/fifo.hpp" +#include "gx/shader_info.hpp" +#include "imgui.hpp" +#include "webgpu/gpu.hpp" +#include +#endif + +#include "input.hpp" +#include "internal.hpp" +#include "window.hpp" + +#include +#include +#include + +#include "system_info.hpp" +#include "tracy/Tracy.hpp" + +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef AURORA_ENABLE_GX +namespace aurora::gx { +// Producer pacing feedback for the adaptive slot count, defined in lib/gx/shader_info.cpp; +// declared here so the C entry point at the bottom of this file can forward to it. +void report_producer_paced(bool paced) noexcept; +} // namespace aurora::gx +#endif + +namespace aurora { +AuroraConfig g_config; +uint32_t g_sdlCustomEventsStart; +char g_gameName[4]; +std::atomic g_frameWorkerWaitCallback{nullptr}; +// Presentation schedule for the frame being sealed, set by the producer. Jobs carry absolute +// deadlines derived from it, so the presenter cannot drift. Zero means present when ready. +std::atomic g_presentScheduleBaseNanos{0}; +std::atomic g_presentScheduleIntervalNanos{0}; + +namespace { +Module Log("aurora"); + +std::atomic g_captureFrame{UINT32_MAX}; +std::string g_captureOutputPath; + +using PresentClock = std::chrono::steady_clock; + +struct PresentTimingSample { + PresentClock::time_point presentedAt{}; + std::chrono::nanoseconds interval{}; + // Slot carried a copy of the native image instead of replayed interpolation, so the present + // counts toward the rate but shows no new motion. + bool duplicated = false; +}; + +std::mutex g_presentTimingMutex; +std::array g_presentTimingSamples{}; +size_t g_presentTimingWriteIndex = 0; +size_t g_presentTimingSampleCount = 0; +std::optional g_lastSuccessfulPresent; +uint64_t g_totalPresentCount = 0; + +void wait_until_precise(PresentClock::time_point deadline) noexcept { + // Busy-spin tail covering the waitable timer's wakeup jitter. 150 us was not enough in + // practice; 400 us absorbs the observed overshoot, about 1.6 ms of one core at 240 Hz. + constexpr auto kSpinWindow = std::chrono::microseconds(400); + auto now = PresentClock::now(); + if (now >= deadline) { + return; + } + + const auto timerDeadline = deadline - now > kSpinWindow ? deadline - kSpinWindow : deadline; +#if defined(_WIN32) +#ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION +#define CREATE_WAITABLE_TIMER_HIGH_RESOLUTION 0x00000002 +#endif + struct HighResolutionTimer { + HANDLE handle = ::CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, + TIMER_MODIFY_STATE | SYNCHRONIZE); + ~HighResolutionTimer() { + if (handle != nullptr) { + ::CloseHandle(handle); + } + } + }; + static thread_local HighResolutionTimer timer; + if (timer.handle != nullptr && timerDeadline > now) { + const auto remaining100ns = + std::chrono::duration_cast>>( + timerDeadline - now); + LARGE_INTEGER due{}; + due.QuadPart = -std::max(remaining100ns.count(), 1); + if (::SetWaitableTimerEx(timer.handle, &due, 0, nullptr, nullptr, nullptr, 0) != FALSE) { + ::WaitForSingleObject(timer.handle, INFINITE); + } + } else +#endif + { + std::this_thread::sleep_until(timerDeadline); + } + + while (PresentClock::now() < deadline) { +#if defined(_WIN32) + YieldProcessor(); +#else + std::this_thread::yield(); +#endif + } +} + +void record_successful_present(bool, uint32_t, + std::chrono::nanoseconds, + std::chrono::nanoseconds, + std::chrono::nanoseconds, + std::chrono::nanoseconds, + std::chrono::nanoseconds, + std::chrono::nanoseconds, bool duplicated, + uint32_t, std::chrono::nanoseconds) noexcept { + const auto now = PresentClock::now(); + std::chrono::nanoseconds interval{}; + { + std::lock_guard lock(g_presentTimingMutex); + if (g_lastSuccessfulPresent) { + interval = std::chrono::duration_cast(now - *g_lastSuccessfulPresent); + } + g_lastSuccessfulPresent = now; + g_presentTimingSamples[g_presentTimingWriteIndex] = { + .presentedAt = now, + .interval = interval, + .duplicated = duplicated, + }; + g_presentTimingWriteIndex = (g_presentTimingWriteIndex + 1) % g_presentTimingSamples.size(); + g_presentTimingSampleCount = std::min(g_presentTimingSampleCount + 1, g_presentTimingSamples.size()); + ++g_totalPresentCount; + } +} + +AuroraPresentTiming snapshot_present_timing() noexcept { + constexpr auto kWindow = std::chrono::seconds(1); + const auto cutoff = PresentClock::now() - kWindow; + std::vector milliseconds; + uint64_t totalPresentCount = 0; + size_t newMotionSamples = 0; + { + std::lock_guard lock(g_presentTimingMutex); + totalPresentCount = g_totalPresentCount; + milliseconds.reserve(g_presentTimingSampleCount); + for (size_t i = 0; i < g_presentTimingSampleCount; ++i) { + const auto& sample = g_presentTimingSamples[i]; + if (sample.presentedAt >= cutoff && sample.interval.count() > 0) { + milliseconds.push_back( + std::chrono::duration(sample.interval).count()); + if (!sample.duplicated) { + ++newMotionSamples; + } + } + } + } + + AuroraPresentTiming result{ + .totalPresentCount = totalPresentCount, + .sampleCount = static_cast(milliseconds.size()), + }; + if (milliseconds.empty()) { + return result; + } + + double totalMilliseconds = 0.0; + for (const double value : milliseconds) { + totalMilliseconds += value; + } + result.averageFrameTimeMs = totalMilliseconds / static_cast(milliseconds.size()); + result.framesPerSecond = result.averageFrameTimeMs > 0.0 ? 1000.0 / result.averageFrameTimeMs : 0.0; + // Duplicated presentation slots keep the presented cadence but carry no new + // motion; scale them out so this reads as the rate the eye actually sees. + result.effectiveFramesPerSecond = + result.framesPerSecond * + (static_cast(newMotionSamples) / static_cast(milliseconds.size())); + for (const double value : milliseconds) { + const double difference = value - result.averageFrameTimeMs; + result.jitterMs += difference * difference; + } + result.jitterMs = std::sqrt(result.jitterMs / static_cast(milliseconds.size())); + std::sort(milliseconds.begin(), milliseconds.end()); + result.p95FrameTimeMs = + milliseconds[static_cast(std::floor(static_cast(milliseconds.size() - 1) * 0.95))]; + return result; +} + +// ImGui draw data lives in the shared context, so starting the next ImGui frame destroys draw +// lists the sealed slots still replay. That is why ImGui callers wait for DONE, not SEALED. +enum class ImGuiFramePolicy { + // Start ImGui's next frame as part of renderer preparation, as the + // synchronous path always has. + Immediate, + // Leave it unstarted; the caller owes an imgui::new_frame() once it has + // finished replaying this frame's draw data. + Deferred, +}; + +bool begin_frame_impl(bool pumpEvents, ImGuiFramePolicy imguiPolicy = ImGuiFramePolicy::Immediate, + bool* imguiNewFrameOwed = nullptr) noexcept; +bool begin_frame_render_state_impl(ImGuiFramePolicy imguiPolicy, bool* imguiNewFrameOwed) noexcept; +void end_frame_impl(bool pumpEvents, bool drainFifo) noexcept; + +// The two publication points of a frame-worker cycle, cleared together under `mutex`. Sealed: +// producer-shared renderer state is free again. Done: slots encoded, presented, ImGui restarted. +enum class FrameWorkerPhase { + Sealed, + Done, +}; + +struct FrameWorkerState { + std::mutex mutex; + std::condition_variable cv; + std::thread thread; + std::thread::id threadId{}; + bool started = false; + bool stop = false; + bool jobPending = false; + // Readiness is polled thousands of times per frame, so these flags double as a publication + // barrier. `sealed` is released before `ready`, and both are cleared under `mutex`. + std::atomic_bool sealed{true}; + std::atomic_bool ready{true}; + bool framePrepared = false; + bool prepareAllowed = false; +}; + +FrameWorkerState g_frameWorker; + +bool frame_worker_requested() noexcept { +#ifdef AURORA_ENABLE_GX + static const bool enabled = [] { +#if defined(_WIN32) + // RenderDoc's D3D12 layer is injected before Aurora starts and needs device and command + // ownership on one thread, so keep frame submission synchronous there. + if (::GetModuleHandleW(L"renderdoc.dll") != nullptr) { + return false; + } +#endif + return true; + }(); +#if defined(_WIN32) + static const bool renderDocLoaded = ::GetModuleHandleW(L"renderdoc.dll") != nullptr; + if (renderDocLoaded) { + static const bool logged = [] { + Log.info("Disabled asynchronous frame submission worker for RenderDoc capture"); + return true; + }(); + (void)logged; + } +#endif + return enabled; +#else + return false; +#endif +} + +#ifdef AURORA_ENABLE_GX +// Returns false when a stop request was observed mid-cycle. +bool run_frame_worker_cycle(gfx::SealedFrame& sealedFrame) noexcept; +#endif + +void frame_worker_main() noexcept { + { + std::lock_guard lock(g_frameWorker.mutex); + g_frameWorker.threadId = std::this_thread::get_id(); + } + +#ifdef AURORA_ENABLE_GX + // Owned by the worker for its whole lifetime so the sealed pass vector and + // its pooled command lists keep their capacity across frames. + gfx::SealedFrame sealedFrame; +#endif + + for (;;) { + { + std::unique_lock lock(g_frameWorker.mutex); + g_frameWorker.cv.wait(lock, [] { return g_frameWorker.stop || g_frameWorker.jobPending; }); + if (g_frameWorker.stop) { + break; + } + g_frameWorker.jobPending = false; + } + + // The CPU already decoded the sealed frame at its GX boundary; the worker only owns + // encode/submit/present, so it never touches the producer's next FIFO buffer. +#ifdef AURORA_ENABLE_GX + if (!run_frame_worker_cycle(sealedFrame)) { + break; + } +#endif + } + + // A stop request can unblock either wait above mid-cycle, so release both phases before + // stop_frame_worker() joins. + { + std::lock_guard lock(g_frameWorker.mutex); + g_frameWorker.sealed.store(true, std::memory_order_release); + g_frameWorker.ready.store(true, std::memory_order_release); + } + g_frameWorker.cv.notify_all(); +} + +void ensure_frame_worker_started() noexcept { + if (!frame_worker_requested()) { + return; + } + std::lock_guard lock(g_frameWorker.mutex); + if (g_frameWorker.started) { + return; + } + g_frameWorker.stop = false; + g_frameWorker.jobPending = false; + g_frameWorker.sealed.store(true, std::memory_order_release); + g_frameWorker.ready.store(true, std::memory_order_release); + g_frameWorker.prepareAllowed = false; + g_frameWorker.started = true; + g_frameWorker.thread = std::thread(frame_worker_main); + Log.info("Enabled bounded asynchronous frame submission worker"); +} + +bool frame_worker_phase_reached(FrameWorkerPhase phase) noexcept { + return phase == FrameWorkerPhase::Sealed ? g_frameWorker.sealed.load(std::memory_order_acquire) + : g_frameWorker.ready.load(std::memory_order_acquire); +} + +bool wait_for_frame_worker_private_for(FrameWorkerPhase phase, std::chrono::microseconds timeout) noexcept; + +void wait_for_frame_worker_private(FrameWorkerPhase phase) noexcept { + constexpr auto kWaitServiceInterval = std::chrono::milliseconds(1); + while (!wait_for_frame_worker_private_for(phase, kWaitServiceInterval)) { + } +} + +bool wait_for_frame_worker_private_for(FrameWorkerPhase phase, std::chrono::microseconds timeout) noexcept { + // Each phase's state is published before its flag is set, which keeps the common + // display-list path out of the worker control mutex. + if (frame_worker_phase_reached(phase)) { + return true; + } + + std::unique_lock lock(g_frameWorker.mutex); + if (!g_frameWorker.started || g_frameWorker.threadId == std::this_thread::get_id()) { + return true; + } + if (timeout <= std::chrono::microseconds::zero()) { + return frame_worker_phase_reached(phase); + } + + const bool reached = + g_frameWorker.cv.wait_for(lock, timeout, [phase] { return frame_worker_phase_reached(phase); }); + if (reached) { + return true; + } + + lock.unlock(); + // Both phases service the guest's alarm/retrace pump identically; the + // producer must keep its own timing alive however long it waits. + if (const auto callback = g_frameWorkerWaitCallback.load(std::memory_order_acquire)) { + callback(); + } + return false; +} + +void stop_frame_worker() noexcept { + { + std::lock_guard lock(g_frameWorker.mutex); + if (!g_frameWorker.started) { + return; + } + g_frameWorker.stop = true; + g_frameWorker.prepareAllowed = true; + } + g_frameWorker.cv.notify_all(); + if (g_frameWorker.thread.joinable()) { + g_frameWorker.thread.join(); + } + std::lock_guard lock(g_frameWorker.mutex); + g_frameWorker.started = false; + g_frameWorker.threadId = {}; + g_frameWorker.framePrepared = false; +} + +uint32_t align_to(uint32_t value, uint32_t alignment) noexcept { + return (value + alignment - 1) & ~(alignment - 1); +} + +void append_u16(std::ofstream& out, uint16_t value) { + const char bytes[] = {static_cast(value), static_cast(value >> 8)}; + out.write(bytes, sizeof(bytes)); +} + +void append_u32(std::ofstream& out, uint32_t value) { + const char bytes[] = {static_cast(value), static_cast(value >> 8), + static_cast(value >> 16), static_cast(value >> 24)}; + out.write(bytes, sizeof(bytes)); +} + +bool write_bmp(const char* path, const uint8_t* pixels, uint32_t width, uint32_t height, + uint32_t bytesPerRow, bool bgra) { + std::ofstream out(path, std::ios::binary | std::ios::trunc); + if (!out) return false; + constexpr uint32_t pixelOffset = 14 + 40; + const uint32_t imageSize = width * height * 4; + out.write("BM", 2); + append_u32(out, pixelOffset + imageSize); + append_u16(out, 0); + append_u16(out, 0); + append_u32(out, pixelOffset); + append_u32(out, 40); + append_u32(out, width); + append_u32(out, height); + append_u16(out, 1); + append_u16(out, 32); + append_u32(out, 0); + append_u32(out, imageSize); + append_u32(out, 2835); + append_u32(out, 2835); + append_u32(out, 0); + append_u32(out, 0); + std::vector row(width * 4); + for (uint32_t y = height; y-- > 0;) { + const auto* source = pixels + static_cast(y) * bytesPerRow; + if (bgra) { + out.write(reinterpret_cast(source), row.size()); + } else { + for (uint32_t x = 0; x < width; ++x) { + row[x * 4 + 0] = source[x * 4 + 2]; + row[x * 4 + 1] = source[x * 4 + 1]; + row[x * 4 + 2] = source[x * 4 + 0]; + row[x * 4 + 3] = source[x * 4 + 3]; + } + out.write(reinterpret_cast(row.data()), row.size()); + } + } + return out.good(); +} + +#ifdef AURORA_ENABLE_GX +// GPU +using webgpu::g_device; +using webgpu::g_instance; +using webgpu::g_queue; +using webgpu::g_surface; +std::recursive_mutex g_rendererGpuMutex; +std::mutex g_queueSubmitMutex; +// Surface ownership, held apart from the renderer mutex: a presentation slot touches the surface, +// its image and the queue, nothing recorded. Lock order is surface then renderer. +std::mutex g_surfaceMutex; +std::atomic g_surfaceReconfigurePending{false}; +std::atomic g_surfaceRecreatePending{false}; + +void request_surface_reconfigure() noexcept { + g_surfaceReconfigurePending.store(true, std::memory_order_release); +} + +void request_surface_recreate() noexcept { + g_surfaceRecreatePending.store(true, std::memory_order_release); + g_surfaceReconfigurePending.store(true, std::memory_order_release); +} + +struct PendingFrameCapture { + wgpu::Buffer buffer; + uint32_t width = 0; + uint32_t height = 0; + uint32_t bytesPerRow = 0; + uint64_t bufferSize = 0; + bool bgra = false; + std::string path; +}; + +std::optional encode_frame_capture(const wgpu::CommandEncoder& encoder, + const webgpu::PresentSource& source) { + const uint32_t requestedFrame = g_captureFrame.load(std::memory_order_acquire); + const uint32_t currentFrame = gfx::current_frame(); + if (requestedFrame == UINT32_MAX || currentFrame < requestedFrame) return std::nullopt; + g_captureFrame.store(UINT32_MAX, std::memory_order_release); + if (currentFrame != requestedFrame) { + Log.error("Missed requested frame capture {} (current frame {})", requestedFrame, currentFrame); + return std::nullopt; + } + if (!source.texture || source.size.width == 0 || source.size.height == 0) { + Log.error("Frame {} capture has no present-source texture", currentFrame); + return std::nullopt; + } + const bool bgra = source.format == wgpu::TextureFormat::BGRA8Unorm || + source.format == wgpu::TextureFormat::BGRA8UnormSrgb; + const bool rgba = source.format == wgpu::TextureFormat::RGBA8Unorm || + source.format == wgpu::TextureFormat::RGBA8UnormSrgb; + if (!bgra && !rgba) { + Log.error("Frame {} capture does not support texture format {}", currentFrame, + magic_enum::enum_name(source.format)); + return std::nullopt; + } + const uint32_t bytesPerRow = align_to(source.size.width * 4, 256); + const uint64_t bufferSize = static_cast(bytesPerRow) * source.size.height; + const wgpu::BufferDescriptor descriptor{ + .label = "Visual validation frame capture", + .usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead, + .size = bufferSize, + }; + auto buffer = g_device.CreateBuffer(&descriptor); + const wgpu::TexelCopyTextureInfo sourceInfo{ + .texture = source.texture, + .mipLevel = 0, + .origin = {0, 0, 0}, + .aspect = wgpu::TextureAspect::All, + }; + const wgpu::TexelCopyBufferInfo destinationInfo{ + .layout = {.offset = 0, .bytesPerRow = bytesPerRow, .rowsPerImage = source.size.height}, + .buffer = buffer, + }; + encoder.CopyTextureToBuffer(&sourceInfo, &destinationInfo, &source.size); + return PendingFrameCapture{ + .buffer = std::move(buffer), + .width = source.size.width, + .height = source.size.height, + .bytesPerRow = bytesPerRow, + .bufferSize = bufferSize, + .bgra = bgra, + .path = g_captureOutputPath, + }; +} + +void complete_frame_capture(PendingFrameCapture& capture) { + wgpu::MapAsyncStatus mapStatus = wgpu::MapAsyncStatus::CallbackCancelled; + wgpu::StringView mapMessage{}; + const auto future = capture.buffer.MapAsync( + wgpu::MapMode::Read, 0, capture.bufferSize, wgpu::CallbackMode::WaitAnyOnly, + [&mapStatus, &mapMessage](wgpu::MapAsyncStatus status, wgpu::StringView message) { + mapStatus = status; + mapMessage = message; + }); + const auto waitStatus = g_instance.WaitAny(future, 5000000000); + if (waitStatus != wgpu::WaitStatus::Success || mapStatus != wgpu::MapAsyncStatus::Success) { + Log.error("Frame capture readback failed wait={} map={} message={}", magic_enum::enum_name(waitStatus), + magic_enum::enum_name(mapStatus), mapMessage); + return; + } + const auto* pixels = static_cast(capture.buffer.GetConstMappedRange(0, capture.bufferSize)); + if (write_bmp(capture.path.c_str(), pixels, capture.width, capture.height, capture.bytesPerRow, capture.bgra)) { + Log.info("Captured rendered frame to '{}' ({}x{})", capture.path, capture.width, capture.height); + } else { + Log.error("Failed to write rendered frame capture to '{}'", capture.path); + } + capture.buffer.Unmap(); +} +#endif + +// AuroraBackend is an anonymous C typedef enum, which magic_enum cannot +// reflect under this toolchain -- name it by hand for diagnostics. +constexpr const char* backend_name(AuroraBackend backend) noexcept { + switch (backend) { + case BACKEND_AUTO: + return "Auto"; + case BACKEND_D3D11: + return "D3D11"; + case BACKEND_D3D12: + return "D3D12"; + case BACKEND_METAL: + return "Metal"; + case BACKEND_VULKAN: + return "Vulkan"; + case BACKEND_OPENGL: + return "OpenGL"; + case BACKEND_OPENGLES: + return "OpenGLES"; + case BACKEND_WEBGPU: + return "WebGPU"; + case BACKEND_NULL: + return "Null"; + } + return "Unknown"; +} + +#ifdef AURORA_ENABLE_GX +constexpr std::array PreferredBackendOrder{ +#ifdef ENABLE_BACKEND_WEBGPU + BACKEND_WEBGPU, +#endif +#ifdef DAWN_ENABLE_BACKEND_D3D12 + BACKEND_D3D12, +#endif +#if defined(_WIN32) && defined(DAWN_ENABLE_BACKEND_D3D11) + BACKEND_D3D11, +#endif +#ifdef DAWN_ENABLE_BACKEND_METAL + BACKEND_METAL, +#endif +#ifdef DAWN_ENABLE_BACKEND_VULKAN + BACKEND_VULKAN, +#endif +#if !defined(_WIN32) && defined(DAWN_ENABLE_BACKEND_D3D11) + BACKEND_D3D11, +#endif +// #ifdef DAWN_ENABLE_BACKEND_DESKTOP_GL +// BACKEND_OPENGL, +// #endif +// #ifdef DAWN_ENABLE_BACKEND_OPENGLES +// BACKEND_OPENGLES, +// #endif +#ifdef DAWN_ENABLE_BACKEND_NULL + BACKEND_NULL, +#endif +}; +#else +constexpr std::array PreferredBackendOrder{}; +#endif + +bool g_initialFrame = false; + + +AuroraInfo initialize(int argc, char* argv[], const AuroraConfig& config) noexcept { + g_config = config; + Log.info("Aurora initializing"); + log_system_information(); + if (g_config.appName == nullptr) { + g_config.appName = "Aurora"; + } else { + g_config.appName = strdup(g_config.appName); + } + if (g_config.userPath == nullptr) { + g_config.userPath = SDL_GetPrefPath(nullptr, g_config.appName); + } else { + g_config.userPath = strdup(g_config.userPath); + } + if (g_config.cachePath == nullptr) { + g_config.cachePath = SDL_GetPrefPath(nullptr, g_config.appName); + } else { + g_config.cachePath = strdup(g_config.cachePath); + } + if (g_config.resourcesPath == nullptr) { + g_config.resourcesPath = SDL_GetBasePath(); + } else { + g_config.resourcesPath = strdup(g_config.resourcesPath); + } + if (g_config.pipelineCachePath == nullptr) { + g_config.pipelineCachePath = g_config.cachePath; + } else { + g_config.pipelineCachePath = strdup(g_config.pipelineCachePath); + } + if (g_config.msaa == 0) { + g_config.msaa = 1; + } + if (g_config.maxTextureAnisotropy == 0) { + g_config.maxTextureAnisotropy = 16; + } + ASSERT(window::initialize(), "Error initializing window"); + + g_sdlCustomEventsStart = SDL_RegisterEvents(2); + ASSERT(g_sdlCustomEventsStart, "Failed to allocate user events: {}", SDL_GetError()); + ASSERT(window::initialize_event_watch(), "Error initializing SDL event watch"); + +#ifdef AURORA_ENABLE_GX + /* Attempt to create a window using the calling application's desired backend */ + const AuroraBackend requestedBackend = config.desiredBackend; + AuroraBackend selectedBackend = requestedBackend; + bool windowCreated = false; + if (selectedBackend != BACKEND_AUTO) { + Log.info("Requested graphics backend: {}", backend_name(selectedBackend)); + if (window::create_window(selectedBackend)) { + if (webgpu::initialize(selectedBackend)) { + windowCreated = true; + } else { + window::destroy_window(); + } + } else { + Log.error("Failed to create a window for backend {}: {}", backend_name(selectedBackend), + SDL_GetError()); + } + if (!windowCreated) { + /* An explicitly requested backend that cannot be brought up falls back to the BACKEND_AUTO + * search instead of aborting, and the substitution is always reported. */ + Log.error("Requested graphics backend {} is unavailable on this system; " + "falling back to automatic selection", + backend_name(requestedBackend)); + } + } + + if (!windowCreated) { + for (const auto backendType : PreferredBackendOrder) { + selectedBackend = backendType; + if (!window::create_window(selectedBackend)) { + continue; + } + if (webgpu::initialize(selectedBackend)) { + windowCreated = true; + break; + } else { + window::destroy_window(); + } + } + } + + ASSERT(windowCreated, "Error creating window: {}", SDL_GetError()); + if (requestedBackend != BACKEND_AUTO && selectedBackend != requestedBackend) { + Log.error("Graphics backend fallback in effect: video.graphics_api requested {}, " + "running on {}", + backend_name(requestedBackend), backend_name(selectedBackend)); + } + + // Initialize SDL_Renderer for ImGui when we can't use a Dawn backend + if (webgpu::g_backendType == wgpu::BackendType::Null) { + ASSERT(window::create_renderer(), "Failed to initialize SDL renderer: {}", SDL_GetError()); + } +#else + AuroraBackend selectedBackend = BACKEND_NULL; + ASSERT(window::create_window(BACKEND_NULL), "Error creating window: {}", SDL_GetError()); + ASSERT(window::create_renderer(), "Failed to initialize SDL renderer: {}", SDL_GetError()); +#endif + + window::show_window(); + +#ifdef AURORA_ENABLE_GX + gfx::initialize(); + + imgui::create_context(); +#endif + const auto size = window::get_window_size(); + Log.info("Using framebuffer size {}x{} scale {}", size.fb_width, size.fb_height, size.scale); +#ifdef AURORA_ENABLE_GX + if (g_config.imGuiInitCallback != nullptr) { + g_config.imGuiInitCallback(&size); + } + imgui::initialize(); +#endif + + g_initialFrame = true; + g_config.desiredBackend = selectedBackend; + return { + .backend = selectedBackend, + .userPath = g_config.userPath, + .cachePath = g_config.cachePath, + .window = window::get_sdl_window(), + .windowSize = size, + }; +} + +#ifdef AURORA_ENABLE_GX +struct AcquiredSurfaceTexture { + wgpu::Texture texture; + wgpu::TextureView view; +}; + +std::optional acquire_surface_texture() noexcept { + if (!window::is_presentable()) { + request_surface_reconfigure(); + return std::nullopt; + } + if (!g_surface) { + request_surface_reconfigure(); + return std::nullopt; + } + + wgpu::SurfaceTexture surfaceTexture; + g_surface.GetCurrentTexture(&surfaceTexture); + switch (surfaceTexture.status) { + case wgpu::SurfaceGetCurrentTextureStatus::SuccessOptimal: + return AcquiredSurfaceTexture{ + .texture = surfaceTexture.texture, + .view = surfaceTexture.texture.CreateView(), + }; + case wgpu::SurfaceGetCurrentTextureStatus::SuccessSuboptimal: + Log.info("Surface texture is suboptimal, deferring swapchain reconfiguration"); + request_surface_reconfigure(); + return AcquiredSurfaceTexture{ + .texture = surfaceTexture.texture, + .view = surfaceTexture.texture.CreateView(), + }; + case wgpu::SurfaceGetCurrentTextureStatus::Timeout: + Log.warn("Surface texture acquisition timed out"); + return std::nullopt; + case wgpu::SurfaceGetCurrentTextureStatus::Outdated: + Log.info("Surface texture is {}, reconfiguring swapchain", magic_enum::enum_name(surfaceTexture.status)); + request_surface_reconfigure(); + return std::nullopt; + case wgpu::SurfaceGetCurrentTextureStatus::Lost: + Log.warn("Surface texture is {}, requesting surface recreation", magic_enum::enum_name(surfaceTexture.status)); + request_surface_recreate(); + return std::nullopt; + case wgpu::SurfaceGetCurrentTextureStatus::Error: + Log.warn("Surface texture is {}, deferring surface recovery", magic_enum::enum_name(surfaceTexture.status)); + request_surface_reconfigure(); + return std::nullopt; + default: + Log.error("Failed to get surface texture: {}", magic_enum::enum_name(surfaceTexture.status)); + return std::nullopt; + } +} + +struct PresentationImage { + webgpu::TextureWithSampler texture; + wgpu::BindGroup bindGroup; +}; + +struct PresentationJob { + std::shared_ptr image; + uint32_t logicalFrame = 0; + // Absolute deadline on PresentClock stamped by the producer's schedule. + // Default (epoch) means "present as soon as ready" (no software pacing). + PresentClock::time_point presentAt{}; + bool interpolated = false; + // Slot carries a copy of the native image rather than a replayed interpolation. It counts as a + // present but not toward effectiveFramesPerSecond. + bool duplicated = false; + // Whole display periods the group slid forward at encode time (late group). + uint32_t slidPeriods = 0; +}; + +std::array>, gx::MaxInterpolatedFrames + 1> + g_presentationImagePools; + +std::shared_ptr acquire_presentation_image(size_t slot, uint32_t width, + uint32_t height) { + auto& pool = g_presentationImagePools.at(slot); + // A use count of one means only the pool holds the image, so no job can be reading it. Idle + // images from an older surface size are dropped here instead of leaking for the run. + for (auto it = pool.begin(); it != pool.end();) { + const auto& image = *it; + if (image->texture.size.width == width && image->texture.size.height == height) { + if (image.use_count() == 1) { + return image; + } + ++it; + continue; + } + if (image.use_count() == 1) { + it = pool.erase(it); + } else { + ++it; + } + } + + auto image = std::make_shared(); + image->texture = webgpu::create_render_texture(width, height, false); + image->bindGroup = webgpu::create_copy_bind_group(image->texture); + pool.emplace_back(image); + return image; +} + +bool present_presentation_job(const PresentationJob& job) { + ZoneScoped; + const auto submissionStarted = PresentClock::now(); + // Keep the threshold far above compositor and scheduling jitter. The timings below separate a + // real surface stall from a bad deadline, and only the former needs a rebuild. + constexpr auto kSurfaceStallThreshold = std::chrono::milliseconds(250); + std::chrono::nanoseconds surfaceLockDuration{}; + std::chrono::nanoseconds acquireDuration{}; + std::chrono::nanoseconds encodeDuration{}; + std::chrono::nanoseconds finishDuration{}; + std::chrono::nanoseconds submitDuration{}; + std::chrono::nanoseconds scheduleWaitDuration{}; + std::chrono::nanoseconds presentDuration{}; + bool presented = false; + if (g_surfaceReconfigurePending.load(std::memory_order_acquire) || + window::native_resize_pending() || !window::is_presentable()) { + return false; + } + std::chrono::nanoseconds lateBy{}; + if (job.presentAt != PresentClock::time_point{}) { + // How expired the deadline already is at dequeue. Positive values mean the + // slot cannot be paced and fires immediately, which is a burst symptom. + lateBy = std::chrono::duration_cast(PresentClock::now() - + job.presentAt); + } + { + window::SurfaceLock surfaceLock; + // Acquire, encode, submit and present are one unit against a configured swapchain, so the + // surface lock covers all of them. The renderer mutex is deliberately not taken. + const auto surfaceLockStarted = PresentClock::now(); + std::unique_lock surfaceOwnership(g_surfaceMutex); + surfaceLockDuration = std::chrono::duration_cast( + PresentClock::now() - surfaceLockStarted); + // Surface contention is waiting, not encoding, so keep it out of the encode timing where a + // reconfigure would look like GPU command recording. + const auto workStarted = PresentClock::now(); + bool surfaceSizeChanged = window::native_resize_pending(); + if (!surfaceSizeChanged && + !g_surfaceReconfigurePending.load(std::memory_order_acquire) && + window::is_presentable() && g_surface) { + // native_window_size_matches compares the OS client size with the configured swapchain, which is + // what native_fb_* reports. One window query instead of SDL's per-call ones. + surfaceSizeChanged = !window::native_window_size_matches( + webgpu::g_graphicsConfig.surfaceConfiguration.width, + webgpu::g_graphicsConfig.surfaceConfiguration.height); + } + if (!surfaceSizeChanged && window::is_presentable()) { + const auto acquireStarted = PresentClock::now(); + auto acquired = acquire_surface_texture(); + acquireDuration = + std::chrono::duration_cast(PresentClock::now() - acquireStarted); + if (acquired) { + const wgpu::CommandEncoderDescriptor encoderDescriptor{ + .label = "Presentation encoder", + }; + const auto encoder = g_device.CreateCommandEncoder(&encoderDescriptor); + const std::array attachments{ + wgpu::RenderPassColorAttachment{ + .view = acquired->view, + .loadOp = wgpu::LoadOp::Clear, + .storeOp = wgpu::StoreOp::Store, + }, + }; + const wgpu::RenderPassDescriptor renderPassDescriptor{ + .label = "Presentation copy pass", + .colorAttachmentCount = attachments.size(), + .colorAttachments = attachments.data(), + }; + const auto pass = encoder.BeginRenderPass(&renderPassDescriptor); + pass.SetPipeline(webgpu::g_CopyPipeline); + pass.SetBindGroup(0, job.image->bindGroup, 0, nullptr); + pass.SetViewport(0.f, 0.f, + static_cast(webgpu::g_graphicsConfig.surfaceConfiguration.width), + static_cast(webgpu::g_graphicsConfig.surfaceConfiguration.height), + 0.f, 1.f); + pass.Draw(3); + pass.End(); + const auto encodeFinished = PresentClock::now(); + encodeDuration = std::chrono::duration_cast( + encodeFinished - workStarted - acquireDuration); + const wgpu::CommandBufferDescriptor cmdBufDescriptor{ + .label = "Presentation command buffer", + }; + const auto finishStarted = PresentClock::now(); + const auto buffer = encoder.Finish(&cmdBufDescriptor); + finishDuration = std::chrono::duration_cast( + PresentClock::now() - finishStarted); + const auto submitStarted = PresentClock::now(); + { + std::lock_guard submitLock(g_queueSubmitMutex); + g_queue.Submit(1, &buffer); + } + submitDuration = std::chrono::duration_cast( + PresentClock::now() - submitStarted); + // Pace the Present() call itself, not the whole unit, so acquire/encode/submit variance stays out + // of the cadence. Holding the image across the wait is safe while the surface lock is held. + if (job.presentAt != PresentClock::time_point{}) { + const auto scheduleWaitStarted = PresentClock::now(); + wait_until_precise(job.presentAt); + scheduleWaitDuration = std::chrono::duration_cast( + PresentClock::now() - scheduleWaitStarted); + } + // A native resize can arrive after acquisition, so drop the obsolete image and let the render + // worker reconfigure at its ordered frame boundary. + if (!g_surfaceReconfigurePending.load(std::memory_order_acquire) && + !window::native_resize_pending() && window::is_presentable() && + window::native_window_size_matches( + webgpu::g_graphicsConfig.surfaceConfiguration.width, + webgpu::g_graphicsConfig.surfaceConfiguration.height)) { + const auto presentStarted = PresentClock::now(); + wgpu::Status presentStatus; + { + std::lock_guard submitLock(g_queueSubmitMutex); + presentStatus = g_surface.Present(); + } + presentDuration = std::chrono::duration_cast( + PresentClock::now() - presentStarted); + if (presentStatus == wgpu::Status::Success) { + presented = true; + record_successful_present( + job.interpolated, job.logicalFrame, acquireDuration, encodeDuration, + finishDuration, submitDuration, presentDuration, + std::chrono::duration_cast(PresentClock::now() - + submissionStarted), + job.duplicated, job.slidPeriods, lateBy); + } else { + Log.warn("Surface present failed: {}", static_cast(presentStatus)); + request_surface_reconfigure(); + } + } + acquired.reset(); + } + } + } + const auto totalDuration = + std::chrono::duration_cast(PresentClock::now() - submissionStarted); + constexpr int kStallRebuildThreshold = 3; + constexpr auto kStallRebuildCooldown = std::chrono::seconds(5); + static int s_consecutiveStalledPresents = 0; + static PresentClock::time_point s_lastStallRebuild{}; + if (totalDuration >= kSurfaceStallThreshold) { + const auto surfaceWorkDuration = + acquireDuration + encodeDuration + finishDuration + submitDuration + presentDuration; + const bool surfaceStalled = surfaceWorkDuration >= kSurfaceStallThreshold; + bool rebuildRequested = false; + if (surfaceStalled) { + ++s_consecutiveStalledPresents; + const auto now = PresentClock::now(); + if (s_consecutiveStalledPresents >= kStallRebuildThreshold && + (s_lastStallRebuild == PresentClock::time_point{} || + now - s_lastStallRebuild >= kStallRebuildCooldown)) { + s_lastStallRebuild = now; + s_consecutiveStalledPresents = 0; + rebuildRequested = true; + } + } else { + s_consecutiveStalledPresents = 0; + } + Log.warn("Presentation job took {:.1f} ms (surface lock {:.1f}, acquire {:.1f}, encode {:.1f}, " + "finish {:.1f}, submit {:.1f}, schedule wait {:.1f}, present {:.1f}){}", + std::chrono::duration(totalDuration).count(), + std::chrono::duration(surfaceLockDuration).count(), + std::chrono::duration(acquireDuration).count(), + std::chrono::duration(encodeDuration).count(), + std::chrono::duration(finishDuration).count(), + std::chrono::duration(submitDuration).count(), + std::chrono::duration(scheduleWaitDuration).count(), + std::chrono::duration(presentDuration).count(), + rebuildRequested ? "; rebuilding the surface" : ""); + if (rebuildRequested) { + request_surface_reconfigure(); + } + } else { + s_consecutiveStalledPresents = 0; + } + return presented; +} + +struct PresenterState { + std::mutex mutex; + std::condition_variable cv; + std::thread thread; + std::deque jobs; + bool started = false; + bool stop = false; + bool presenting = false; +}; + +PresenterState g_presenter; +std::atomic g_presenterStarted{false}; + +void presenter_main() noexcept { + if (!SDL_SetCurrentThreadPriority(SDL_THREAD_PRIORITY_HIGH)) { + Log.warn("Could not raise the asynchronous presenter thread priority: {}", SDL_GetError()); + } + for (;;) { + PresentationJob job; + { + std::unique_lock lock(g_presenter.mutex); + g_presenter.cv.wait(lock, [] { return g_presenter.stop || !g_presenter.jobs.empty(); }); + if (g_presenter.stop && g_presenter.jobs.empty()) { + break; + } + job = std::move(g_presenter.jobs.front()); + g_presenter.jobs.pop_front(); + g_presenter.presenting = true; + } + g_presenter.cv.notify_all(); + + present_presentation_job(job); + + { + std::lock_guard lock(g_presenter.mutex); + g_presenter.presenting = false; + } + g_presenter.cv.notify_all(); + } +} + +void ensure_presenter_started() { + std::lock_guard lock(g_presenter.mutex); + if (g_presenter.started) { + return; + } + g_presenter.stop = false; + g_presenter.presenting = false; + g_presenter.thread = std::thread(presenter_main); + g_presenter.started = true; + g_presenterStarted.store(true, std::memory_order_release); + Log.info("Enabled bounded asynchronous presentation worker"); +} + +void wait_for_presenter_idle() noexcept { + if (!g_presenterStarted.load(std::memory_order_acquire)) { + return; + } + std::unique_lock lock(g_presenter.mutex); + g_presenter.cv.wait( + lock, [] { return g_presenter.jobs.empty() && !g_presenter.presenting; }); +} + +void enqueue_presentations(std::vector&& jobs) { + ensure_presenter_started(); + // Scale the bound with the group being handed over: a 240 Hz frame enqueues four jobs at once, + // and a bound sized for two groups blocked the frame worker on a draining one. + const size_t maximumQueuedJobs = + (std::max)(static_cast(2 * (gx::MaxInterpolatedFrames + 1)), 3 * jobs.size()); + std::unique_lock lock(g_presenter.mutex); + if (g_presenter.stop) { + return; + } + if (g_presenter.jobs.size() + jobs.size() > maximumQueuedJobs) { + // Presentation is a real-time stream, not a lossless queue: waiting for room couples the guest + // and audio clocks to a blocked Present(). Keep the newest group and drop obsolete images. + const size_t dropped = g_presenter.jobs.size(); + g_presenter.jobs.clear(); + if (dropped != 0) { + // A stuck Present can keep replacement mode active for a while. Aggregate + // the warning instead of turning a driver stall into a log flood. + static size_t droppedSinceWarning = 0; + static PresentClock::time_point lastWarning{}; + droppedSinceWarning += dropped; + const auto now = PresentClock::now(); + if (lastWarning == PresentClock::time_point{} || now - lastWarning >= std::chrono::seconds(1)) { + Log.warn("Presenter fell behind; dropped {} stale presentation jobs", droppedSinceWarning); + droppedSinceWarning = 0; + lastWarning = now; + } + } + } + for (auto& job : jobs) { + g_presenter.jobs.emplace_back(std::move(job)); + } + lock.unlock(); + g_presenter.cv.notify_all(); +} + +void stop_presenter() noexcept { + if (!g_presenterStarted.load(std::memory_order_acquire)) { + return; + } + { + std::lock_guard lock(g_presenter.mutex); + g_presenter.stop = true; + } + g_presenter.cv.notify_all(); + if (g_presenter.thread.joinable()) { + g_presenter.thread.join(); + } + { + std::lock_guard lock(g_presenter.mutex); + g_presenter.jobs.clear(); + g_presenter.started = false; + g_presenter.presenting = false; + } + g_presenterStarted.store(false, std::memory_order_release); +} + +// `presentSource` is latched in the seal prologue: by the time this encodes, the producer's next +// gfx::begin_frame() may already have cleared the display-copy override. +void encode_presentation_snapshot(const wgpu::CommandEncoder& encoder, + const webgpu::PresentSource& presentSource, + const PresentationImage& image, + bool includeImGui) { + ZoneScoped; + auto viewport = webgpu::calculate_present_viewport( + image.texture.size.width, image.texture.size.height, presentSource.size.width, + presentSource.size.height); + float presentAspect = 0.f; + if (window::get_present_aspect_ratio(presentAspect)) { + viewport = webgpu::calculate_present_viewport_for_aspect( + image.texture.size.width, image.texture.size.height, presentAspect); + } + wgpu::BindGroup presentBindGroup = presentSource.bindGroup; + { + const std::array attachments{ + wgpu::RenderPassColorAttachment{ + .view = image.texture.view, + .loadOp = wgpu::LoadOp::Clear, + .storeOp = wgpu::StoreOp::Store, + }, + }; + const wgpu::RenderPassDescriptor renderPassDescriptor{ + .label = "Interpolation snapshot pass", + .colorAttachmentCount = attachments.size(), + .colorAttachments = attachments.data(), + }; + const auto pass = encoder.BeginRenderPass(&renderPassDescriptor); + pass.SetPipeline(webgpu::g_CopyPipeline); + pass.SetBindGroup(0, presentBindGroup, 0, nullptr); + pass.SetViewport(viewport.left, viewport.top, viewport.width, viewport.height, + viewport.znear, viewport.zfar); + pass.Draw(3); + pass.End(); + } + if (includeImGui) { + const std::array attachments{ + wgpu::RenderPassColorAttachment{ + .view = image.texture.view, + .loadOp = wgpu::LoadOp::Load, + .storeOp = wgpu::StoreOp::Store, + }, + }; + const wgpu::RenderPassDescriptor renderPassDescriptor{ + .label = "Snapshot ImGui pass", + .colorAttachmentCount = attachments.size(), + .colorAttachments = attachments.data(), + }; + const auto pass = encoder.BeginRenderPass(&renderPassDescriptor); + pass.SetViewport(0.f, 0.f, static_cast(image.texture.size.width), + static_cast(image.texture.size.height), 0.f, 1.f); + imgui::render(pass); + pass.End(); + } +} +#endif + +void shutdown() noexcept { + stop_frame_worker(); +#ifdef AURORA_ENABLE_GX + stop_presenter(); + g_presentationImagePools = {}; + imgui::shutdown(); + gfx::shutdown(); + webgpu::shutdown(); +#endif + input::shutdown(); + window::shutdown(); +} + +const AuroraEvent* update() noexcept { + ZoneScoped; + if (g_initialFrame) { + g_initialFrame = false; + input::initialize(); + } + return window::poll_events(); +} + +bool begin_frame_impl(bool pumpEvents, ImGuiFramePolicy imguiPolicy, bool* imguiNewFrameOwed) noexcept { + ZoneScoped; +#ifdef AURORA_ENABLE_GX + webgpu::fail_if_device_lost(); + if (pumpEvents) { + window::pump_events(); + } + const bool surfaceReconfigurePending = + g_surfaceReconfigurePending.load(std::memory_order_acquire); + const bool surfaceMutationRequired = + surfaceReconfigurePending || !window::is_presentable() || !g_surface || + window::native_resize_pending() || + !window::native_window_size_matches( + webgpu::g_graphicsConfig.surfaceConfiguration.width, + webgpu::g_graphicsConfig.surfaceConfiguration.height); + if (surfaceMutationRequired) { + wait_for_presenter_idle(); + window::SurfaceLock surfaceLock; + // Reconfiguring invalidates any image the presenter holds, so take the surface exclusively; a + // drained queue does not stop a later job. Order is surface before renderer everywhere. + std::lock_guard surfaceOwnership(g_surfaceMutex); + std::lock_guard gpuLock(g_rendererGpuMutex); + // Surface configuration belongs to the render thread; the SDL thread must not reconfigure Dawn + // while this worker is acquiring or presenting. + if (!window::is_presentable()) { + webgpu::release_surface(); + return false; + } + if (window::is_paused()) { + return false; + } + const bool consumeSurfaceReconfigure = + g_surfaceReconfigurePending.exchange(false, std::memory_order_acq_rel); + const bool consumeSurfaceRecreate = + g_surfaceRecreatePending.exchange(false, std::memory_order_acq_rel); + if (!g_surface || consumeSurfaceReconfigure) { + // Reconfigure in place unless the surface was actually lost. See + // g_surfaceRecreatePending for why destroying a live surface here is fatal + // while a capture overlay is attached. + webgpu::refresh_surface(consumeSurfaceRecreate); + if (!g_surface) { + return false; + } + } + if (window::native_resize_pending() || + !window::native_window_size_matches(webgpu::g_graphicsConfig.surfaceConfiguration.width, + webgpu::g_graphicsConfig.surfaceConfiguration.height)) { + window::sync_frame_buffer_size(); + } + } else if (window::is_paused()) { + return false; + } + + return begin_frame_render_state_impl(imguiPolicy, imguiNewFrameOwed); +#else + (void)imguiPolicy; + (void)imguiNewFrameOwed; + return true; +#endif +} + +bool begin_frame_render_state_impl(ImGuiFramePolicy imguiPolicy, bool* imguiNewFrameOwed) noexcept { +#ifdef AURORA_ENABLE_GX + std::lock_guard gpuLock(g_rendererGpuMutex); + // Note the debt before gfx::begin_frame() can fail: the synchronous path always started the + // ImGui frame here, and the runtime's retry loop depends on that pairing. + if (imguiPolicy == ImGuiFramePolicy::Immediate) { + imgui::new_frame(window::get_window_size()); + } else if (imguiNewFrameOwed != nullptr) { + *imguiNewFrameOwed = true; + } + if (!gfx::begin_frame()) { + return false; + } +#else + (void)imguiPolicy; + (void)imguiNewFrameOwed; +#endif + return true; +} + +#ifdef AURORA_ENABLE_GX +// Everything the mutex-free encode phase needs, latched while the renderer GPU mutex is held. +// None of it may be re-read from a global later; the producer has already begun the next frame. +struct SealedFrameContext { + wgpu::CommandEncoder encoder; // slot 0's encoder; already holds the staging copies + webgpu::PresentSource presentSource{}; + uint64_t scheduleBaseNanos = 0; + uint64_t scheduleIntervalNanos = 0; + uint32_t interpolatedFrameCount = 0; + uint32_t snapshotWidth = 1; + uint32_t snapshotHeight = 1; + uint32_t logicalFrame = 0; + bool interpolationActive = false; + bool replayInterpolatedFrames = false; +}; + +// Phase 1: everything that touches producer-shared renderer state. Needs g_rendererGpuMutex and +// a FIFO already drained into the recorded pass list. +void seal_frame_locked(gfx::SealedFrame& sealedFrame, SealedFrameContext& ctx) { + ZoneScopedN("Seal frame"); + const auto encoderDescriptor = wgpu::CommandEncoderDescriptor{ + .label = "Redraw encoder", + }; + ctx.encoder = g_device.CreateCommandEncoder(&encoderDescriptor); + // Probe-sized CPU-consumed copies read back asynchronously. Their downscale blits push uniforms, + // so prepare them while the producer's staging buffers are still mapped. + gfx::efb_ram::seal_async_downloads(); + gfx::end_frame(ctx.encoder); + gfx::g_stats.presentedFrameCount = 0; + gfx::g_stats.interpolatedFrameCount = 0; + // Latched before the producer's next gfx::begin_frame() calls + // gx::begin_frame_interpolation(), which resets both of these. + ctx.interpolatedFrameCount = gx::interpolated_frame_count(); + ctx.interpolationActive = ctx.interpolatedFrameCount != 0; + ctx.replayInterpolatedFrames = ctx.interpolationActive && gx::frame_interpolation_replay_safe(); + ctx.scheduleBaseNanos = g_presentScheduleBaseNanos.load(std::memory_order_acquire); + ctx.scheduleIntervalNanos = g_presentScheduleIntervalNanos.load(std::memory_order_acquire); + const auto windowSize = window::get_window_size(); + ctx.snapshotWidth = (std::max)(windowSize.native_fb_width, 1u); + ctx.snapshotHeight = (std::max)(windowSize.native_fb_height, 1u); + ctx.logicalFrame = gfx::current_frame(); + // Latched before webgpu::clear_present_source_override() in the producer's + // next gfx::begin_frame(). + ctx.presentSource = webgpu::current_present_source(); + // ImGui draw lists are built once per frame and replayed by each slot's ImGui pass, which is why + // the next ImGui frame cannot start until the encode phase is done. + imgui::render_frame_data(); + // Drop the sealed frame's lazy RAM-readback requests while the producer is still excluded; it + // starts registering the next frame's as soon as SEALED is published. + gfx::efb_ram::cancel(); + // Detach the recorded passes. From here the producer's list is empty and the + // encode phase reads only worker-private state. + gfx::seal_frame(sealedFrame); + gfx::expire_bind_group_cache(); +} + +// Phase 2: encode every presentation slot. Reads only `ctx` and the sealed passes, so it runs +// without the renderer GPU mutex while the producer records the next frame. +std::vector encode_sealed_frame(gfx::SealedFrame& sealedFrame, SealedFrameContext& ctx) { + ZoneScopedN("Encode sealed frame"); + auto encoder = std::move(ctx.encoder); + const auto encoderDescriptor = wgpu::CommandEncoderDescriptor{ + .label = "Redraw encoder", + }; + // Absolute slot deadlines: slot k of jobCount presents at base + k * interval / jobCount. With + // interpolation off, pace to the boundary that just passed plus 6.5 ms; late frames free-run. + constexpr uint64_t kNativePresentOffsetNanos = 6'500'000; + const uint32_t presentationJobCount = ctx.interpolatedFrameCount + 1; + const auto slotPresentDeadline = [&](uint32_t slot) -> PresentClock::time_point { + if (ctx.scheduleBaseNanos == 0 || ctx.scheduleIntervalNanos == 0) { + return {}; + } + if (!ctx.interpolationActive) { + return PresentClock::time_point{std::chrono::nanoseconds{ + ctx.scheduleBaseNanos - ctx.scheduleIntervalNanos + kNativePresentOffsetNanos}}; + } + const uint64_t offsetNanos = + (ctx.scheduleIntervalNanos * static_cast(slot)) / presentationJobCount; + return PresentClock::time_point{std::chrono::nanoseconds{ctx.scheduleBaseNanos + offsetNanos}}; + }; + std::vector presentationJobs; + presentationJobs.reserve(presentationJobCount); + + // Each slot is submitted as soon as it is encoded, so the GPU starts slot 0 while slot 1 is still + // recording. Queue order preserves the ordering the single batched buffer gave. + const wgpu::CommandBufferDescriptor cmdBufDescriptor{ + .label = "Presentation slot command buffer", + }; + const auto submitEncodedSlot = [&](wgpu::CommandEncoder& target) { + const auto buffer = target.Finish(&cmdBufDescriptor); + std::lock_guard submitLock(g_queueSubmitMutex); + g_queue.Submit(1, &buffer); + }; + + if (ctx.replayInterpolatedFrames) { + for (uint32_t interpolatedFrame = 0; interpolatedFrame < ctx.interpolatedFrameCount; + ++interpolatedFrame) { + gfx::render(sealedFrame, encoder, static_cast(interpolatedFrame), false); + auto image = + acquire_presentation_image(interpolatedFrame, ctx.snapshotWidth, ctx.snapshotHeight); + encode_presentation_snapshot(encoder, ctx.presentSource, *image, true); + presentationJobs.push_back({ + .image = std::move(image), + .logicalFrame = ctx.logicalFrame, + .presentAt = slotPresentDeadline(interpolatedFrame), + .interpolated = true, + }); + submitEncodedSlot(encoder); + encoder = g_device.CreateCommandEncoder(&encoderDescriptor); + } + } + + // A demanded CPU-visible EFB readback submits a prefix of the frame, so replaying the resumed + // stream would mutate an already-rendered EFB. Render once, then duplicate into the slots. + gfx::render(sealedFrame, encoder, -1, true); + // The copy targets now hold this frame's resolves, so queue their readbacks on the same encoder; + // completion is harvested in gfx::after_submit, never waited on here. + gfx::efb_ram::encode_async_downloads(encoder); + if (!ctx.replayInterpolatedFrames) { + for (uint32_t interpolatedFrame = 0; interpolatedFrame < ctx.interpolatedFrameCount; + ++interpolatedFrame) { + auto image = + acquire_presentation_image(interpolatedFrame, ctx.snapshotWidth, ctx.snapshotHeight); + encode_presentation_snapshot(encoder, ctx.presentSource, *image, true); + presentationJobs.push_back({ + .image = std::move(image), + .logicalFrame = ctx.logicalFrame, + .presentAt = slotPresentDeadline(interpolatedFrame), + .interpolated = true, + .duplicated = true, + }); + submitEncodedSlot(encoder); + encoder = g_device.CreateCommandEncoder(&encoderDescriptor); + } + } + auto finalImage = + acquire_presentation_image(ctx.interpolatedFrameCount, ctx.snapshotWidth, ctx.snapshotHeight); + encode_presentation_snapshot(encoder, ctx.presentSource, *finalImage, true); + auto pendingFrameCapture = encode_frame_capture(encoder, ctx.presentSource); + presentationJobs.push_back({ + .image = std::move(finalImage), + .logicalFrame = ctx.logicalFrame, + .presentAt = slotPresentDeadline(ctx.interpolatedFrameCount), + .interpolated = false, + }); + submitEncodedSlot(encoder); + + // A group that finished encoding past its anchor slides forward by whole display periods, never + // per slot. The cursor keeps two groups off one anchor, which bursts then holds for a period. + static PresentClock::time_point s_lastGroupAnchor{}; + if (ctx.interpolationActive && ctx.scheduleIntervalNanos != 0 && !presentationJobs.empty() && + presentationJobs.front().presentAt != PresentClock::time_point{}) { + const std::chrono::nanoseconds interval{static_cast(ctx.scheduleIntervalNanos)}; + auto anchor = presentationJobs.front().presentAt; + const auto now = PresentClock::now(); + if (now > anchor) { + const auto behind = std::chrono::duration_cast(now - anchor); + const uint64_t periods = + static_cast(behind.count()) / ctx.scheduleIntervalNanos + 1u; + anchor += std::chrono::nanoseconds{static_cast(periods * ctx.scheduleIntervalNanos)}; + } + if (s_lastGroupAnchor != PresentClock::time_point{} && anchor <= s_lastGroupAnchor) { + anchor = s_lastGroupAnchor + interval; + } + const auto shift = + std::chrono::duration_cast(anchor - presentationJobs.front().presentAt); + if (shift.count() > 0) { + const uint32_t slidPeriods = static_cast( + (static_cast(shift.count()) + ctx.scheduleIntervalNanos - 1u) / + ctx.scheduleIntervalNanos); + for (auto& job : presentationJobs) { + job.presentAt += shift; + job.slidPeriods = slidPeriods; + } + } + s_lastGroupAnchor = anchor; + } else { + // No schedule (interpolation off, boot/black presents): the grid is gone, + // so the cursor must not constrain the next scheduled group. + s_lastGroupAnchor = {}; + } + + if (pendingFrameCapture.has_value()) { + complete_frame_capture(*pendingFrameCapture); + } + gfx::after_submit(); + gfx::g_stats.presentedFrameCount = static_cast(presentationJobs.size()); + gfx::g_stats.interpolatedFrameCount = ctx.interpolatedFrameCount; + return presentationJobs; +} + +// Phase 3: hand the encoded group to whoever owns presentation. +void publish_presentations(std::vector&& presentationJobs, bool interpolationActive) { + // Keep presentation on the presenter whenever the async frame worker runs, even with + // interpolation off, so every mode shares one surface/resize path. RenderDoc keeps the sync path. + if (frame_worker_requested() || interpolationActive || + g_presenterStarted.load(std::memory_order_acquire)) { + enqueue_presentations(std::move(presentationJobs)); + } else { + for (const auto& job : presentationJobs) { + present_presentation_job(job); + } + } +} + +void record_frame_telemetry() { + TracyPlotConfig("aurora: lastVertSize", tracy::PlotFormatType::Memory, false, true, 0); + TracyPlotConfig("aurora: lastUniformSize", tracy::PlotFormatType::Memory, false, true, 0); + TracyPlotConfig("aurora: lastIndexSize", tracy::PlotFormatType::Memory, false, true, 0); + TracyPlotConfig("aurora: lastStorageSize", tracy::PlotFormatType::Memory, false, true, 0); + TracyPlotConfig("aurora: lastTextureUploadSize", tracy::PlotFormatType::Memory, false, true, 0); + + TracyPlot("aurora: queuedPipelines", static_cast(gfx::g_stats.queuedPipelines)); + TracyPlot("aurora: createdPipelines", static_cast(gfx::g_stats.createdPipelines)); + TracyPlot("aurora: drawCallCount", static_cast(gfx::g_stats.drawCallCount)); + TracyPlot("aurora: mergedDrawCallCount", static_cast(gfx::g_stats.mergedDrawCallCount)); + TracyPlot("aurora: lastVertSize", static_cast(gfx::g_stats.lastVertSize)); + TracyPlot("aurora: lastUniformSize", static_cast(gfx::g_stats.lastUniformSize)); + TracyPlot("aurora: lastIndexSize", static_cast(gfx::g_stats.lastIndexSize)); + TracyPlot("aurora: lastStorageSize", static_cast(gfx::g_stats.lastStorageSize)); + TracyPlot("aurora: lastTextureUploadSize", static_cast(gfx::g_stats.lastTextureUploadSize)); + TracyPlot("aurora: frameIndex", static_cast(gfx::current_frame())); +#if defined(TRACY_ENABLE) && defined(_WIN32) + const auto fileTimeValue = [](const FILETIME& value) noexcept { + ULARGE_INTEGER result{}; + result.LowPart = value.dwLowDateTime; + result.HighPart = value.dwHighDateTime; + return result.QuadPart; + }; + FILETIME creation{}, exit{}, processKernel{}, processUser{}, threadKernel{}, threadUser{}; + const bool processTimesAvailable = + GetProcessTimes(GetCurrentProcess(), &creation, &exit, &processKernel, &processUser) != FALSE; + const bool threadTimesAvailable = + GetThreadTimes(GetCurrentThread(), &creation, &exit, &threadKernel, &threadUser) != FALSE; + const uint64_t processCpu100ns = + processTimesAvailable ? fileTimeValue(processKernel) + fileTimeValue(processUser) : 0; + const uint64_t threadCpu100ns = + threadTimesAvailable ? fileTimeValue(threadKernel) + fileTimeValue(threadUser) : 0; + static uint64_t previousProcessCpu100ns = processCpu100ns; + static uint64_t previousThreadCpu100ns = threadCpu100ns; + TracyPlot("aurora: processCpuUsPerFrame", + static_cast((processCpu100ns - previousProcessCpu100ns) / 10)); + TracyPlot("aurora: mainThreadCpuUsPerFrame", + static_cast((threadCpu100ns - previousThreadCpu100ns) / 10)); + previousProcessCpu100ns = processCpu100ns; + previousThreadCpu100ns = threadCpu100ns; +#endif + FrameMarkNamed("Aurora frame"); +} + +// One complete frame-worker cycle. The scene encode only leaves the renderer mutex when +// interpolation actually inserts slots; otherwise both phases publish together. +bool run_frame_worker_cycle(gfx::SealedFrame& sealedFrame) noexcept { + ZoneScopedN("Frame worker cycle"); + webgpu::fail_if_device_lost(); + SealedFrameContext ctx; + std::vector presentationJobs; + bool overlapEncode = false; + { + std::lock_guard gpuLock(g_rendererGpuMutex); + seal_frame_locked(sealedFrame, ctx); + overlapEncode = ctx.interpolationActive; + if (!overlapEncode) { + presentationJobs = encode_sealed_frame(sealedFrame, ctx); + } + } + if (!overlapEncode) { + publish_presentations(std::move(presentationJobs), ctx.interpolationActive); + } + + { + std::unique_lock lock(g_frameWorker.mutex); + g_frameWorker.cv.wait(lock, [] { return g_frameWorker.stop || g_frameWorker.prepareAllowed; }); + if (g_frameWorker.stop) { + return false; + } + g_frameWorker.prepareAllowed = false; + } + + // Preparing the next frame belongs to the SEALED phase: without a fresh pass 0 and mapped + // staging buffers the producer's drain has nowhere to put its commands. + bool imguiNewFrameOwed = false; + const bool prepared = begin_frame_impl( + false, overlapEncode ? ImGuiFramePolicy::Deferred : ImGuiFramePolicy::Immediate, + &imguiNewFrameOwed); + + { + std::lock_guard lock(g_frameWorker.mutex); + g_frameWorker.framePrepared = prepared; + g_frameWorker.sealed.store(true, std::memory_order_release); + if (!overlapEncode) { + g_frameWorker.ready.store(true, std::memory_order_release); + } + } + g_frameWorker.cv.notify_all(); + + if (overlapEncode) { + // Mutex-free: the producer drains and records the next frame in parallel, taking the renderer + // mutex per drain, and this phase never takes it. + presentationJobs = encode_sealed_frame(sealedFrame, ctx); + publish_presentations(std::move(presentationJobs), ctx.interpolationActive); + if (imguiNewFrameOwed) { + // Safe only now: every slot has replayed this frame's ImGui draw lists. + std::lock_guard gpuLock(g_rendererGpuMutex); + imgui::new_frame(window::get_window_size()); + } + { + std::lock_guard lock(g_frameWorker.mutex); + g_frameWorker.ready.store(true, std::memory_order_release); + } + g_frameWorker.cv.notify_all(); + } + + record_frame_telemetry(); + return true; +} +#endif + +// Synchronous frame submission: seal, encode and present inline on the calling thread. Used when +// the frame worker is disabled (RenderDoc captures) and on the boot path. +void end_frame_impl(bool pumpEvents, bool drainFifo) noexcept { + ZoneScoped; +#ifdef AURORA_ENABLE_GX + webgpu::fail_if_device_lost(); + if (pumpEvents) { + window::pump_events(); + } + gfx::SealedFrame sealedFrame; + SealedFrameContext ctx; + std::vector presentationJobs; + { + std::lock_guard gpuLock(g_rendererGpuMutex); + if (drainFifo) { + gx::fifo::drain(); + } + seal_frame_locked(sealedFrame, ctx); + presentationJobs = encode_sealed_frame(sealedFrame, ctx); + } + publish_presentations(std::move(presentationJobs), ctx.interpolationActive); + record_frame_telemetry(); +#else + (void)pumpEvents; + (void)drainFifo; +#endif +} + +bool begin_frame() noexcept { +#ifdef AURORA_ENABLE_GX + // The asynchronous fast path below can return a logically prepared frame + // without entering begin_frame_impl(), so loss must be checked before it. + webgpu::fail_if_device_lost(); +#endif + if (!frame_worker_requested()) { + return begin_frame_impl(true); + } + + ensure_frame_worker_started(); + // SDL needs event pumping on the window-owning producer thread, and the worker passes + // pumpEvents=false, so keep it here even when the fast path returns early. + window::pump_events(); + bool waitForSurfacePreparation = false; +#ifdef AURORA_ENABLE_GX + // A surface mutation can legitimately fail preparation, and optimistic success would let GX/ImGui + // record into a frame that was never begun, so join this path and return its real result. + waitForSurfacePreparation = + !window::is_presentable() || !g_surface || + window::native_resize_pending() || window::is_paused() || + !window::native_window_size_matches( + webgpu::g_graphicsConfig.surfaceConfiguration.width, + webgpu::g_graphicsConfig.surfaceConfiguration.height); +#endif + bool workerPreparationPending = false; + { + std::lock_guard lock(g_frameWorker.mutex); + // The runtime begins right after an asynchronous end, so treat the worker's pending begin as an + // active frame unless a resize needs the real preparation result. + if (!g_frameWorker.ready.load(std::memory_order_acquire)) { + g_frameWorker.prepareAllowed = true; + g_frameWorker.cv.notify_one(); + if (!waitForSurfacePreparation) { + return true; + } + workerPreparationPending = true; + } else if (g_frameWorker.framePrepared) { + return true; + } + } + + if (workerPreparationPending) { + // DONE, not SEALED: this only runs while the window is changing and the caller is about to act on + // the surface, so keep the resize path fully serialized. + wait_for_frame_worker_private(FrameWorkerPhase::Done); + std::lock_guard lock(g_frameWorker.mutex); + return g_frameWorker.framePrepared; + } + + { + std::lock_guard lock(g_frameWorker.mutex); + if (g_frameWorker.framePrepared) { + return true; + } + } + + const bool prepared = begin_frame_impl(false); + { + std::lock_guard lock(g_frameWorker.mutex); + g_frameWorker.framePrepared = prepared; + } + return prepared; +} + +void end_frame() noexcept { +#ifdef AURORA_ENABLE_GX + webgpu::fail_if_device_lost(); +#endif + if (!frame_worker_requested()) { + end_frame_impl(true, true); + return; + } + + ensure_frame_worker_started(); + // DONE: this seals another frame, which means reusing the worker's encoder + // state and its SealedFrame. The previous cycle must be completely finished. + wait_for_frame_worker_private(FrameWorkerPhase::Done); + + // Seal all current GX work on the CPU while the renderer is known ready. + // Later FIFO writes belong exclusively to the next frame. + { + std::lock_guard gpuLock(g_rendererGpuMutex); + gx::fifo::drain(); + } + { + std::lock_guard lock(g_frameWorker.mutex); + g_frameWorker.framePrepared = false; + g_frameWorker.sealed.store(false, std::memory_order_release); + g_frameWorker.ready.store(false, std::memory_order_release); + g_frameWorker.jobPending = true; + g_frameWorker.prepareAllowed = false; + } + g_frameWorker.cv.notify_one(); +} +} // namespace + +void wait_for_frame_worker() noexcept { wait_for_frame_worker_private(FrameWorkerPhase::Done); } +std::chrono::nanoseconds wait_for_frame_worker_sealed() noexcept { + if (g_frameWorker.sealed.load(std::memory_order_acquire)) { + return std::chrono::nanoseconds::zero(); + } + const auto started = std::chrono::steady_clock::now(); + wait_for_frame_worker_private(FrameWorkerPhase::Sealed); + return std::chrono::duration_cast(std::chrono::steady_clock::now() - started); +} +bool wait_for_frame_worker_for(std::chrono::microseconds timeout) noexcept { + return wait_for_frame_worker_private_for(FrameWorkerPhase::Done, timeout); +} +std::recursive_mutex& renderer_gpu_mutex() noexcept { return g_rendererGpuMutex; } +} // namespace aurora + +// C API bindings +AuroraInfo aurora_initialize(int argc, char* argv[], const AuroraConfig* config) { + return aurora::initialize(argc, argv, *config); +} +void aurora_shutdown() { aurora::shutdown(); } +const AuroraEvent* aurora_update() { return aurora::update(); } +bool aurora_begin_frame() { return aurora::begin_frame(); } +void aurora_end_frame() { aurora::end_frame(); } +void aurora_set_frame_worker_wait_callback(AuroraFrameWorkerWaitCallback callback) { + aurora::g_frameWorkerWaitCallback.store(callback, std::memory_order_release); +} +void aurora_wait_for_frame_worker() { aurora::wait_for_frame_worker(); } +bool aurora_wait_for_frame_worker_for(uint32_t timeoutMicros) { + return aurora::wait_for_frame_worker_for(std::chrono::microseconds(timeoutMicros)); +} +void aurora_set_present_schedule(uint64_t baseNanos, uint64_t intervalNanos) { + aurora::g_presentScheduleBaseNanos.store(baseNanos, std::memory_order_release); + aurora::g_presentScheduleIntervalNanos.store(intervalNanos, std::memory_order_release); +} +void aurora_report_producer_paced(bool paced) { +#ifdef AURORA_ENABLE_GX + aurora::gx::report_producer_paced(paced); +#else + (void)paced; +#endif +} +void aurora_get_frame_interpolation_diagnostics(AuroraFrameInterpolationDiagnostics* diagnostics) { + if (diagnostics != nullptr) { + aurora::gx::get_frame_interpolation_diagnostics(*diagnostics); + } +} + +void aurora_get_present_timing(AuroraPresentTiming* timing) { + if (timing != nullptr) { + *timing = aurora::snapshot_present_timing(); + } +} +void aurora_set_frame_interpolation_fps(uint32_t targetFps) { +#ifdef AURORA_ENABLE_GX + aurora::gx::set_frame_interpolation_fps(targetFps); +#else + (void)targetFps; +#endif +} +uint32_t aurora_get_frame_interpolation_fps() { +#ifdef AURORA_ENABLE_GX + return aurora::gx::frame_interpolation_fps(); +#else + return 0; +#endif +} +void aurora_request_frame_capture(uint32_t frame, const char* outputPath) { + aurora::g_captureOutputPath = outputPath != nullptr ? outputPath : "frame_capture.bmp"; + aurora::g_captureFrame.store(frame, std::memory_order_release); +} +bool aurora_flush_efb_copies_to_ram() { +#ifdef AURORA_ENABLE_GX + if (!aurora::gfx::efb_ram::has_pending()) { + return true; + } + if (!aurora::gfx::efb_ram::prepare_downloads()) { + return false; + } + + // This finalizes the frame still being recorded, on the producer thread, so join the whole cycle + // first: the encode phase owns the previous passes, EFB targets and image pool. + aurora::wait_for_frame_worker(); + // The renderer is about to submit a prefix of the active frame. Its resumed + // suffix cannot safely be replayed against the same mutable EFB resources. + aurora::gx::mark_frame_interpolation_replay_unsafe(); + aurora::gx::fifo::drain(); + const wgpu::CommandEncoderDescriptor encoderDescriptor{ + .label = "GX CPU-visible EFB copy encoder", + }; + auto encoder = aurora::webgpu::g_device.CreateCommandEncoder(&encoderDescriptor); + aurora::gfx::end_batch(encoder); + aurora::gfx::render(encoder); + aurora::gfx::efb_ram::encode_downloads(encoder); + const wgpu::CommandBufferDescriptor commandDescriptor{ + .label = "GX CPU-visible EFB copy command buffer", + }; + const auto commandBuffer = encoder.Finish(&commandDescriptor); + { + std::lock_guard submitLock(aurora::g_queueSubmitMutex); + aurora::webgpu::g_queue.Submit(1, &commandBuffer); + } + const bool copied = aurora::gfx::efb_ram::complete_downloads(); + aurora::gfx::after_submit(); + const bool resumed = aurora::gfx::resume_frame(); + return copied && resumed; +#else + return true; +#endif +} +bool aurora_flush_efb_copy_to_ram(void* dest) { +#ifdef AURORA_ENABLE_GX + if (dest == nullptr || !aurora::gfx::efb_ram::has_pending(dest) || + !aurora::gfx::efb_ram::prepare_downloads(dest)) { + return false; + } + + // See aurora_flush_efb_copies_to_ram: this encodes the in-progress frame on + // the producer thread, so the worker's overlapped encode has to be finished. + aurora::wait_for_frame_worker(); + // Preserve the requested output cadence by duplicating the completed native + // image instead of replaying this split frame. + aurora::gx::mark_frame_interpolation_replay_unsafe(); + aurora::gx::fifo::drain(); + const wgpu::CommandEncoderDescriptor encoderDescriptor{ + .label = "GX demanded EFB copy encoder", + }; + auto encoder = aurora::webgpu::g_device.CreateCommandEncoder(&encoderDescriptor); + aurora::gfx::end_batch(encoder); + aurora::gfx::render(encoder); + aurora::gfx::efb_ram::encode_downloads(encoder, dest); + const wgpu::CommandBufferDescriptor commandDescriptor{ + .label = "GX demanded EFB copy command buffer", + }; + const auto commandBuffer = encoder.Finish(&commandDescriptor); + { + std::lock_guard submitLock(aurora::g_queueSubmitMutex); + aurora::webgpu::g_queue.Submit(1, &commandBuffer); + } + const bool copied = aurora::gfx::efb_ram::complete_downloads(); + aurora::gfx::after_submit(); + const bool resumed = aurora::gfx::resume_frame(); + return copied && resumed; +#else + (void)dest; + return true; +#endif +} +AuroraBackend aurora_get_backend() { return aurora::g_config.desiredBackend; } +const AuroraBackend* aurora_get_available_backends(size_t* count) { + if (count != nullptr) { + *count = aurora::PreferredBackendOrder.size(); + } + return aurora::PreferredBackendOrder.data(); +} +void aurora_set_log_level(AuroraLogLevel level) { aurora::g_config.logLevel = level; } +void aurora_set_pause_on_focus_lost(bool value) { aurora::g_config.pauseOnFocusLost = value; } +void aurora_set_disable_copy_filter(bool disabled) { aurora::g_config.disableCopyFilter = disabled; } +bool aurora_get_disable_copy_filter() { return aurora::g_config.disableCopyFilter; } +void aurora_set_background_input(bool value) { + aurora::g_config.allowJoystickBackgroundEvents = value; + aurora::window::set_background_input(value); +} +void aurora_set_display_mode(AuroraDisplayMode mode) { aurora::window::set_display_mode(mode); } +AuroraDisplayMode aurora_get_display_mode() { return aurora::window::get_display_mode(); } diff --git a/aurora-main/lib/card/BlockAllocationTable.cpp b/aurora-main/lib/card/BlockAllocationTable.cpp new file mode 100644 index 0000000..95739bb --- /dev/null +++ b/aurora-main/lib/card/BlockAllocationTable.cpp @@ -0,0 +1,106 @@ +#include "BlockAllocationTable.hpp" + +#include +#include + +#include "../internal.hpp" +#include "Util.hpp" + +namespace aurora::card { +void BlockAllocationTable::swapEndian() { + m_checksum = bswap(m_checksum); + m_checksumInv = bswap(m_checksumInv); + m_updateCounter = bswap(m_updateCounter); + m_freeBlocks = bswap(m_freeBlocks); + m_lastAllocated = bswap(m_lastAllocated); + std::for_each(std::begin(m_map), std::end(m_map), [](uint16_t& val) { val = bswap(val); }); +} + +void BlockAllocationTable::updateChecksum() { + swapEndian(); + calculateChecksumBE(reinterpret_cast(raw.data() + 4), 0xFFE, &m_checksum, &m_checksumInv); + swapEndian(); +} + +bool BlockAllocationTable::valid() const { + uint16_t ckSum = 0; + uint16_t ckSumInv = 0; + const_cast(*this).swapEndian(); + calculateChecksumBE(reinterpret_cast(raw.data() + 4), 0xFFE, &ckSum, &ckSumInv); + bool res = ckSum == m_checksum && ckSumInv == m_checksumInv; + const_cast(*this).swapEndian(); + return res; +} + +BlockAllocationTable::BlockAllocationTable(uint32_t blockCount) +: m_checksum{0} +, m_checksumInv{0} +, m_updateCounter{0} +, m_freeBlocks{static_cast(blockCount - FSTBlocks)} +, m_lastAllocated{4} +, m_map{{}} { + updateChecksum(); +} + +uint16_t BlockAllocationTable::getNextBlock(uint16_t block) const { + if ((block < FSTBlocks) || (block > (BATSize - FSTBlocks))) + return 0xFFFF; + + return m_map[block - FSTBlocks]; +} + +uint16_t BlockAllocationTable::nextFreeBlock(uint16_t maxBlock, uint16_t startingBlock) const { + if (m_freeBlocks > 0) { + maxBlock = std::min(maxBlock, static_cast(BATSize)); + for (uint16_t i = startingBlock; i < maxBlock; ++i) + if (m_map[i - FSTBlocks] == 0) + return i; + + for (uint16_t i = FSTBlocks; i < startingBlock; ++i) + if (m_map[i - FSTBlocks] == 0) + return i; + } + + return 0xFFFF; +} + +bool BlockAllocationTable::clear(uint16_t first, uint16_t count) { + std::vector blocks; + while (first != 0xFFFF && first != 0) { + blocks.push_back(first); + first = getNextBlock(first); + } + if (first > 0) { + size_t length = blocks.size(); + if (length != count) + return false; + + for (size_t i = 0; i < length; ++i) + m_map[blocks.at(i) - FSTBlocks] = 0; + m_freeBlocks += count; + return true; + } + return false; +} + +uint16_t BlockAllocationTable::allocateBlocks(uint16_t count, uint16_t maxBlocks) { + uint16_t firstBlock = nextFreeBlock(maxBlocks - FSTBlocks, m_lastAllocated + 1); + uint16_t freeBlock = firstBlock; + if (freeBlock != 0xFFFF) { + uint16_t tmpCount = count; + while ((count--) > 0 && freeBlock != 0xFFFF) { + m_map[(freeBlock - FSTBlocks)] = 0xFFFF; + if (count != 0) { + m_map[(freeBlock - FSTBlocks)] = nextFreeBlock(maxBlocks - FSTBlocks, freeBlock + 1); + freeBlock = m_map[(freeBlock - FSTBlocks)]; + } + } + if (freeBlock == 0xFFFF) + return 0xFFFF; + + m_lastAllocated = freeBlock; + m_freeBlocks -= tmpCount; + } + return firstBlock; +} +} // namespace aurora::card diff --git a/aurora-main/lib/card/BlockAllocationTable.hpp b/aurora-main/lib/card/BlockAllocationTable.hpp new file mode 100644 index 0000000..56f7a40 --- /dev/null +++ b/aurora-main/lib/card/BlockAllocationTable.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +#include "Constants.hpp" + +namespace aurora::card { +class BlockAllocationTable { + friend class CardRawFile; +#pragma pack(push, 4) + union { + struct { + uint16_t m_checksum; + uint16_t m_checksumInv; + uint16_t m_updateCounter; + uint16_t m_freeBlocks; + uint16_t m_lastAllocated; + std::array m_map; + }; + std::array raw{}; + }; +#pragma pack(pop) + + void swapEndian(); + void updateChecksum(); + bool valid() const; + +public: + explicit BlockAllocationTable(uint32_t blockCount = (static_cast(ECardSize::Card2043Mb) * MbitToBlocks)); + ~BlockAllocationTable() = default; + + uint16_t getNextBlock(uint16_t block) const; + uint16_t nextFreeBlock(uint16_t maxBlock, uint16_t startingBlock) const; + bool clear(uint16_t first, uint16_t count); + uint16_t allocateBlocks(uint16_t count, uint16_t maxBlocks); + uint16_t numFreeBlocks() const { return m_freeBlocks; } +}; +} // namespace aurora::card diff --git a/aurora-main/lib/card/CardGciFolder.cpp b/aurora-main/lib/card/CardGciFolder.cpp new file mode 100644 index 0000000..2b06e2e --- /dev/null +++ b/aurora-main/lib/card/CardGciFolder.cpp @@ -0,0 +1,429 @@ +#include "CardGciFolder.hpp" + +#include +#include "../fs_helper.hpp" + +#include "Directory.hpp" +#include "FileIO.hpp" +#include "../internal.hpp" + +namespace { +aurora::Module Log("aurora::card"); +} + +namespace aurora::card { + +CardGciFolder::GciFile* CardGciFolder::getFile(uint32_t idx) { + if (m_files.size() > idx) { + auto file = &m_files[idx]; + if (file->opened) + return file; + } + return nullptr; +} + +CardGciFolder::GciFile* CardGciFolder::getFile(FileHandle& fh) { return getFile(fh.getFileNo()); } + +const CardGciFolder::GciFile* CardGciFolder::getFile(uint32_t idx) const { + if (m_files.size() > idx) { + auto file = &m_files[idx]; + if (file->opened) + return file; + } + return nullptr; +} + +const CardGciFolder::GciFile* CardGciFolder::getFile(FileHandle& fh) const { return getFile(fh.getFileNo()); } + +CardGciFolder::CardGciFolder() {} + +CardGciFolder::CardGciFolder(CardGciFolder&& other) { + m_files = std::move(other.m_files); + m_bat = std::move(other.m_bat); + m_folderPath = other.m_folderPath; + m_encoding = other.m_encoding; + + CardGciFolder::setCurrentGame(other.m_game); + CardGciFolder::setCurrentMaker(other.m_maker); +} + +CardGciFolder& CardGciFolder::operator=(CardGciFolder&& other) { return *this; } + +void CardGciFolder::InitCard(const char* game, const char* maker) { + setCurrentGame(game); + setCurrentMaker(maker); +} + +ECardResult CardGciFolder::openFile(const char* filename, FileHandle& handleOut) { + int idx = 0; + for (auto& gciFile : m_files) { + if (strcmp(filename, gciFile.file.m_filename) == 0) { + gciFile.opened = true; + if (gciFile.fileSize == 0) + gciFile.fileSize = std::filesystem::file_size(m_folderPath / gciFile.filename); + + handleOut = FileHandle(idx, 0); + return ECardResult::READY; + } + idx++; + } + + return ECardResult::NOCARD; +} + +ECardResult CardGciFolder::openFile(uint32_t fileno, FileHandle& handleOut) { + if (m_files.size() > fileno) { + auto& gciFile = m_files[fileno]; + handleOut = FileHandle(fileno, 0); + gciFile.opened = true; + if (gciFile.fileSize == 0) + gciFile.fileSize = std::filesystem::file_size(m_folderPath / gciFile.filename); + + return ECardResult::READY; + } + + return ECardResult::NOCARD; +} + +ECardResult CardGciFolder::createFile(const char* filename, size_t size, FileHandle& handleOut) { + std::string gciFilename = fmt::format("{}-{}-{}.gci", m_maker, m_game, filename); + uint16_t neededBlocks = ROUND_UP_8192(size) / BlockSize; + size_t fileSize = sizeof(File) + size; + constexpr uint16_t maxBlocks = ((uint16_t)ECardSize::Card2043Mb * MbitToBlocks) - FSTBlocks; + + std::vector fileBuf(fileSize); + + File* gciFileHeader = (File*)fileBuf.data(); + + std::memcpy(gciFileHeader->m_game, m_game, 4); + std::memcpy(gciFileHeader->m_maker, m_maker, 2); + strncpy(gciFileHeader->m_filename, filename, std::size(gciFileHeader->m_filename)); + + gciFileHeader->m_modifiedTime = static_cast(getGCTime()); + gciFileHeader->m_blockCount = neededBlocks; + gciFileHeader->m_firstBlock = m_bat.allocateBlocks(neededBlocks, maxBlocks); + + // write big endian header for dolphin compat + gciFileHeader->swapEndian(); + FileIO file(m_folderPath / gciFilename, true); + if (!file || !file.fileWrite(fileBuf.data(), fileBuf.size(), 0)) { + return ECardResult::IOERROR; + } + + gciFileHeader->swapEndian(); + m_files.push_back({*gciFileHeader, fileSize, reinterpret_cast(gciFilename.c_str()), false}); // push non-endian swapped header first + handleOut = FileHandle(m_files.size() - 1, 0); + + return ECardResult::READY; +} + +ECardResult CardGciFolder::closeFile(FileHandle& fh) { + auto file = getFile(fh); + if (file) { + file->opened = false; + return ECardResult::READY; + } + + return ECardResult::NOFILE; +} + +void CardGciFolder::deleteFile(const FileHandle& fh) { + auto file = getFile(fh.getFileNo()); + if (!file) + return; + + FileIO fileIO(m_folderPath / file->filename, true); + if (fileIO) + fileIO.deleteFile(); +} + +ECardResult CardGciFolder::deleteFile(const char* filename) { return ECardResult::NOCARD; } + +ECardResult CardGciFolder::deleteFile(uint32_t fileno) { return ECardResult::NOCARD; } + +ECardResult CardGciFolder::renameFile(const char* oldName, const char* newName) { + for (auto& gciFile : m_files) { + if (strcmp(oldName, gciFile.file.m_filename) == 0) { + strncpy(gciFile.file.m_filename, newName, std::size(gciFile.file.m_filename)); + return ECardResult::READY; + } + } + + return ECardResult::NOCARD; +} + +ECardResult CardGciFolder::fileWrite(FileHandle& fh, const void* buf, size_t size) { + auto file = getFile(fh); + if (file) { + FileIO fileIO(m_folderPath / file->filename); + if (fileIO) { + if (fileIO.fileWrite(buf, size, sizeof(File) + fh.offset)) + return ECardResult::READY; + return ECardResult::IOERROR; + } + return ECardResult::NOFILE; + } + + return ECardResult::NOCARD; +} + +ECardResult CardGciFolder::fileRead(FileHandle& fh, void* dst, size_t size) { + auto file = getFile(fh); + if (file) { + FileIO fileIO(m_folderPath / file->filename); + if (fileIO) { + if (fileIO.fileRead(dst, size, sizeof(File) + fh.offset)) + return ECardResult::READY; + return ECardResult::IOERROR; + } + return ECardResult::NOFILE; + } + + return ECardResult::NOCARD; +} + +void CardGciFolder::seek(FileHandle& fh, int32_t pos, SeekOrigin whence) { + auto file = getFile(fh); + if (file) { + switch (whence) { + case SeekOrigin::Begin: + fh.offset = pos; + break; + case SeekOrigin::Current: + fh.offset += pos; + break; + case SeekOrigin::End: + fh.offset = file->fileSize - pos; + break; + } + } +} + +ECardResult CardGciFolder::getStatus(const FileHandle& fh, CardStat& statOut) const { + return getStatus(fh.getFileNo(), statOut); +} + +ECardResult CardGciFolder::getStatus(uint32_t fileNo, CardStat& statOut) const { + auto gciFile = getFile(fileNo); + + if (!gciFile) + return ECardResult::NOFILE; + const auto file = &gciFile->file; + + std::strncpy(statOut.x0_fileName, file->m_filename, 32); + statOut.x20_length = file->m_blockCount * BlockSize; + statOut.x24_time = file->m_modifiedTime; + memmove(statOut.x28_gameName.data(), file->m_game, 4); + memmove(statOut.x2c_company.data(), file->m_maker, 4); + + statOut.x2e_bannerFormat = file->m_bannerFlags; + statOut.x30_iconAddr = file->m_iconAddress; + statOut.x34_iconFormat = file->m_iconFmt; + statOut.x36_iconSpeed = file->m_animSpeed; + statOut.x38_commentAddr = file->m_commentAddr; + + if (file->m_iconAddress == UINT32_MAX) { + statOut.x3c_offsetBanner = UINT32_MAX; + statOut.x40_offsetBannerTlut = UINT32_MAX; + statOut.x44_offsetIcon.fill(UINT32_MAX); + statOut.x64_offsetIconTlut = UINT32_MAX; + statOut.x68_offsetData = file->m_commentAddr + 64; + } else { + uint32_t cur = file->m_iconAddress; + statOut.x3c_offsetBanner = cur; + cur += BannerSize(statOut.GetBannerFormat()); + statOut.x40_offsetBannerTlut = cur; + cur += TlutSize(statOut.GetBannerFormat()); + bool palette = false; + for (size_t i = 0; i < statOut.x44_offsetIcon.size(); ++i) { + statOut.x44_offsetIcon[i] = cur; + const EImageFormat fmt = statOut.GetIconFormat(static_cast(i)); + if (fmt == EImageFormat::C8) { + palette = true; + } + cur += IconSize(fmt); + } + if (palette) { + statOut.x64_offsetIconTlut = cur; + cur += TlutSize(EImageFormat::C8); + } else + statOut.x64_offsetIconTlut = UINT32_MAX; + statOut.x68_offsetData = cur; + } + + return ECardResult::READY; +} + +ECardResult CardGciFolder::setStatus(const FileHandle& fh, const CardStat& stat) { + return setStatus(fh.getFileNo(), stat); +} + +ECardResult CardGciFolder::setStatus(uint32_t fileNo, const CardStat& stat) { + auto gciFile = getFile(fileNo); + + if (!gciFile) + return ECardResult::NOFILE; + const auto file = &gciFile->file; + + file->m_bannerFlags = stat.x2e_bannerFormat; + file->m_iconAddress = stat.x30_iconAddr; + file->m_iconFmt = stat.x34_iconFormat; + file->m_animSpeed = stat.x36_iconSpeed; + file->m_commentAddr = stat.x38_commentAddr; + + return ECardResult::READY; +} + +void CardGciFolder::setCurrentGame(const char* game) { + if (game != nullptr && std::strlen(game) == 4) { + std::memcpy(m_game, game, 4); + } +} + +const uint8_t* CardGciFolder::getCurrentGame() const { + if (std::strlen(m_game) == sizeof(m_game) - 1) { + return reinterpret_cast(m_game); + } + + return nullptr; +} + +void CardGciFolder::setCurrentMaker(const char* maker) { + if (maker != nullptr && std::strlen(maker) == 2) { + std::memcpy(m_maker, maker, 2); + } +} + +const uint8_t* CardGciFolder::getCurrentMaker() const { + if (std::strlen(m_maker) == sizeof(m_maker) - 1) { + return reinterpret_cast(m_maker); + } + + return nullptr; +} + +void CardGciFolder::getSerial(uint64_t& serial) { + serial = 0; // TODO +} + +void CardGciFolder::getChecksum(uint16_t& checksum, uint16_t& inverse) const { + // TODO + checksum = 0; + inverse = 0; +} + +void CardGciFolder::getFreeBlocks(int32_t& bytesNotUsed, int32_t& filesNotUsed) const { + bytesNotUsed = m_bat.numFreeBlocks() * BlockSize; + filesNotUsed = MaxFiles; +} + +void CardGciFolder::getEncoding(uint16_t& encoding) const { encoding = (uint16_t)m_encoding; } + +void CardGciFolder::format(ECardSlot deviceId, ECardSize size, EEncoding encoding) { + m_encoding = encoding; + + if (!std::filesystem::create_directories(m_folderPath)) { + Log.error("Failed to create directory: {}", fs_path_to_string(m_folderPath)); + } +} + +void CardGciFolder::commit() { + for (auto& gciFile : m_files) { + if (gciFile.opened) { + FileIO file(m_folderPath / gciFile.filename); + + File tempFile = gciFile.file; + tempFile.swapEndian(); + file.fileWrite(&tempFile, sizeof(File), 0); // update header + } + } +} + +bool CardGciFolder::open(const std::filesystem::path& filepath) { + m_folderPath = filepath; + + std::error_code ec; + if (!std::filesystem::exists(filepath, ec) || !std::filesystem::is_directory(filepath, ec)) { + if (ec) { + Log.warn("Failed to inspect GCI folder '{}': {}", fs_path_to_string(filepath), ec.message()); + } + return false; + } + + std::filesystem::directory_iterator it( + filepath, std::filesystem::directory_options::skip_permission_denied, ec); + if (ec) { + Log.warn("Failed to enumerate GCI folder '{}': {}", fs_path_to_string(filepath), ec.message()); + return false; + } + + const std::filesystem::directory_iterator end; + while (it != end) { + const auto path = it->path(); + const auto status = it->status(ec); + if (ec) { + Log.warn("Failed to inspect GCI folder entry '{}': {}", fs_path_to_string(path), ec.message()); + return false; + } + if (!std::filesystem::is_regular_file(status)) { + it.increment(ec); + if (ec) { + Log.warn("Failed to continue enumerating GCI folder '{}': {}", fs_path_to_string(filepath), ec.message()); + return false; + } + continue; + } + + if (path.extension() != ".gci") { + it.increment(ec); + if (ec) { + Log.warn("Failed to continue enumerating GCI folder '{}': {}", fs_path_to_string(filepath), ec.message()); + return false; + } + continue; + } + + FileIO file(path); + if (!file) { + Log.warn("Failed to open GCI file '{}'", fs_path_to_string(path)); + return false; + } + + File fileData; + if (!file.fileRead(&fileData, sizeof(File), 0)) { + Log.warn("Failed to read GCI file '{}'", fs_path_to_string(path)); + return false; + } + fileData.swapEndian(); + + m_files.push_back({fileData, file.fileSize(), path.filename().u8string(), false}); + + it.increment(ec); + if (ec) { + Log.warn("Failed to continue enumerating GCI folder '{}': {}", fs_path_to_string(filepath), ec.message()); + return false; + } + } + + return true; +} + +void CardGciFolder::close() { + m_files.clear(); + m_folderPath = ""; + m_bat = BlockAllocationTable(); +} + +static std::filesystem::path g_cardFileNameEmpty = ""; + +const std::filesystem::path& CardGciFolder::cardFilename() const { return g_cardFileNameEmpty; } + +ECardResult CardGciFolder::getError() const { return ECardResult::READY; } + +ProbeResults CardGciFolder::probeCardFile(const std::filesystem::path& filename) { + if (!std::filesystem::exists(filename) || !std::filesystem::is_directory(filename)) + return {ECardResult::NOCARD, 0, 0}; + return {ECardResult::READY, static_cast(ECardSize::Card2043Mb), BlockSize}; +} + +} // namespace aurora::card diff --git a/aurora-main/lib/card/CardGciFolder.hpp b/aurora-main/lib/card/CardGciFolder.hpp new file mode 100644 index 0000000..b74c689 --- /dev/null +++ b/aurora-main/lib/card/CardGciFolder.hpp @@ -0,0 +1,77 @@ +#pragma once +#include +#include + +#include "BlockAllocationTable.hpp" +#include "CommonData.h" +#include "File.hpp" +#include "ICard.hpp" + +namespace aurora::card { + +class CardGciFolder : public ICard { +private: + struct GciFile { + File file; + size_t fileSize; + std::u8string filename; + bool opened = false; + }; + + std::vector m_files; + std::filesystem::path m_folderPath; + BlockAllocationTable m_bat; + + EEncoding m_encoding = EEncoding::ASCII; + + char m_game[5] = {'\0'}; + char m_maker[3] = {'\0'}; + + GciFile* getFile(FileHandle& fh); + const GciFile* getFile(FileHandle& fh) const; + GciFile* getFile(uint32_t idx); + const GciFile* getFile(uint32_t idx) const; +public: + CardGciFolder(); + ~CardGciFolder() override = default; + + CardGciFolder(const CardGciFolder& other) = delete; + CardGciFolder& operator=(const CardGciFolder& other) = delete; + CardGciFolder(CardGciFolder&& other); + CardGciFolder& operator=(CardGciFolder&& other); + + void InitCard(const char* game, const char* maker) override; + + ECardResult openFile(const char* filename, FileHandle& handleOut) override; + ECardResult openFile(uint32_t fileno, FileHandle& handleOut) override; + ECardResult createFile(const char* filename, size_t size, FileHandle& handleOut) override; + ECardResult closeFile(FileHandle& fh) override; + void deleteFile(const FileHandle& fh) override; + ECardResult deleteFile(const char* filename) override; + ECardResult deleteFile(uint32_t fileno) override; + ECardResult renameFile(const char* oldName, const char* newName) override; + ECardResult fileWrite(FileHandle& fh, const void* buf, size_t size) override; + ECardResult fileRead(FileHandle& fh, void* dst, size_t size) override; + void seek(FileHandle& fh, int32_t pos, SeekOrigin whence) override; + ECardResult getStatus(const FileHandle& fh, CardStat& statOut) const override; + ECardResult getStatus(uint32_t fileNo, CardStat& statOut) const override; + ECardResult setStatus(const FileHandle& fh, const CardStat& stat) override; + ECardResult setStatus(uint32_t fileNo, const CardStat& stat) override; + void setCurrentGame(const char* game) override; + const uint8_t* getCurrentGame() const override; + void setCurrentMaker(const char* maker) override; + const uint8_t* getCurrentMaker() const override; + void getSerial(uint64_t& serial) override; + void getChecksum(uint16_t& checksum, uint16_t& inverse) const override; + void getFreeBlocks(int32_t& bytesNotUsed, int32_t& filesNotUsed) const override; + void getEncoding(uint16_t& encoding) const override; + void format(ECardSlot deviceId, ECardSize size = ECardSize::Card2043Mb, EEncoding encoding = EEncoding::ASCII) override; + void commit() override; + bool open(const std::filesystem::path& filepath) override; + void close() override; + const std::filesystem::path& cardFilename() const override; + ECardResult getError() const override; + ProbeResults probeCardFile(const std::filesystem::path& filename) override; +}; + +} diff --git a/aurora-main/lib/card/CardRawFile.cpp b/aurora-main/lib/card/CardRawFile.cpp new file mode 100644 index 0000000..f897327 --- /dev/null +++ b/aurora-main/lib/card/CardRawFile.cpp @@ -0,0 +1,817 @@ +#include "CardRawFile.hpp" + +#include +#include +#include +#include +#include + +#include "../internal.hpp" +#include "SRAM.hpp" + +namespace aurora::card { + +static void NullFileAccess() { fprintf(stderr, "Attempted to access null file\n"); } + +void CardRawFile::CardHeader::_swapEndian() { + m_formatTime = bswap(m_formatTime); + m_sramBias = bswap(m_sramBias); + m_sramLanguage = bswap(m_sramLanguage); + m_unknown = bswap(m_unknown); + m_deviceId = bswap(m_deviceId); + m_sizeMb = bswap(m_sizeMb); + m_encoding = bswap(m_encoding); + m_updateCounter = bswap(m_updateCounter); + m_checksum = bswap(m_checksum); + m_checksumInv = bswap(m_checksumInv); +} + +CardRawFile::CardRawFile() { m_ch.raw.fill(0xFF); } + +CardRawFile::CardRawFile(CardRawFile&& other) { + m_ch.raw = other.m_ch.raw; + m_filename = std::move(other.m_filename); + m_fileHandle = std::move(other.m_fileHandle); + m_dirs = std::move(other.m_dirs); + m_bats = std::move(other.m_bats); + m_currentDir = other.m_currentDir; + m_currentBat = other.m_currentBat; + + m_maxBlock = other.m_maxBlock; + std::copy(std::cbegin(other.m_game), std::cend(other.m_game), std::begin(m_game)); + std::copy(std::cbegin(other.m_maker), std::cend(other.m_maker), std::begin(m_maker)); +} + +CardRawFile& CardRawFile::operator=(CardRawFile&& other) { + close(); + + m_ch.raw = other.m_ch.raw; + m_filename = std::move(other.m_filename); + m_fileHandle = std::move(other.m_fileHandle); + m_dirs = std::move(other.m_dirs); + m_bats = std::move(other.m_bats); + m_currentDir = other.m_currentDir; + m_currentBat = other.m_currentBat; + + m_maxBlock = other.m_maxBlock; + std::copy(std::cbegin(other.m_game), std::cend(other.m_game), std::begin(m_game)); + std::copy(std::cbegin(other.m_maker), std::cend(other.m_maker), std::begin(m_maker)); + + return *this; +} + +ECardResult CardRawFile::_pumpOpen() { + if (m_opened) + return ECardResult::READY; + + if (!m_fileHandle) + return ECardResult::NOCARD; + + m_ch._swapEndian(); + m_maxBlock = m_ch.m_sizeMb * MbitToBlocks; + + m_dirs[0].swapEndian(); + m_dirs[1].swapEndian(); + m_bats[0].swapEndian(); + m_bats[1].swapEndian(); + + /* Check for data integrity, restoring valid data in case of corruption if possible */ + if (!m_dirs[0].valid() && m_dirs[1].valid()) + m_dirs[0] = m_dirs[1]; + else if (!m_dirs[1].valid() && m_dirs[0].valid()) + m_dirs[1] = m_dirs[0]; + if (!m_bats[0].valid() && m_bats[1].valid()) + m_bats[0] = m_bats[1]; + else if (!m_bats[1].valid() && m_bats[0].valid()) + m_bats[1] = m_bats[0]; + + if (m_dirs[0].data.m_updateCounter > m_dirs[1].data.m_updateCounter) + m_currentDir = 0; + else + m_currentDir = 1; + + if (m_bats[0].m_updateCounter > m_bats[1].m_updateCounter) + m_currentBat = 0; + else + m_currentBat = 1; + + m_opened = true; + + return ECardResult::READY; +} + +void CardRawFile::InitCard(const char* game, const char* maker) { + m_ch.raw.fill(0xFF); + + if (game != nullptr && std::strlen(game) == 4) { + std::memcpy(m_game, game, 4); + } + if (maker != nullptr && std::strlen(maker) == 2) { + std::memcpy(m_maker, maker, 2); + } +} + +CardRawFile::~CardRawFile() { CardRawFile::close(); } + +ECardResult CardRawFile::openFile(const char* filename, FileHandle& handleOut) { + const ECardResult openRes = _pumpOpen(); + if (openRes != ECardResult::READY) { + return openRes; + } + + handleOut = {}; + const File* const f = m_dirs[m_currentDir].getFile(m_game, m_maker, filename); + if (!f || f->m_game[0] == 0xFF) { + return ECardResult::NOFILE; + } + + const int32_t idx = m_dirs[m_currentDir].indexForFile(f); + if (idx != -1) { + handleOut = FileHandle(idx); + return ECardResult::READY; + } + + return ECardResult::FATAL_ERROR; +} + +ECardResult CardRawFile::openFile(uint32_t fileno, FileHandle& handleOut) { + ECardResult openRes = _pumpOpen(); + if (openRes != ECardResult::READY) + return openRes; + + handleOut = {}; + File* f = m_dirs[m_currentDir].getFile(fileno); + if (!f || f->m_game[0] == 0xFF) + return ECardResult::NOFILE; + handleOut = FileHandle(fileno); + return ECardResult::READY; +} + +void CardRawFile::_updateDirAndBat(const Directory& dir, const BlockAllocationTable& bat) { + m_currentDir = !m_currentDir; + Directory& updateDir = m_dirs[m_currentDir]; + updateDir = dir; + updateDir.data.m_updateCounter++; + updateDir.updateChecksum(); + + m_currentBat = !m_currentBat; + BlockAllocationTable& updateBat = m_bats[m_currentBat]; + updateBat = bat; + updateBat.m_updateCounter++; + updateBat.updateChecksum(); + + m_dirty = true; +} + +void CardRawFile::_updateChecksum() { + m_ch._swapEndian(); + calculateChecksumBE(reinterpret_cast(m_ch.raw.data()), 0xFE, &m_ch.m_checksum, &m_ch.m_checksumInv); + m_ch._swapEndian(); +} + +File* CardRawFile::_fileFromHandle(const FileHandle& fh) const { + if (!fh) { + NullFileAccess(); + return nullptr; + } + return const_cast(m_dirs[m_currentDir]).getFile(fh.idx); +} + +ECardResult CardRawFile::createFile(const char* filename, size_t size, FileHandle& handleOut) { + ECardResult openRes = _pumpOpen(); + if (openRes != ECardResult::READY) + return openRes; + + handleOut = {}; + + if (size <= 0) + return ECardResult::FATAL_ERROR; + if (strlen(filename) > 32) + return ECardResult::NAMETOOLONG; + if (m_dirs[m_currentDir].getFile(m_game, m_maker, filename)) + return ECardResult::EXIST; + uint16_t neededBlocks = ROUND_UP_8192(size) / BlockSize; + if (neededBlocks > m_bats[m_currentBat].numFreeBlocks()) + return ECardResult::INSSPACE; + if (!m_dirs[m_currentDir].hasFreeFile()) + return ECardResult::NOENT; + + Directory dir = m_dirs[m_currentDir]; + BlockAllocationTable bat = m_bats[m_currentBat]; + File* f = dir.getFirstFreeFile(m_game, m_maker, filename); + uint16_t block = bat.allocateBlocks(neededBlocks, m_maxBlock - FSTBlocks); + if (f && block != 0xFFFF) { + f->m_modifiedTime = static_cast(getGCTime()); + f->m_firstBlock = block; + f->m_blockCount = neededBlocks; + + handleOut = FileHandle(dir.indexForFile(f)); + _updateDirAndBat(dir, bat); + return ECardResult::READY; + } + + return ECardResult::FATAL_ERROR; +} + +ECardResult CardRawFile::closeFile(FileHandle& fh) { + fh.offset = 0; + return ECardResult::READY; +} + +FileHandle CardRawFile::firstFile() { + const File* const f = m_dirs[m_currentDir].getFirstNonFreeFile(0, m_game, m_maker); + + if (f == nullptr) { + return {}; + } + + return FileHandle(m_dirs[m_currentDir].indexForFile(f)); +} + +FileHandle CardRawFile::nextFile(const FileHandle& cur) { + if (!cur) { + NullFileAccess(); + return {}; + } + + const File* const next = m_dirs[m_currentDir].getFirstNonFreeFile(cur.idx + 1, m_game, m_maker); + if (next == nullptr) { + return {}; + } + + return FileHandle(m_dirs[m_currentDir].indexForFile(next)); +} + +const char* CardRawFile::getFilename(const FileHandle& fh) const { + const File* const f = _fileFromHandle(fh); + + if (f == nullptr) { + return nullptr; + } + + return f->m_filename; +} + +void CardRawFile::_deleteFile(File& f, BlockAllocationTable& bat) { + uint16_t block = f.m_firstBlock; + while (block != 0xFFFF) { + /* TODO: add a fragmentation check */ + uint16_t nextBlock = bat.getNextBlock(block); + bat.clear(block, 1); + block = nextBlock; + } + f = File(); +} + +void CardRawFile::deleteFile(const FileHandle& fh) { + if (!fh) { + NullFileAccess(); + return; + } + Directory dir = m_dirs[m_currentDir]; + BlockAllocationTable bat = m_bats[m_currentBat]; + _deleteFile(*dir.getFile(fh.idx), bat); + _updateDirAndBat(dir, bat); +} + +ECardResult CardRawFile::deleteFile(const char* filename) { + ECardResult openRes = _pumpOpen(); + if (openRes != ECardResult::READY) + return openRes; + + Directory dir = m_dirs[m_currentDir]; + File* f = dir.getFile(m_game, m_maker, filename); + if (!f) + return ECardResult::NOFILE; + + BlockAllocationTable bat = m_bats[m_currentBat]; + _deleteFile(*f, bat); + _updateDirAndBat(dir, bat); + return ECardResult::READY; +} + +ECardResult CardRawFile::deleteFile(uint32_t fileno) { + ECardResult openRes = _pumpOpen(); + if (openRes != ECardResult::READY) + return openRes; + + Directory dir = m_dirs[m_currentDir]; + File* f = dir.getFile(fileno); + if (!f) + return ECardResult::NOFILE; + + BlockAllocationTable bat = m_bats[m_currentBat]; + _deleteFile(*f, bat); + _updateDirAndBat(dir, bat); + return ECardResult::READY; +} + +ECardResult CardRawFile::renameFile(const char* oldName, const char* newName) { + const ECardResult openRes = _pumpOpen(); + if (openRes != ECardResult::READY) { + return openRes; + } + + if (std::strlen(newName) > 32) { + return ECardResult::NAMETOOLONG; + } + + Directory dir = m_dirs[m_currentDir]; + File* f = dir.getFile(m_game, m_maker, oldName); + if (f == nullptr) { + return ECardResult::NOFILE; + } + + if (File* replF = dir.getFile(m_game, m_maker, newName)) { + BlockAllocationTable bat = m_bats[m_currentBat]; + _deleteFile(*replF, bat); + std::memset(f->m_filename, 0, 32); + std::strncpy(f->m_filename, newName, 32); + _updateDirAndBat(dir, bat); + } else { + std::memset(f->m_filename, 0, 32); + std::strncpy(f->m_filename, newName, 32); + _updateDirAndBat(dir, m_bats[m_currentBat]); + } + return ECardResult::READY; +} + +ECardResult CardRawFile::fileWrite(FileHandle& fh, const void* buf, size_t size) { + ECardResult openRes = _pumpOpen(); + if (openRes != ECardResult::READY) + return openRes; + + if (!fh) { + NullFileAccess(); + return ECardResult::NOFILE; + } + File* file = m_dirs[m_currentDir].getFile(fh.idx); + if (!file) + return ECardResult::NOFILE; + + /* Block handling is a little different from cache handling, + * since each block can be in an arbitrary location we must + * first find our starting block. + */ + const uint16_t blockId = static_cast(fh.offset / BlockSize); + uint16_t block = file->m_firstBlock; + for (uint16_t i = 0; i < blockId; i++) + block = m_bats[m_currentBat].getNextBlock(block); + + const uint8_t* tmpBuf = static_cast(buf); + uint16_t curBlock = block; + uint32_t blockOffset = fh.offset % BlockSize; + size_t rem = size; + while (rem) { + if (curBlock == 0xFFFF) + return ECardResult::NOFILE; + + size_t cacheSize = rem; + if (cacheSize + blockOffset > BlockSize) + cacheSize = BlockSize - blockOffset; + uint32_t offset = (curBlock * BlockSize) + blockOffset; + if (!m_fileHandle.fileWrite(tmpBuf, cacheSize, offset)) + return ECardResult::FATAL_ERROR; + tmpBuf += cacheSize; + rem -= cacheSize; + blockOffset += cacheSize; + if (blockOffset >= BlockSize) { + curBlock = m_bats[m_currentBat].getNextBlock(curBlock); + blockOffset = 0; + } + } + fh.offset += size; + + return ECardResult::READY; +} + +ECardResult CardRawFile::fileRead(FileHandle& fh, void* dst, size_t size) { + ECardResult openRes = _pumpOpen(); + if (openRes != ECardResult::READY) + return openRes; + + if (!fh) { + NullFileAccess(); + return ECardResult::NOFILE; + } + File* file = m_dirs[m_currentDir].getFile(fh.idx); + if (!file) + return ECardResult::NOFILE; + /* Block handling is a little different from cache handling, + * since each block can be in an arbitrary location we must + * first find our starting block. + */ + const uint16_t blockId = static_cast(fh.offset / BlockSize); + uint16_t block = file->m_firstBlock; + for (uint16_t i = 0; i < blockId; i++) + block = m_bats[m_currentBat].getNextBlock(block); + + uint8_t* tmpBuf = static_cast(dst); + uint16_t curBlock = block; + uint32_t blockOffset = fh.offset % BlockSize; + size_t rem = size; + while (rem) { + if (curBlock == 0xFFFF) + return ECardResult::NOFILE; + + size_t cacheSize = rem; + if (cacheSize + blockOffset > BlockSize) + cacheSize = BlockSize - blockOffset; + uint32_t offset = (curBlock * BlockSize) + blockOffset; + if (!m_fileHandle.fileRead(tmpBuf, cacheSize, offset)) + return ECardResult::FATAL_ERROR; + tmpBuf += cacheSize; + rem -= cacheSize; + blockOffset += cacheSize; + if (blockOffset >= BlockSize) { + curBlock = m_bats[m_currentBat].getNextBlock(curBlock); + blockOffset = 0; + } + } + fh.offset += size; + + return ECardResult::READY; +} + +void CardRawFile::seek(FileHandle& fh, int32_t pos, SeekOrigin whence) { + if (!fh) { + NullFileAccess(); + return; + } + File* file = m_dirs[m_currentDir].getFile(fh.idx); + if (!file) + return; + + switch (whence) { + case SeekOrigin::Begin: + fh.offset = pos; + break; + case SeekOrigin::Current: + fh.offset += pos; + break; + case SeekOrigin::End: + fh.offset = static_cast(file->m_blockCount * BlockSize) - pos; + break; + } +} + +int32_t CardRawFile::tell(const FileHandle& fh) const { return fh.offset; } + +void CardRawFile::setPublic(const FileHandle& fh, bool pub) { + File* file = _fileFromHandle(fh); + if (!file) + return; + + if (pub) + file->m_permissions |= EPermissions::Public; + else + file->m_permissions &= ~EPermissions::Public; +} + +bool CardRawFile::isPublic(const FileHandle& fh) const { + File* file = _fileFromHandle(fh); + if (!file) + return false; + + return static_cast(file->m_permissions & EPermissions::Public); +} + +void CardRawFile::setCanCopy(const FileHandle& fh, bool copy) const { + File* file = _fileFromHandle(fh); + if (!file) + return; + + if (copy) + file->m_permissions &= ~EPermissions::NoCopy; + else + file->m_permissions |= EPermissions::NoCopy; +} + +bool CardRawFile::canCopy(const FileHandle& fh) const { + File* file = _fileFromHandle(fh); + if (!file) + return false; + + return !static_cast(file->m_permissions & EPermissions::NoCopy); +} + +void CardRawFile::setCanMove(const FileHandle& fh, bool move) { + File* file = _fileFromHandle(fh); + if (!file) + return; + + if (move) + file->m_permissions &= ~EPermissions::NoMove; + else + file->m_permissions |= EPermissions::NoMove; +} + +bool CardRawFile::canMove(const FileHandle& fh) const { + File* file = _fileFromHandle(fh); + if (!file) + return false; + + return !static_cast(file->m_permissions & EPermissions::NoMove); +} + +ECardResult CardRawFile::getStatus(const FileHandle& fh, CardStat& statOut) const { + if (!fh) { + NullFileAccess(); + return ECardResult::NOFILE; + } + return getStatus(fh.idx, statOut); +} + +ECardResult CardRawFile::getStatus(uint32_t fileNo, CardStat& statOut) const { + ECardResult openRes = const_cast(this)->_pumpOpen(); + if (openRes != ECardResult::READY) + return openRes; + + const File* file = const_cast(m_dirs[m_currentDir]).getFile(fileNo); + if (!file || file->m_game[0] == 0xFF) + return ECardResult::NOFILE; + + std::strncpy(statOut.x0_fileName, file->m_filename, 32); + statOut.x20_length = file->m_blockCount * BlockSize; + statOut.x24_time = file->m_modifiedTime; + memmove(statOut.x28_gameName.data(), file->m_game, 4); + memmove(statOut.x2c_company.data(), file->m_maker, 4); + + statOut.x2e_bannerFormat = file->m_bannerFlags; + statOut.x30_iconAddr = file->m_iconAddress; + statOut.x34_iconFormat = file->m_iconFmt; + statOut.x36_iconSpeed = file->m_animSpeed; + statOut.x38_commentAddr = file->m_commentAddr; + + if (file->m_iconAddress == UINT32_MAX) { + statOut.x3c_offsetBanner = UINT32_MAX; + statOut.x40_offsetBannerTlut = UINT32_MAX; + statOut.x44_offsetIcon.fill(UINT32_MAX); + statOut.x64_offsetIconTlut = UINT32_MAX; + statOut.x68_offsetData = file->m_commentAddr + 64; + } else { + uint32_t cur = file->m_iconAddress; + statOut.x3c_offsetBanner = cur; + cur += BannerSize(statOut.GetBannerFormat()); + statOut.x40_offsetBannerTlut = cur; + cur += TlutSize(statOut.GetBannerFormat()); + bool palette = false; + for (size_t i = 0; i < statOut.x44_offsetIcon.size(); ++i) { + statOut.x44_offsetIcon[i] = cur; + const EImageFormat fmt = statOut.GetIconFormat(static_cast(i)); + if (fmt == EImageFormat::C8) { + palette = true; + } + cur += IconSize(fmt); + } + if (palette) { + statOut.x64_offsetIconTlut = cur; + cur += TlutSize(EImageFormat::C8); + } else + statOut.x64_offsetIconTlut = UINT32_MAX; + statOut.x68_offsetData = cur; + } + + return ECardResult::READY; +} + +ECardResult CardRawFile::setStatus(const FileHandle& fh, const CardStat& stat) { + if (!fh) { + NullFileAccess(); + return ECardResult::NOFILE; + } + return setStatus(fh.idx, stat); +} + +ECardResult CardRawFile::setStatus(uint32_t fileNo, const CardStat& stat) { + ECardResult openRes = _pumpOpen(); + if (openRes != ECardResult::READY) + return openRes; + + Directory dir = m_dirs[m_currentDir]; + File* file = dir.getFile(fileNo); + if (!file || file->m_game[0] == 0xFF) + return ECardResult::NOFILE; + + file->m_bannerFlags = stat.x2e_bannerFormat; + file->m_iconAddress = stat.x30_iconAddr; + file->m_iconFmt = stat.x34_iconFormat; + file->m_animSpeed = stat.x36_iconSpeed; + file->m_commentAddr = stat.x38_commentAddr; + + _updateDirAndBat(dir, m_bats[m_currentBat]); + return ECardResult::READY; +} + +void CardRawFile::setCurrentGame(const char* game) { + if (game == nullptr) { + std::memset(m_game, 0, sizeof(m_game)); + return; + } + + constexpr size_t copy_amount = sizeof(m_game) - 1; + if (std::strlen(game) != copy_amount) { + return; + } + + std::memcpy(m_game, game, copy_amount); +} + +const uint8_t* CardRawFile::getCurrentGame() const { + if (std::strlen(m_game) == sizeof(m_game) - 1) { + return reinterpret_cast(m_game); + } + + return nullptr; +} + +void CardRawFile::setCurrentMaker(const char* maker) { + if (maker == nullptr) { + std::memset(m_maker, 0, sizeof(m_maker)); + return; + } + + constexpr size_t copy_amount = sizeof(m_maker) - 1; + if (std::strlen(maker) != copy_amount) { + return; + } + + std::memcpy(m_maker, maker, copy_amount); +} + +const uint8_t* CardRawFile::getCurrentMaker() const { + if (std::strlen(m_maker) == sizeof(m_maker) - 1) { + return reinterpret_cast(m_maker); + } + + return nullptr; +} + +void CardRawFile::getSerial(uint64_t& serial) { + m_ch._swapEndian(); + + std::array serialBuf{}; + for (size_t i = 0; i < serialBuf.size(); i++) { + serialBuf[i] = bswap(*reinterpret_cast(m_ch.raw.data() + (i * 4))); + } + serial = static_cast(serialBuf[0] ^ serialBuf[2] ^ serialBuf[4] ^ serialBuf[6]) << 32 | + (serialBuf[1] ^ serialBuf[3] ^ serialBuf[5] ^ serialBuf[7]); + + m_ch._swapEndian(); +} + +void CardRawFile::getChecksum(uint16_t& checksum, uint16_t& inverse) const { + checksum = m_ch.m_checksum; + inverse = m_ch.m_checksumInv; +} + +void CardRawFile::getFreeBlocks(int32_t& bytesNotUsed, int32_t& filesNotUsed) const { + bytesNotUsed = m_bats[m_currentBat].numFreeBlocks() * 0x2000; + filesNotUsed = m_dirs[m_currentDir].numFreeFiles(); +} + +void CardRawFile::getEncoding(uint16_t& encoding) const { encoding = m_ch.m_encoding; } + +void CardRawFile::format(ECardSlot id, ECardSize size, EEncoding encoding) { + m_ch.raw.fill(0xFF); + + uint64_t rand = static_cast(getGCTime()); + m_ch.m_formatTime = rand; + for (size_t i = 0; i < m_ch.m_serial.size(); i++) { + rand = (((rand * static_cast(0x41c64e6d)) + static_cast(0x3039)) >> 16); + m_ch.m_serial[i] = static_cast(g_SRAM.flash_id[uint32_t(id)][i] + uint32_t(rand)); + rand = (((rand * static_cast(0x41c64e6d)) + static_cast(0x3039)) >> 16); + rand &= static_cast(0x7fffULL); + } + + m_ch.m_sramBias = static_cast(bswap(g_SRAM.counter_bias)); + m_ch.m_sramLanguage = static_cast(g_SRAM.lang); + m_ch.m_unknown = 0; /* 1 works for slot A, 0 both */ + m_ch.m_deviceId = 0; + m_ch.m_sizeMb = static_cast(size); + m_maxBlock = m_ch.m_sizeMb * MbitToBlocks; + m_ch.m_encoding = static_cast(encoding); + _updateChecksum(); + m_dirs[0] = Directory(); + m_dirs[1] = m_dirs[0]; + m_bats[0] = BlockAllocationTable(static_cast(size) * MbitToBlocks); + m_bats[1] = m_bats[0]; + m_currentDir = 1; + m_currentBat = 1; + + m_fileHandle = {}; + m_fileHandle = FileIO(m_filename, true); + + if (m_fileHandle) { + const uint32_t blockCount = (static_cast(size) * MbitToBlocks) - 5; + + m_tmpCh = m_ch; + m_tmpCh._swapEndian(); + m_fileHandle.fileWrite(m_tmpCh.raw.data(), BlockSize, 0); + m_tmpDirs[0] = m_dirs[0]; + m_tmpDirs[0].swapEndian(); + m_fileHandle.fileWrite(m_tmpDirs[0].raw.data(), BlockSize, BlockSize * 1); + m_tmpDirs[1] = m_dirs[1]; + m_tmpDirs[1].swapEndian(); + m_fileHandle.fileWrite(m_tmpDirs[1].raw.data(), BlockSize, BlockSize * 2); + m_tmpBats[0] = m_bats[0]; + m_tmpBats[0].swapEndian(); + m_fileHandle.fileWrite(m_tmpBats[0].raw.data(), BlockSize, BlockSize * 3); + m_tmpBats[1] = m_bats[1]; + m_tmpBats[1].swapEndian(); + m_fileHandle.fileWrite(m_tmpBats[1].raw.data(), BlockSize, BlockSize * 4); + + std::unique_ptr dummyBlock; + dummyBlock.reset(new uint8_t[BlockSize * blockCount]); + memset(dummyBlock.get(), 0xFF, BlockSize * blockCount); + m_fileHandle.fileWrite(dummyBlock.get(), BlockSize, BlockSize * blockCount); + m_dirty = false; + } +} + +ProbeResults CardRawFile::probeCardFile(const std::filesystem::path& filename) { + if (!std::filesystem::exists(filename)) + return {ECardResult::NOCARD, 0, 0}; + return {ECardResult::READY, static_cast(std::filesystem::file_size(filename) / BlockSize) / MbitToBlocks, + BlockSize}; +} + +void CardRawFile::commit() { + if (!m_dirty) + return; + if (m_fileHandle) { + m_tmpCh = m_ch; + m_tmpCh._swapEndian(); + m_fileHandle.fileWrite(&m_tmpCh, BlockSize, 0); + m_tmpDirs[0] = m_dirs[0]; + m_tmpDirs[0].updateChecksum(); + m_tmpDirs[0].swapEndian(); + m_fileHandle.fileWrite(m_tmpDirs[0].raw.data(), BlockSize, BlockSize * 1); + m_tmpDirs[1] = m_dirs[1]; + m_tmpDirs[1].updateChecksum(); + m_tmpDirs[1].swapEndian(); + m_fileHandle.fileWrite(m_tmpDirs[1].raw.data(), BlockSize, BlockSize * 2); + m_tmpBats[0] = m_bats[0]; + m_tmpBats[0].updateChecksum(); + m_tmpBats[0].swapEndian(); + m_fileHandle.fileWrite(m_tmpBats[0].raw.data(), BlockSize, BlockSize * 3); + m_tmpBats[1] = m_bats[1]; + m_tmpBats[1].updateChecksum(); + m_tmpBats[1].swapEndian(); + m_fileHandle.fileWrite(m_tmpBats[1].raw.data(), BlockSize, BlockSize * 4); + m_dirty = false; + } +} + +bool CardRawFile::open(const std::filesystem::path& filepath) { + m_opened = false; + m_filename = filepath; + m_fileHandle = FileIO(m_filename); + if (m_fileHandle) { + if (!m_fileHandle.fileRead(m_ch.raw.data(), BlockSize, 0)) + return false; + if (!m_fileHandle.fileRead(m_dirs[0].raw.data(), BlockSize, BlockSize * 1)) + return false; + if (!m_fileHandle.fileRead(m_dirs[1].raw.data(), BlockSize, BlockSize * 2)) + return false; + if (!m_fileHandle.fileRead(m_bats[0].raw.data(), BlockSize, BlockSize * 3)) + return false; + if (!m_fileHandle.fileRead(m_bats[1].raw.data(), BlockSize, BlockSize * 4)) + return false; + return true; + } + return false; +} + +void CardRawFile::close() { + m_opened = false; + if (m_fileHandle) { + commit(); + m_fileHandle = {}; + } +} + +ECardResult CardRawFile::getError() const { + if (!m_fileHandle) + return ECardResult::NOCARD; + + ECardResult openRes = const_cast(this)->_pumpOpen(); + if (openRes != ECardResult::READY) + return openRes; + + uint16_t ckSum, ckSumInv; + const_cast(*this).m_ch._swapEndian(); + calculateChecksumBE(reinterpret_cast(m_ch.raw.data()), 0xFE, &ckSum, &ckSumInv); + bool res = ckSum == m_ch.m_checksum && ckSumInv == m_ch.m_checksumInv; + const_cast(*this).m_ch._swapEndian(); + + if (!res) + return ECardResult::BROKEN; + if (!m_dirs[0].valid() && !m_dirs[1].valid()) + return ECardResult::BROKEN; + if (!m_bats[0].valid() && !m_bats[1].valid()) + return ECardResult::BROKEN; + + return ECardResult::READY; +} +} // namespace aurora::card diff --git a/aurora-main/lib/card/CardRawFile.hpp b/aurora-main/lib/card/CardRawFile.hpp new file mode 100644 index 0000000..c13ab2c --- /dev/null +++ b/aurora-main/lib/card/CardRawFile.hpp @@ -0,0 +1,438 @@ +#pragma once + +#include +#include +#include + +#include "FileIO.hpp" +#include "BlockAllocationTable.hpp" +#include "Directory.hpp" +#include "File.hpp" +#include "Util.hpp" + +#include "CommonData.h" +#include "ICard.hpp" + +namespace aurora::card { + +class CardRawFile : public ICard { +#pragma pack(push, 4) + struct CardHeader { + union { + struct { + std::array m_serial; + uint64_t m_formatTime; + int32_t m_sramBias; + uint32_t m_sramLanguage; + uint32_t m_unknown; + uint16_t m_deviceId; /* 0 for Slot A, 1 for Slot B */ + uint16_t m_sizeMb; + uint16_t m_encoding; + std::array padding; + uint16_t m_updateCounter; + uint16_t m_checksum; + uint16_t m_checksumInv; + }; + std::array raw; + }; + void _swapEndian(); + }; +#pragma pack(pop) + + CardHeader m_ch; + CardHeader m_tmpCh; + + std::filesystem::path m_filename; + FileIO m_fileHandle; + std::array m_dirs; + std::array m_bats; + std::array m_tmpDirs; + std::array m_tmpBats; + uint8_t m_currentDir; + uint8_t m_currentBat; + + uint16_t m_maxBlock; + char m_game[5] = {'\0'}; + char m_maker[3] = {'\0'}; + + void _updateDirAndBat(const Directory& dir, const BlockAllocationTable& bat); + void _updateChecksum(); + File* _fileFromHandle(const FileHandle& fh) const; + void _deleteFile(File& f, BlockAllocationTable& bat); + + bool m_dirty = false; + bool m_opened = false; + ECardResult _pumpOpen(); + +public: + CardRawFile(); + CardRawFile(const CardRawFile& other) = delete; + CardRawFile& operator=(const CardRawFile& other) = delete; + CardRawFile(CardRawFile&& other); + CardRawFile& operator=(CardRawFile&& other); + + /** + * @brief Card + * + * @param game The game code. + * @param maker The maker code. + */ + void InitCard(const char* game = nullptr, const char* maker = nullptr) override; + + ~CardRawFile() override; + + /** + * @brief openFile + * + * @param[in] filename The name of the file to open. + * @param[out] handleOut Out reference that will contain the opened file handle. + * + * @return A result indicating the error status of the operation. + */ + ECardResult openFile(const char* filename, FileHandle& handleOut) override; + + /** + * @brief openFile + * + * @param[in] fileno The file index. + * @param[out] handleOut Out reference that will contain the opened file handle. + * + * @return A result indicating the error status of the operation. + */ + ECardResult openFile(uint32_t fileno, FileHandle& handleOut) override; + + /** + * @brief createFile + * + * @param[in] filename The name of the file to create. + * @param[out] handleOut Out reference that will contain the handle of the created file. + * + * @return A result indicating the error status of the operation. + */ + ECardResult createFile(const char* filename, size_t size, FileHandle& handleOut) override; + + /** + * @brief closeFile + * + * @param fh FileHandle to close + * + * @return READY + */ + ECardResult closeFile(FileHandle& fh) override; + + /** + * @brief firstFile + * + * @return The first file within this card instance. If + * no file is able to be found, then an invalid + * handle will be returned. + */ + FileHandle firstFile(); + + /** + * @brief nextFile + * + * @param cur The file handle indicating the current file handle. + * + * @return The next file within this card. If no file can be found, + * an invalid handle will be returned. + */ + FileHandle nextFile(const FileHandle& cur); + + /** + * @brief getFilename + * + * @param fh A valid file handle to retrieve the name of. + * + * @return Gets the name of the given file. + */ + const char* getFilename(const FileHandle& fh) const; + + /** + * @brief deleteFile + * + * @param fh File handle to delete. + */ + void deleteFile(const FileHandle& fh) override; + + /** + * @brief deleteFile + * + * @param filename The name of the file to delete. + */ + ECardResult deleteFile(const char* filename) override; + + /** + * @brief deleteFile + * + * @param fileno The file number indicating the file to delete. + */ + ECardResult deleteFile(uint32_t fileno) override; + + /** + * @brief renameFile + * + * @param oldName The old name of the file. + * @param newName The new name to assign to the file. + */ + ECardResult renameFile(const char* oldName, const char* newName) override; + + /** + * @brief write + * + * @param fh A valid file handle to write to. + * @param buf The buffer to write to the file. + * @param size The size of the given buffer. + */ + ECardResult fileWrite(FileHandle& fh, const void* buf, size_t size) override; + + /** + * @brief read + * + * @param fh A valid file handle to read from. + * @param dst A buffer to read data into. + * @param size The size of the buffer to read into. + */ + ECardResult fileRead(FileHandle& fh, void* dst, size_t size) override; + + /** + * @brief seek + * + * @param fh A valid file handle. + * @param pos The position to seek to. + * @param whence The origin to seek relative to. + */ + void seek(FileHandle& fh, int32_t pos, SeekOrigin whence) override; + + /** + * @brief Returns the current offset of the specified file. + * + * @param fh The file to retrieve the offset from. + * + * @return The offset or -1 if an invalid handle is passed. + */ + int32_t tell(const FileHandle& fh) const; + + /** + * @brief setPublic + * + * @param fh The file handle to make public. + * @param pub Whether or not this file is public (i.e. readable by any game). + */ + void setPublic(const FileHandle& fh, bool pub); + + /** + * @brief isPublic + * + * @param fh The file handle to query. + * + * @return Whether or not this file is public (i.e. readable by any game). + */ + bool isPublic(const FileHandle& fh) const; + + /** + * @brief setCanCopy + * + * @param fh The file handle to set as copyable. + * @param copy Whether or not to set the file handle as copyable. + */ + void setCanCopy(const FileHandle& fh, bool copy) const; + + /** + * @brief canCopy + * + * @param fh The file handle to query. + * + * @return Whether or not this file handle is copyable by the IPL. + */ + bool canCopy(const FileHandle& fh) const; + + /** + * @brief setCanMove + * + * @param fh The file handle to set as moveable. + * @param move Whether or not to set the file handle as movable. + */ + void setCanMove(const FileHandle& fh, bool move); + + /** + * @brief canMove + * + * @param fh The file handle to query. + * + * @return whether or not the file can be moved by the IPL. + */ + bool canMove(const FileHandle& fh) const; + + /** + * @brief getStatus + * + * @param fh Handle of requested file + * @param statOut Structure to fill with file stat + * + * @return NOFILE or READY + */ + ECardResult getStatus(const FileHandle& fh, CardStat& statOut) const override; + + /** + * @brief getStatus + * + * @param fileNo Number of requested file + * @param statOut Structure to fill with file stat + * + * @return NOFILE or READY + */ + ECardResult getStatus(uint32_t fileNo, CardStat& statOut) const override; + + /** + * @brief setStatus + * + * @param fh Handle of requested file + * @param stat Structure to access for file stat + * + * @return NOFILE or READY + */ + ECardResult setStatus(const FileHandle& fh, const CardStat& stat) override; + + /** + * @brief setStatus + * + * @param fileNo Number of requested file + * @param stat Structure to access for file stat + * + * @return NOFILE or READY + */ + ECardResult setStatus(uint32_t fileNo, const CardStat& stat) override; + +#if 0 // TODO: Async-friendly implementations + /** + * @brief Copies a file from the current Card instance to a specified Card instance + * @param fh The file to copy + * @param dest The destination Card instance + * @return True if successful, false otherwise + */ + bool copyFileTo(FileHandle& fh, CardRawFile& dest); + + /** + * @brief moveFileTo + * @param fh + * @param dest + * @return + */ + bool moveFileTo(FileHandle& fh, CardRawFile& dest); +#endif + + /** + * @brief Sets the current game, if not null any openFile requests will only return files that match this game + * + * @param game The target game id, e.g "GM8E" + * + * @sa openFile + */ + void setCurrentGame(const char* game) override; + + /** + * @brief Returns the currently selected game. + * + * @return The selected game, or nullptr + */ + const uint8_t* getCurrentGame() const override; + + /** + * @brief Sets the current maker, if not null any openFile requests will only return files that match this maker. + * + * @param maker The target maker id, e.g "01". + * + * @sa openFile + */ + void setCurrentMaker(const char* maker) override; + + /** + * @brief Returns the currently selected maker. + * + * @return The selected maker, or nullptr. + */ + const uint8_t* getCurrentMaker() const override; + + /** + * @brief Retrieves the format assigned serial. + * + * @param[out] serial Out reference that will contain the serial number. + */ + void getSerial(uint64_t& serial) override; + + /** + * @brief Retrieves the checksum values of the Card system header. + * + * @param checksum The checksum of the system header. + * @param inverse The inverse checksum of the system header. + */ + void getChecksum(uint16_t& checksum, uint16_t& inverse) const override; + + /** + * @brief Retrieves the available storage and directory space. + * + * @param bytesNotUsed Number of free bytes out. + * @param filesNotUsed Number of free files out. + */ + void getFreeBlocks(int32_t& bytesNotUsed, int32_t& filesNotUsed) const override; + + /** + * @brief Retrieves the current file's encoding. + * + * @param encoding File encoding out. + */ + void getEncoding(uint16_t& encoding) const override; + + /** + * @brief Formats the memory card and assigns a new serial. + * + * @param deviceId The slot to format. + * @param size The desired size of the file @sa ECardSize. + * @param encoding The desired encoding @sa EEncoding. + */ + void format(ECardSlot deviceId, ECardSize size = ECardSize::Card2043Mb, EEncoding encoding = EEncoding::ASCII) override; + + /** + * @brief Returns basic stats about a card image without opening a handle. + * + * @return ProbeResults structure. + */ + ProbeResults probeCardFile(const std::filesystem::path& filename) override; + + /** + * @brief Writes any changes to the Card instance immediately to disk.
+ * Note: Under normal circumstances there is no need to call this function. + */ + void commit() override; + + /** + * @brief Opens card image (does nothing if currently open path matches). + */ + bool open(const std::filesystem::path& filepath) override; + + /** + * @brief Commits changes to disk and closes host file. + */ + void close() override; + + /** + * @brief Access host filename of card. + * + * @return A view to the card's filename. + */ + const std::filesystem::path& cardFilename() const override { return m_filename; } + + /** + * @brief Gets card-scope error state. + * + * @return READY, BROKEN, or NOCARD + */ + ECardResult getError() const override; + + /** + * @return Whether or not the card is within a ready state. + */ + explicit operator bool() const { return getError() == ECardResult::READY; } +}; +} // namespace aurora::card diff --git a/aurora-main/lib/card/CommonData.h b/aurora-main/lib/card/CommonData.h new file mode 100644 index 0000000..14caa06 --- /dev/null +++ b/aurora-main/lib/card/CommonData.h @@ -0,0 +1,59 @@ +#pragma once + +#include + +#include "Util.hpp" +#include "Constants.hpp" + +namespace aurora::card { + +class FileHandle { + friend class CardRawFile; + friend class CardGciFolder; + uint32_t idx = UINT32_MAX; + int32_t offset = 0; + explicit FileHandle(uint32_t idx) : idx(idx) {} + +public: + FileHandle() = default; + explicit FileHandle(uint32_t idx, int32_t offset) : idx(idx), offset(offset) {} + + [[nodiscard]] uint32_t getFileNo() const { return idx; } + [[nodiscard]] uint32_t getOffset() const { return offset; } + explicit operator bool() const { return getFileNo() != UINT32_MAX; } +}; + +struct ProbeResults { + ECardResult x0_error; + uint32_t x4_cardSize; /* in megabits */ + uint32_t x8_sectorSize; /* in bytes */ +}; + +struct CardStat { + /* read-only (Set by Card::getStatus) */ + char x0_fileName[CARD_FILENAME_MAX]; + uint32_t x20_length; + uint32_t x24_time; /* seconds since 01/01/2000 midnight */ + std::array x28_gameName; + std::array x2c_company; + + /* read/write (Set by Card::getStatus/Card::setStatus) */ + uint8_t x2e_bannerFormat; + uint8_t x2f___padding; + uint32_t x30_iconAddr; /* offset to the banner, bannerTlut, icon, iconTlut data set. */ + uint16_t x34_iconFormat; + uint16_t x36_iconSpeed; + uint32_t x38_commentAddr; /* offset to the pair of 32 byte character strings. */ + + /* read-only (Set by Card::getStatus) */ + uint32_t x3c_offsetBanner; + uint32_t x40_offsetBannerTlut; + std::array x44_offsetIcon; + uint32_t x64_offsetIconTlut; + uint32_t x68_offsetData; + + EImageFormat GetBannerFormat() const { return EImageFormat(x2e_bannerFormat & 0x3); } + EImageFormat GetIconFormat(int idx) const { return EImageFormat((x34_iconFormat >> (idx * 2)) & 0x3); } +}; + +} diff --git a/aurora-main/lib/card/Constants.hpp b/aurora-main/lib/card/Constants.hpp new file mode 100644 index 0000000..1367e91 --- /dev/null +++ b/aurora-main/lib/card/Constants.hpp @@ -0,0 +1,109 @@ +#pragma once + +#include + +#include "Util.hpp" + +#define CARD_FILENAME_MAX 32 +#define CARD_ICON_MAX 8 + +namespace aurora::card { +constexpr uint32_t BlockSize = 0x2000; +constexpr uint32_t MaxFiles = 127; +constexpr uint32_t FSTBlocks = 5; +constexpr uint32_t MbitToBlocks = 0x10; +constexpr uint32_t BATSize = 0xFFB; + +/** + * @brief The EPermissions enum + */ +enum class EPermissions : uint8_t { + Public = (1 << 2), + NoCopy = (1 << 3), + NoMove = (1 << 4), + Global = (1 << 5), + Company = (1 << 6) +}; +ENABLE_BITWISE_ENUM(EPermissions) + +enum class EImageFormat : uint8_t { + None, + C8, + RGB5A3, +}; + +enum class EAnimationType { + Loop = 0, + Bounce = 2, +}; + +enum class EAnimationSpeed { + End, + Fast, + Middle, + Slow, +}; + +enum class SeekOrigin { Begin, Current, End }; + +/** + * @brief The ECardSlot enum + */ +enum class ECardSlot : uint16_t { SlotA, SlotB }; + +/** + * @brief The ECardSize enum + */ +enum class ECardSize : uint16_t { + Card59Mb = 0x04, + Card123Mb = 0x08, + Card251Mb = 0x10, + Card507Mb = 0x20, + Card1019Mb = 0x40, + Card2043Mb = 0x80 +}; + +/** + * @brief The EEncoding enum + */ +enum class EEncoding : uint16_t { + ASCII, /**< Standard ASCII Encoding */ + SJIS /**< SJIS Encoding for japanese */ +}; + +constexpr uint32_t BannerSize(EImageFormat fmt) { + switch (fmt) { + default: + case EImageFormat::None: + return 0; + case EImageFormat::C8: + return 3584; + case EImageFormat::RGB5A3: + return 6144; + } +} + +constexpr uint32_t IconSize(EImageFormat fmt) { + switch (fmt) { + default: + case EImageFormat::None: + return 0; + case EImageFormat::C8: + return 1024; + case EImageFormat::RGB5A3: + return 2048; + } +} + +constexpr uint32_t TlutSize(EImageFormat fmt) { + switch (fmt) { + default: + case EImageFormat::None: + case EImageFormat::RGB5A3: + return 0; + case EImageFormat::C8: + return 512; + } +} + +} // namespace aurora::card diff --git a/aurora-main/lib/card/Directory.cpp b/aurora-main/lib/card/Directory.cpp new file mode 100644 index 0000000..b2a9bcb --- /dev/null +++ b/aurora-main/lib/card/Directory.cpp @@ -0,0 +1,142 @@ +#include "Directory.hpp" + +#include +#include + +#include "../internal.hpp" +#include "Util.hpp" + +namespace aurora::card { +void Directory::swapEndian() { + std::for_each(std::begin(data.m_files), std::end(data.m_files), [](File& f) { f.swapEndian(); }); + + data.m_updateCounter = bswap(data.m_updateCounter); + data.m_checksum = bswap(data.m_checksum); + data.m_checksumInv = bswap(data.m_checksumInv); +} + +void Directory::updateChecksum() { + swapEndian(); + calculateChecksumBE(reinterpret_cast(raw.data()), 0xFFE, &data.m_checksum, &data.m_checksumInv); + swapEndian(); +} + +bool Directory::valid() const { + uint16_t ckSum, ckSumInv; + const_cast(*this).swapEndian(); + calculateChecksumBE(reinterpret_cast(raw.data()), 0xFFE, &ckSum, &ckSumInv); + const bool res = (ckSum == data.m_checksum && ckSumInv == data.m_checksumInv); + const_cast(*this).swapEndian(); + return res; +} + +Directory::Directory() { + raw.fill(0xFF); + data.m_updateCounter = 0; + updateChecksum(); +} + +Directory::Directory(const RawData& rawData) : raw{rawData} {} + +bool Directory::hasFreeFile() const { + return std::any_of(std::cbegin(data.m_files), std::cend(data.m_files), + [](const auto& file) { return file.m_game[0] == 0xFF; }); +} + +int32_t Directory::numFreeFiles() const { + return int32_t(std::count_if(std::cbegin(data.m_files), std::cend(data.m_files), + [](const auto& file) { return file.m_game[0] == 0xFF; })); +} + +File* Directory::getFirstFreeFile(const char* game, const char* maker, const char* filename) { + const auto iter = std::find_if(std::begin(data.m_files), std::end(data.m_files), + [](const auto& file) { return file.m_game[0] == 0xFF; }); + + if (iter == std::end(data.m_files)) { + return nullptr; + } + + *iter = File(filename); + if (game != nullptr && std::strlen(game) == 4) { + std::memcpy(iter->m_game, game, 4); + } + if (maker != nullptr && std::strlen(maker) == 2) { + std::memcpy(iter->m_maker, maker, 2); + } + + return &*iter; +} + +File* Directory::getFirstNonFreeFile(uint32_t start, const char* game, const char* maker) { + const auto iter = + std::find_if(std::begin(data.m_files) + start, std::end(data.m_files), [game, maker](const auto& file) { + if (file.m_game[0] == 0xFF) { + return false; + } + + const auto* const game_ptr = reinterpret_cast(file.m_game); + const auto game_size = 4; + if (game != nullptr && std::strlen(game) == game_size && std::strncmp(game_ptr, game, game_size) != 0) { + return false; + } + + const auto* const maker_ptr = reinterpret_cast(file.m_maker); + const auto maker_size = 2; + if (maker != nullptr && std::strlen(maker) == maker_size && std::strncmp(maker_ptr, maker, maker_size) != 0) { + return false; + } + + return true; + }); + + if (iter == std::end(data.m_files)) { + return nullptr; + } + + return &*iter; +} + +File* Directory::getFile(const char* game, const char* maker, const char* filename) { + const auto iter = std::find_if(std::begin(data.m_files), std::end(data.m_files), [=](const auto& file) { + const auto game_size = 4; + if (game != nullptr && std::strlen(game) == game_size && std::memcmp(file.m_game, game, game_size) != 0) { + return false; + } + + const auto maker_size = 2; + if (maker != nullptr && std::strlen(maker) == maker_size && std::memcmp(file.m_maker, maker, maker_size) != 0) { + return false; + } + + return std::strncmp(file.m_filename, filename, 32) == 0; + }); + + if (iter == std::cend(data.m_files)) { + return nullptr; + } + + return &*iter; +} + +File* Directory::getFile(uint32_t idx) { + if (idx >= MaxFiles) { + return nullptr; + } + + return &data.m_files[idx]; +} + +int32_t Directory::indexForFile(const File* f) const { + if (f == nullptr) { + return -1; + } + + const auto it = + std::find_if(std::cbegin(data.m_files), std::cend(data.m_files), [&f](const File& file) { return f == &file; }); + if (it == std::cend(data.m_files)) { + return -1; + } + + return it - std::cbegin(data.m_files); +} +} // namespace aurora::card diff --git a/aurora-main/lib/card/Directory.hpp b/aurora-main/lib/card/Directory.hpp new file mode 100644 index 0000000..bee9027 --- /dev/null +++ b/aurora-main/lib/card/Directory.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +#include "File.hpp" + +namespace aurora::card { +class Directory { + friend class CardRawFile; + +#pragma pack(push, 4) + using RawData = std::array; + struct Data { + File m_files[MaxFiles]; + uint8_t padding[0x3a]; + uint16_t m_updateCounter; + uint16_t m_checksum; + uint16_t m_checksumInv; + }; +#pragma pack(pop) + + union { + Data data; + RawData raw; + }; + + void swapEndian(); + void updateChecksum(); + bool valid() const; + +public: + Directory(); + explicit Directory(const RawData& rawData); + ~Directory() = default; + + bool hasFreeFile() const; + int32_t numFreeFiles() const; + File* getFirstFreeFile(const char* game, const char* maker, const char* filename); + File* getFirstNonFreeFile(uint32_t start, const char* game, const char* maker); + File* getFile(const char* game, const char* maker, const char* filename); + File* getFile(uint32_t idx); + int32_t indexForFile(const File* f) const; +}; +} // namespace aurora::card diff --git a/aurora-main/lib/card/DolphinCardPath.cpp b/aurora-main/lib/card/DolphinCardPath.cpp new file mode 100644 index 0000000..0d0adf0 --- /dev/null +++ b/aurora-main/lib/card/DolphinCardPath.cpp @@ -0,0 +1,118 @@ +#include + +#include "DolphinCardPath.hpp" + +#if WIN32 +#include "windows.h" +#include "winreg.h" +#include "shlobj_core.h" +#endif + +#include + +#include "../internal.hpp" + +#if WIN32 +namespace { + std::string ConvertWideToANSI(const std::wstring& wstr) { + int count = WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), wstr.length(), NULL, 0, NULL, NULL); + std::string str(count, 0); + WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), -1, &str[0], count, NULL, NULL); + return str; + } +} +#endif + +namespace aurora::card { +aurora::Module Log("aurora::card"); + +#if WIN32 + +#if WINDOWS_STORE +using namespace Windows::Storage; +#endif + +std::string ResolveDolphinCardPath(ECardSlot slot, const char* regionCode, bool isGciFolder) { +#if !WINDOWS_STORE + /* Detect where the User directory is. There are two different cases + * 1. HKCU\Software\Dolphin Emulator\UserConfigPath exists + * -> Use this as the user directory path + * 2. My Documents exists + * -> Use My Documents\Dolphin Emulator as the User directory path + */ + + /* Check our registry keys */ + HKEY hkey; + wchar_t configPath[MAX_PATH] = {0}; + if (RegOpenKeyEx(HKEY_CURRENT_USER, TEXT("Software\\Dolphin Emulator"), 0, KEY_QUERY_VALUE, &hkey) == ERROR_SUCCESS) { + DWORD size = MAX_PATH; + if (RegQueryValueEx(hkey, TEXT("UserConfigPath"), nullptr, nullptr, (LPBYTE)configPath, &size) != ERROR_SUCCESS) + configPath[0] = 0; + RegCloseKey(hkey); + } + + /* Get My Documents path in case we need it. */ + PWSTR my_documents; + bool my_documents_found = + SUCCEEDED(SHGetKnownFolderPath(FOLDERID_Documents, 0, NULL, &my_documents)); + + std::wstring w_path; + if (configPath[0]) /* Case 1 */ + w_path = configPath; + else if (my_documents_found) /* Case 2 */ { + w_path = my_documents; + w_path += L"\\Dolphin Emulator"; + } + else /* Unable to find */ { + Log.error("Unable to find Dolphin Emulator user directory!"); + return {}; + } +#else + StorageFolder ^ localFolder = ApplicationData::Current->LocalFolder; + std::string path(localFolder->Path->Data()); +#endif + + std::string path; + if (isGciFolder) { + path = fmt::format("{}/GC/{}/Card {}", ConvertWideToANSI(w_path), regionCode, slot == ECardSlot::SlotA ? 'A' : 'B'); + }else { + path = fmt::format("{}/GC/MemoryCard{}.{}.raw", ConvertWideToANSI(w_path), slot == ECardSlot::SlotA ? 'A' : 'B', regionCode); + } + + if (!std::filesystem::exists(path)) { + Log.error("Unable to find Dolphin Card file! Search Path: {}", path); + return {}; + } + + return path; +} +#else +static std::optional GetPrefPath(const char* app) { + char* path = SDL_GetPrefPath(nullptr, app); + if (path == nullptr) { + return {}; + } + std::string str{path}; + SDL_free(path); + return str; +} + +std::string ResolveDolphinCardPath(ECardSlot slot, const char* regionCode, bool isGciFolder) { + const auto dolphinPath = GetPrefPath("dolphin-emu"); + if (!dolphinPath) { + return {}; + } + auto path = *dolphinPath; + if (isGciFolder) { + path += fmt::format("GC/{}/Card {}", regionCode, slot == ECardSlot::SlotA ? 'A' : 'B'); + }else { + path += fmt::format("GC/MemoryCard{}.{}.raw", slot == ECardSlot::SlotA ? 'A' : 'B', regionCode); + } + + if (!std::filesystem::exists(path)) return {}; + + return path; +} +#endif + +} diff --git a/aurora-main/lib/card/DolphinCardPath.hpp b/aurora-main/lib/card/DolphinCardPath.hpp new file mode 100644 index 0000000..2a45182 --- /dev/null +++ b/aurora-main/lib/card/DolphinCardPath.hpp @@ -0,0 +1,9 @@ +#pragma once +#include +#include + +#include "Constants.hpp" + +namespace aurora::card { +std::string ResolveDolphinCardPath(ECardSlot slot, const char* regionCode, bool isGciFolder); +} \ No newline at end of file diff --git a/aurora-main/lib/card/File.cpp b/aurora-main/lib/card/File.cpp new file mode 100644 index 0000000..8be888c --- /dev/null +++ b/aurora-main/lib/card/File.cpp @@ -0,0 +1,28 @@ +#include "File.hpp" + +#include +#include + +#include "../internal.hpp" + +namespace aurora::card { +File::File() { raw.fill(0xFF); } + +File::File(const RawData& rawData) : raw{rawData} {} + +File::File(const char* filename) { + raw.fill(0); + std::memset(m_filename, 0, std::size(m_filename)); + std::strncpy(m_filename, filename, std::size(m_filename) - 1); +} +void File::swapEndian() { + m_modifiedTime = bswap(m_modifiedTime); + m_iconAddress = bswap(m_iconAddress); + m_iconFmt = bswap(m_iconFmt); + m_animSpeed = bswap(m_animSpeed); + m_firstBlock = bswap(m_firstBlock); + m_blockCount = bswap(m_blockCount); + m_reserved2 = bswap(m_reserved2); + m_commentAddr = bswap(m_commentAddr); +} +} // namespace aurora::card diff --git a/aurora-main/lib/card/File.hpp b/aurora-main/lib/card/File.hpp new file mode 100644 index 0000000..7bb9b5d --- /dev/null +++ b/aurora-main/lib/card/File.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +#include "Constants.hpp" + +namespace aurora::card { +class File { + friend class IFileHandle; + friend class Directory; + friend class CardRawFile; + friend class CardGciFolder; +#pragma pack(push, 4) + using RawData = std::array; + union { + struct { + uint8_t m_game[4]; + uint8_t m_maker[2]; + uint8_t m_reserved; + uint8_t m_bannerFlags; +#if __GNUC__ && !__clang__ + __attribute__((nonstring)) +#endif + char m_filename[0x20]; + uint32_t m_modifiedTime; + uint32_t m_iconAddress; + uint16_t m_iconFmt; + uint16_t m_animSpeed; + EPermissions m_permissions; + int8_t m_copyCounter; + uint16_t m_firstBlock; + uint16_t m_blockCount; + uint16_t m_reserved2; + uint32_t m_commentAddr; + }; + RawData raw; + }; +#pragma pack(pop) + void swapEndian(); + +public: + File(); + explicit File(const RawData& rawData); + explicit File(const char* filename); + ~File() = default; +}; +} // namespace aurora::card diff --git a/aurora-main/lib/card/FileIO.cpp b/aurora-main/lib/card/FileIO.cpp new file mode 100644 index 0000000..c19185f --- /dev/null +++ b/aurora-main/lib/card/FileIO.cpp @@ -0,0 +1,124 @@ +#include "FileIO.hpp" + +#include +#include + +#include +#include + +#include "../fs_helper.hpp" + +namespace aurora::card { + +FileIO::FileIO(const std::filesystem::path& filename, bool truncate) { + m_path = fs_path_to_string(filename);; + SDL_IOStream* stream = nullptr; + + stream = fileOpen(m_path, truncate ? "w+b" : "r+b"); + + if (stream != nullptr) { + SDL_FlushIO(stream); + SDL_CloseIO(stream); + m_ready = true; + } +} + +FileIO::FileIO(FileIO&& other) noexcept { + m_path = std::move(other.m_path); + m_ready = std::exchange(other.m_ready, false); +} + +FileIO& FileIO::operator=(FileIO&& other) noexcept { + if (this != &other) { + m_path = std::move(other.m_path); + m_ready = std::exchange(other.m_ready, false); + } + return *this; +} + +bool FileIO::fileRead(void* buf, size_t length, off_t offset) { + if (!isReady()) { + return false; + } + + SDL_IOStream* stream = fileOpen(m_path, "rb"); + if (stream == nullptr) { + return false; + } + + if (SDL_SeekIO(stream, offset, SDL_IO_SEEK_SET) < 0) { + SDL_CloseIO(stream); + return false; + } + + size_t total = 0; + auto* dst = static_cast(buf); + while (total < length) { + const size_t read = SDL_ReadIO(stream, dst + total, length - total); + if (read == 0) { + SDL_CloseIO(stream); + return false; + } + total += read; + } + + SDL_CloseIO(stream); + return true; +} + +bool FileIO::fileWrite(const void* buf, size_t length, off_t offset) { + if (!isReady()) { + return false; + } + + SDL_IOStream* stream = fileOpen(m_path, "r+b"); + if (stream == nullptr) { + stream = fileOpen(m_path, "w+b"); + } + if (stream == nullptr) { + return false; + } + + if (SDL_SeekIO(stream, offset, SDL_IO_SEEK_SET) < 0) { + SDL_FlushIO(stream); + SDL_CloseIO(stream); + return false; + } + + size_t total = 0; + auto* src = static_cast(buf); + while (total < length) { + const size_t written = SDL_WriteIO(stream, src + total, length - total); + if (written == 0) { + SDL_CloseIO(stream); + return false; + } + total += written; + } + + SDL_FlushIO(stream); + SDL_CloseIO(stream); + return true; +} + +size_t FileIO::fileSize() const { + SDL_PathInfo info; + const auto tr = fs_path_to_string(m_path); + if (SDL_GetPathInfo(m_path.c_str(), &info)) { + return info.size; + } + return 0; +} + +bool FileIO::deleteFile() { + if (SDL_RemovePath(m_path.c_str())) { + m_ready = false; + m_path.clear(); + return true; + } + return false; +} + +FileIO::operator bool() const { return isReady(); } + +} // namespace aurora::card diff --git a/aurora-main/lib/card/FileIO.hpp b/aurora-main/lib/card/FileIO.hpp new file mode 100644 index 0000000..98fa929 --- /dev/null +++ b/aurora-main/lib/card/FileIO.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +#include + +namespace aurora::card { + +class FileIO { + std::string m_path; + bool m_ready = false; + + bool isReady() const { return m_ready && !m_path.empty(); }; + +public: + FileIO() = default; + explicit FileIO(const std::filesystem::path& filename, bool truncate = false); + ~FileIO() = default; + + FileIO(FileIO&& other) noexcept; + FileIO& operator=(FileIO&& other) noexcept; + FileIO(const FileIO& other) = delete; + FileIO& operator=(const FileIO& other) = delete; + + static SDL_IOStream* fileOpen(const std::string& path, const char* mode) {return path.empty() ? nullptr : SDL_IOFromFile(path.c_str(), mode);} + bool fileRead(void* buf, size_t length, off_t offset); + bool fileWrite(const void* buf, size_t length, off_t offset); + size_t fileSize() const; + bool deleteFile(); + explicit operator bool() const; +}; + +} // namespace aurora::card diff --git a/aurora-main/lib/card/ICard.hpp b/aurora-main/lib/card/ICard.hpp new file mode 100644 index 0000000..a169d4d --- /dev/null +++ b/aurora-main/lib/card/ICard.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "Constants.hpp" +#include "Util.hpp" +#include + +namespace aurora::card { +class ICard { +public: + virtual ~ICard() = default; + + virtual void InitCard(const char* game = nullptr, const char* maker = nullptr) = 0; + + virtual ECardResult openFile(const char* filename, FileHandle& handleOut) = 0; + virtual ECardResult openFile(uint32_t fileno, FileHandle& handleOut) = 0; + virtual ECardResult createFile(const char* filename, size_t size, FileHandle& handleOut) = 0; + virtual ECardResult closeFile(FileHandle& fh) = 0; + virtual void deleteFile(const FileHandle& fh) = 0; + virtual ECardResult deleteFile(const char* filename) = 0; + virtual ECardResult deleteFile(uint32_t fileno) = 0; + virtual ECardResult renameFile(const char* oldName, const char* newName) = 0; + virtual ECardResult fileWrite(FileHandle& fh, const void* buf, size_t size) = 0; + virtual ECardResult fileRead(FileHandle& fh, void* dst, size_t size) = 0; + virtual void seek(FileHandle& fh, int32_t pos, SeekOrigin whence) = 0; + virtual ECardResult getStatus(const FileHandle& fh, CardStat& statOut) const = 0; + virtual ECardResult getStatus(uint32_t fileNo, CardStat& statOut) const = 0; + virtual ECardResult setStatus(const FileHandle& fh, const CardStat& stat) = 0; + virtual ECardResult setStatus(uint32_t fileNo, const CardStat& stat) = 0; + virtual void setCurrentGame(const char* game) = 0; + virtual const uint8_t* getCurrentGame() const = 0; + virtual void setCurrentMaker(const char* maker) = 0; + virtual const uint8_t* getCurrentMaker() const = 0; + virtual void getSerial(uint64_t& serial) = 0; + virtual void getChecksum(uint16_t& checksum, uint16_t& inverse) const = 0; + virtual void getFreeBlocks(int32_t& bytesNotUsed, int32_t& filesNotUsed) const = 0; + virtual void getEncoding(uint16_t& encoding) const = 0; + virtual void format(ECardSlot deviceId, ECardSize size = ECardSize::Card2043Mb, EEncoding encoding = EEncoding::ASCII) = 0; + virtual void commit() = 0; + virtual bool open(const std::filesystem::path& filepath) = 0; + virtual void close() = 0; + virtual const std::filesystem::path& cardFilename() const = 0; + virtual ECardResult getError() const = 0; + virtual ProbeResults probeCardFile(const std::filesystem::path& filename) = 0; +}; +} \ No newline at end of file diff --git a/aurora-main/lib/card/SRAM.cpp b/aurora-main/lib/card/SRAM.cpp new file mode 100644 index 0000000..a39448f --- /dev/null +++ b/aurora-main/lib/card/SRAM.cpp @@ -0,0 +1,29 @@ +#include "SRAM.hpp" +namespace aurora::card { +// clang-format off +const SRAM g_SRAM = +{{ + 0xFF, 0x6B, + 0x00, 0x91, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0x40, + 0x00, + 0x00, + 0x00, + 0x2C, + 0x41, 0x58, 0x49, 0x4F, 0x44, 0x4C, 0x5F, 0x53, 0x4C, 0x4F, 0x54, 0x41, + 0x41, 0x58, 0x49, 0x4F, 0x44, 0x4C, 0x5F, 0x53, 0x4C, 0x4F, 0x54, 0x42, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + 0x00, + 0x00, + 0x6E, 0x6D, + 0x00, 0x00, + 0x00, 0x00 +}}; +// clang-format on +} // namespace aurora::card diff --git a/aurora-main/lib/card/SRAM.hpp b/aurora-main/lib/card/SRAM.hpp new file mode 100644 index 0000000..b035dd6 --- /dev/null +++ b/aurora-main/lib/card/SRAM.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include +#include + +// Modified code taken from libogc + +/*------------------------------------------------------------- +system.h -- OS functions and initialization +Copyright (C) 2004 +Michael Wiedenbauer (shagkur) +Dave Murphy (WinterMute) +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any +damages arising from the use of this software. +Permission is granted to anyone to use this software for any +purpose, including commercial applications, and to alter it and +redistribute it freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you +must not claim that you wrote the original software. If you use +this software in a product, an acknowledgment in the product +documentation would be appreciated but is not required. +2. Altered source versions must be plainly marked as such, and +must not be misrepresented as being the original software. +3. This notice may not be removed or altered from any source +distribution. +-------------------------------------------------------------*/ + +namespace aurora::card { +#pragma pack(push, 1) +union SRAMFlags { + uint8_t Hex; + struct { + uint8_t : 2; + uint8_t sound : 1; // Audio settings; 0 = Mono, 1 = Stereo + uint8_t initialized : 1; // if 0, displays prompt to set language on boot and asks user to set options and time/date + uint8_t : 2; + uint8_t boot_menu : 1; // if 1, skips logo animation and boots into the system menu regardless of if there is a + // disc inserted + uint8_t progressive : 1; // if 1, automatically displays Progressive Scan prompt in games that support it + }; +}; + +union SRAM { + using FlashID = std::array, 2>; + + std::array p_SRAM; + struct // Stored configuration value from the system SRAM area + { + uint16_t checksum; // Holds the block checksum. + uint16_t checksum_inv; // Holds the inverse block checksum + uint32_t ead0; // Unknown attribute + uint32_t ead1; // Unknown attribute + uint32_t counter_bias; // Bias value for the realtime clock + int8_t display_offsetH; // Pixel offset for the VI + uint8_t ntd; // Unknown attribute + uint8_t lang; // Language of system + SRAMFlags flags; // Device and operations flag + + // Stored configuration value from the extended SRAM area + FlashID flash_id; // flash_id[2][12] 96bit memorycard unlock flash ID + uint32_t wirelessKbd_id; // Device ID of last connected wireless keyboard + std::array wirelessPad_id; // 16-bit device ID of last connected pad. + uint8_t dvderr_code; // last non-recoverable error from DVD interface + uint8_t __padding0; // reserved + std::array flashID_chksum; // 8-bit checksum of unlock flash ID + uint32_t __padding1; // padding + }; +}; +#pragma pack(pop) + +extern const SRAM g_SRAM; +} // namespace aurora::card diff --git a/aurora-main/lib/card/Util.cpp b/aurora-main/lib/card/Util.cpp new file mode 100644 index 0000000..0447183 --- /dev/null +++ b/aurora-main/lib/card/Util.cpp @@ -0,0 +1,41 @@ +#include + +#include "../internal.hpp" + +namespace aurora::card { +uint64_t getGCTime() { + time_t sysTime, tzDST; + + time(&sysTime); + + // Account for DST where needed + tm* gmTime = localtime(&sysTime); + if (gmTime->tm_isdst == 1) + tzDST = 3600; + else + tzDST = 0; + + // Lazy way to get local time in sec + gmTime = gmtime(&sysTime); + time_t tzDiff = sysTime - mktime(gmTime); + + return static_cast(sysTime + tzDiff + tzDST) - 0x386D4380; +} + +void calculateChecksumBE(const uint16_t* data, size_t len, uint16_t* checksum, uint16_t* checksumInv) { + *checksum = 0; + *checksumInv = 0; + for (size_t i = 0; i < len; ++i) { + *checksum += bswap(data[i]); + *checksumInv += bswap(static_cast(data[i] ^ 0xFFFF)); + } + + *checksum = bswap(*checksum); + *checksumInv = bswap(*checksumInv); + if (*checksum == 0xFFFF) + *checksum = 0; + if (*checksumInv == 0xFFFF) + *checksumInv = 0; +} + +} // namespace aurora::card diff --git a/aurora-main/lib/card/Util.hpp b/aurora-main/lib/card/Util.hpp new file mode 100644 index 0000000..0875d7e --- /dev/null +++ b/aurora-main/lib/card/Util.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include + +#ifndef ENABLE_BITWISE_ENUM +#define ENABLE_BITWISE_ENUM(type) \ + constexpr type operator|(type a, type b) { \ + using T = std::underlying_type_t; \ + return type(static_cast(a) | static_cast(b)); \ + } \ + constexpr type operator&(type a, type b) { \ + using T = std::underlying_type_t; \ + return type(static_cast(a) & static_cast(b)); \ + } \ + constexpr type& operator|=(type& a, type b) { \ + using T = std::underlying_type_t; \ + a = type(static_cast(a) | static_cast(b)); \ + return a; \ + } \ + constexpr type& operator&=(type& a, type b) { \ + using T = std::underlying_type_t; \ + a = type(static_cast(a) & static_cast(b)); \ + return a; \ + } \ + constexpr type operator~(type key) { \ + using T = std::underlying_type_t; \ + return type(~static_cast(key)); \ + } \ + constexpr bool True(type key) { \ + using T = std::underlying_type_t; \ + return static_cast(key) != 0; \ + } \ + constexpr bool False(type key) { \ + using T = std::underlying_type_t; \ + return static_cast(key) == 0; \ + } +#endif + +#define ROUND_UP_8192(val) (((val) + 8191) & ~8191) + +namespace aurora::card { + +uint64_t getGCTime(); + +/** + * @brief calculateChecksum + * @param data + * @param len + * @param checksum + * @param checksumInv + */ +void calculateChecksumBE(const uint16_t* data, size_t len, uint16_t* checksum, uint16_t* checksumInv); + +#undef NOFILE + +enum class ECardResult { + CRC_MISMATCH = -1003, /* Extension enum for Retro's CRC check */ + FATAL_ERROR = -128, + ENCODING = -13, + NAMETOOLONG = -12, + INSSPACE = -9, + NOENT = -8, + EXIST = -7, + BROKEN = -6, + IOERROR = -5, + NOFILE = -4, + NOCARD = -3, + WRONGDEVICE = -2, + BUSY = -1, + READY = 0 +}; +} // namespace aurora::card diff --git a/aurora-main/lib/dawn/BackendBinding.cpp b/aurora-main/lib/dawn/BackendBinding.cpp new file mode 100644 index 0000000..bb8fba4 --- /dev/null +++ b/aurora-main/lib/dawn/BackendBinding.cpp @@ -0,0 +1,46 @@ +#include "BackendBinding.hpp" + +#include + +#if !defined(SDL_PLATFORM_MACOS) && !defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_TVOS) +#include +#endif + +namespace aurora::webgpu::utils { +std::shared_ptr SetupWindowAndGetSurfaceDescriptorCocoa(SDL_Window* window); + +std::shared_ptr SetupWindowAndGetSurfaceDescriptor(SDL_Window* window) { +#if defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_TVOS) + return SetupWindowAndGetSurfaceDescriptorCocoa(window); +#else + const auto props = SDL_GetWindowProperties(window); +#if defined(SDL_PLATFORM_ANDROID) + std::shared_ptr desc = + std::make_shared(); + desc->window = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_ANDROID_WINDOW_POINTER, nullptr); + return std::move(desc); +#elif defined(SDL_PLATFORM_WIN32) + std::shared_ptr desc = std::make_shared(); + desc->hwnd = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr); + desc->hinstance = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_INSTANCE_POINTER, nullptr); + return std::move(desc); +#elif defined(SDL_PLATFORM_LINUX) + const char* driver = SDL_GetCurrentVideoDriver(); + if (SDL_strcmp(driver, "wayland") == 0) { + std::shared_ptr desc = std::make_shared(); + desc->display = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, nullptr); + desc->surface = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, nullptr); + return std::move(desc); + } + if (SDL_strcmp(driver, "x11") == 0) { + std::shared_ptr desc = std::make_shared(); + desc->display = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, nullptr); + desc->window = SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0); + return std::move(desc); + } +#endif + return nullptr; +#endif +} + +} // namespace aurora::webgpu::utils diff --git a/aurora-main/lib/dawn/BackendBinding.hpp b/aurora-main/lib/dawn/BackendBinding.hpp new file mode 100644 index 0000000..ac49d5c --- /dev/null +++ b/aurora-main/lib/dawn/BackendBinding.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +struct SDL_Window; + +namespace aurora::webgpu::utils { + +std::shared_ptr SetupWindowAndGetSurfaceDescriptor(SDL_Window* window); + +} // namespace aurora::webgpu::utils diff --git a/aurora-main/lib/dawn/MetalBinding.mm b/aurora-main/lib/dawn/MetalBinding.mm new file mode 100644 index 0000000..421bd9e --- /dev/null +++ b/aurora-main/lib/dawn/MetalBinding.mm @@ -0,0 +1,13 @@ +#include "BackendBinding.hpp" + +#import +#include + +namespace aurora::webgpu::utils { +std::shared_ptr SetupWindowAndGetSurfaceDescriptorCocoa(SDL_Window* window) { + SDL_MetalView view = SDL_Metal_CreateView(window); + std::shared_ptr desc = std::make_shared(); + desc->layer = SDL_Metal_GetLayer(view); + return std::move(desc); +} +} // namespace aurora::webgpu::utils diff --git a/aurora-main/lib/dolphin/AR.cpp b/aurora-main/lib/dolphin/AR.cpp new file mode 100644 index 0000000..c2cb3bc --- /dev/null +++ b/aurora-main/lib/dolphin/AR.cpp @@ -0,0 +1,115 @@ +#include +#include "../internal.hpp" +#include "dolphin/os.h" + +static aurora::Module Log("aurora::ar"); + +static u32 AR_StackPointer; +static u32* AR_BlockLength; +static u32 AR_FreeBlocks; +static BOOL AR_init_flag; + +#define ARAM_STACK_START 0x4000; + +// ARAM emulation: allocate a large buffer to simulate the GameCube's Auxiliary RAM. +// ARAM "addresses" are offsets into this buffer. On GameCube, ARAM is 16 MB starting +// at a base address returned by ARInit. We emulate this by malloc'ing a buffer +// and using a simple bump allocator (matching ARAlloc behavior on real hardware). +static u8* sAramBuffer = nullptr; + +// Convert an ARAM "address" (offset) to a real host pointer +static u8* aramToHost(u32 aramAddr) { + if (!sAramBuffer || aramAddr >= aurora::g_config.mem2Size) { + return nullptr; + } + return sAramBuffer + aramAddr; +} + +u32 ARAlloc(u32 length) { + u32 tmp; + + ASSERTMSGLINE(430, !(length & 0x1F), "ARAlloc(): length is not multiple of 32bytes!"); + ASSERTMSGLINE(434, length <= (__AR_Size - __AR_StackPointer), "ARAlloc(): Out of ARAM!"); + ASSERTMSGLINE(435, __AR_FreeBlocks, "ARAlloc(): No more free blocks!"); + + tmp = AR_StackPointer; + AR_StackPointer += length; + *AR_BlockLength = length; + AR_BlockLength += 1; + AR_FreeBlocks -= 1; + return tmp; +} + +u32 ARFree(u32* length) { + AR_BlockLength -= 1; + if (length) { + *length = *AR_BlockLength; + } + AR_StackPointer -= *AR_BlockLength; + AR_FreeBlocks += 1; + return AR_StackPointer; +} + +BOOL ARCheckInit(void) { return AR_init_flag; } + +u32 ARInit(u32* stack_index_addr, u32 num_entries) { + if (aurora::g_config.mem2Size == 0) { + Log.warn("ARInit called but no mem2Size specified in AuroraConfig. ARAM will not be available!"); + return 0; + } + + if (AR_init_flag == TRUE) { + return ARAM_STACK_START; + } + + sAramBuffer = (u8*)calloc(1, aurora::g_config.mem2Size); + if (sAramBuffer) { + Log.debug("Initialized 0x{:X} bytes of ARAM!", aurora::g_config.mem2Size); + } else { + Log.fatal("Failed to allocate ARAM!"); + } + + AR_StackPointer = ARAM_STACK_START; + AR_FreeBlocks = num_entries; + AR_BlockLength = stack_index_addr; + + AR_init_flag = TRUE; + return AR_StackPointer; +} + +u32 ARGetSize(void) { return aurora::g_config.mem2Size; } + +#pragma mark ARQ +void ARQPostRequest(ARQRequest* request, u32 owner, u32 type, u32 priority, uintptr_t source, uintptr_t dest, + u32 length, ARQCallback callback) { + // Emulate ARAM DMA transfers using memcpy. + // type 0 = MRAM -> ARAM, type 1 = ARAM -> MRAM + if (type == ARAM_DIR_MRAM_TO_ARAM) { + // Main RAM -> ARAM: source is a host pointer (cast to u32), dest is an ARAM offset + u8* hostSrc = (u8*)(uintptr_t)source; + u8* aramDst = aramToHost(dest); + if (aramDst && hostSrc) { + memcpy(aramDst, hostSrc, length); + } + } else { + // ARAM -> Main RAM: source is an ARAM offset, dest is a host pointer (cast to u32) + u8* aramSrc = aramToHost(source); + u8* hostDst = (u8*)(uintptr_t)dest; + if (aramSrc && hostDst) { + memcpy(hostDst, aramSrc, length); + } + } + + // Immediately invoke the callback (synchronous on PC, no DMA latency) + if (callback) { + callback((uintptr_t)request); + } +} + +void ARQInit() { + // Nothing to do on PC - ARAM is initialized in ARInit +} + +void* ARGetStorageAddress() { + return sAramBuffer; +} diff --git a/aurora-main/lib/dolphin/card.cpp b/aurora-main/lib/dolphin/card.cpp new file mode 100644 index 0000000..626ce4f --- /dev/null +++ b/aurora-main/lib/dolphin/card.cpp @@ -0,0 +1,742 @@ +#include "dolphin/card.h" + +#include + +#include "../internal.hpp" +#include "dolphin/types.h" + +#include "../card/CardRawFile.hpp" +#include "../card/DolphinCardPath.hpp" +#include "../logging.hpp" +#include "../card/CardGciFolder.hpp" +#include "../fs_helper.hpp" + +namespace { +aurora::Module Log("aurora::card"); +std::array, 2> CardChannels = {{}}; +std::array cardPaths; + +constexpr uint16_t CARD_SECTOR_SIZE = 8192; + +const char* GetCardRegion() { + switch (aurora::g_gameName[3]) { + case 'E': + default: + return "USA"; + case 'P': + return "EUR"; + case 'J': + return "JAP"; + } +} + +#define GET_CARD(slot) CardChannels[slot] +#define CARD_USE_GCI_FOLDER SelectedFileType == CARD_GCIFOLDER +#define CARD_READY(slot) (CARD_USE_GCI_FOLDER ? true : std::filesystem::exists(CardChannels[slot]->cardFilename())) +#define CARD_STUB Log.debug("{} is stubbed.", __FUNCTION__); + +bool Initialized = false; +CARDFileType SelectedFileType = CARD_GCIFOLDER; +bool UseFastMode = false; + +void CopyKabuFileHandleToDolphin(const s32 chan, const aurora::card::FileHandle& handle, CARDFileInfo* fileInfo) { + fileInfo->chan = chan; + fileInfo->fileNo = handle.getFileNo(); + fileInfo->offset = handle.getOffset(); +} + +aurora::card::FileHandle CreateKabuFileHandleFromDolphin(const CARDFileInfo* fileInfo) { + return aurora::card::FileHandle{static_cast(fileInfo->fileNo), fileInfo->offset}; +} + +std::filesystem::path GetCardFullPath(const std::filesystem::path& path, const aurora::card::ECardSlot slot) { + if (path.empty()) + return ""; + + if (CARD_USE_GCI_FOLDER) { + return path / GetCardRegion() / (slot == aurora::card::ECardSlot::SlotA ? "Card A" : "Card B"); + } else { + return path / fmt::format("MemoryCard{}.{}.raw", slot == aurora::card::ECardSlot::SlotA ? "A" : "B", GetCardRegion()); + } +} +} // namespace + +extern "C" { + +void CopyKabuStatsToDolphin(const aurora::card::CardStat& kabuStats, CARDStat* stats) { + memcpy(stats->fileName, kabuStats.x0_fileName, std::size(kabuStats.x0_fileName)); + memcpy(stats->gameName, kabuStats.x28_gameName.data(), kabuStats.x28_gameName.size()); + memcpy(stats->company, kabuStats.x2c_company.data(), kabuStats.x2c_company.size()); + memcpy(stats->offsetIcon, kabuStats.x44_offsetIcon.data(), kabuStats.x44_offsetIcon.size()); + + stats->length = kabuStats.x20_length; + stats->time = kabuStats.x24_time; + stats->bannerFormat = kabuStats.x2e_bannerFormat; + stats->iconAddr = kabuStats.x30_iconAddr; + stats->iconFormat = kabuStats.x34_iconFormat; + stats->iconSpeed = kabuStats.x36_iconSpeed; + stats->commentAddr = kabuStats.x38_commentAddr; + stats->offsetBanner = kabuStats.x3c_offsetBanner; + stats->offsetBannerTlut = kabuStats.x40_offsetBannerTlut; + stats->offsetIconTlut = kabuStats.x64_offsetIconTlut; + stats->offsetData = kabuStats.x68_offsetData; +} + +void CopyDolphinStatsToKabu(aurora::card::CardStat& kabuStats, const CARDStat* stats) { + memcpy(kabuStats.x0_fileName, stats->fileName, std::size(kabuStats.x0_fileName)); + memcpy(kabuStats.x28_gameName.data(), stats->gameName, kabuStats.x28_gameName.size()); + memcpy(kabuStats.x2c_company.data(), stats->company, kabuStats.x2c_company.size()); + memcpy(kabuStats.x44_offsetIcon.data(), stats->offsetIcon, kabuStats.x44_offsetIcon.size()); + + kabuStats.x20_length = stats->length; + kabuStats.x24_time = stats->time; + kabuStats.x2e_bannerFormat = stats->bannerFormat; + kabuStats.x30_iconAddr = stats->iconAddr; + kabuStats.x34_iconFormat = stats->iconFormat; + kabuStats.x36_iconSpeed = stats->iconSpeed; + kabuStats.x38_commentAddr = stats->commentAddr; + kabuStats.x3c_offsetBanner = stats->offsetBanner; + kabuStats.x40_offsetBannerTlut = stats->offsetBannerTlut; + kabuStats.x64_offsetIconTlut = stats->offsetIconTlut; + kabuStats.x68_offsetData = stats->offsetData; +} + +void CARDDetectDolphin(const s32 chan) { + if (Initialized) { + Log.fatal("CARDDetectDolphin() called after CARDInit()!"); + } + + if (chan == 0 || chan == 1) { + cardPaths[chan] = + aurora::card::ResolveDolphinCardPath(static_cast(chan), GetCardRegion(), CARD_USE_GCI_FOLDER); + if (cardPaths[chan].empty()) { + Log.error("Failed to detect Dolphin Card!"); + return; + } + Log.info("Detected Dolphin Card at: {}", fs_path_to_string(cardPaths[chan])); + } else { + cardPaths[0] = aurora::card::ResolveDolphinCardPath(aurora::card::ECardSlot::SlotA, GetCardRegion(), CARD_USE_GCI_FOLDER); + cardPaths[1] = aurora::card::ResolveDolphinCardPath(aurora::card::ECardSlot::SlotB, GetCardRegion(), CARD_USE_GCI_FOLDER); + + if (cardPaths[0].empty() && cardPaths[1].empty()) { + Log.error("Failed to detect Dolphin Card!"); + return; + } + + Log.info( + "Detected Dolphin Card at: {} and {}", + fs_path_to_string(cardPaths[0]), + fs_path_to_string(cardPaths[1])); + } +} + +void CARDSetBasePath(const char* path, const s32 chan) { + if (Initialized) { + Log.fatal("CARDSetBasePath() called after CARDInit()!"); + } + + std::filesystem::path filePath(path); + + if (filePath.has_filename() && !std::filesystem::is_directory(filePath)) { + filePath = filePath.remove_filename(); + Log.warn("Path supplied a filename, discarding. New Path: {}", fs_path_to_string(filePath)); + } + + if (chan == 0 || chan == 1) { + cardPaths[chan] = GetCardFullPath(filePath, static_cast(chan)); + } else { + cardPaths[0] = GetCardFullPath(filePath, aurora::card::ECardSlot::SlotA); + cardPaths[1] = GetCardFullPath(filePath, aurora::card::ECardSlot::SlotB); + } +} + +void CARDSetLoadType(CARDFileType type) { + SelectedFileType = type; +} + +void CARDInit(const char* game, const char* maker) { + if (Initialized) { + return; + } + Initialized = true; + Log.info("CARD API Initialized BUILT <{} {}>", __DATE__, __TIME__); + + for (int i = 0; i < CardChannels.size(); ++i) { + switch (SelectedFileType) { + case CARD_RAWIMAGE: + CardChannels[i] = std::make_unique(); + break; + case CARD_GCIFOLDER: + CardChannels[i] = std::make_unique(); + break; + } + CardChannels[i]->InitCard(game, maker); + } + + std::filesystem::path cardWorkingDir; + if (aurora::g_config.userPath != nullptr) + cardWorkingDir = reinterpret_cast(aurora::g_config.userPath); + else + cardWorkingDir = std::filesystem::current_path(); + + bool loadedCard = false; + + for (int i = 0; i < 2; ++i) { + // use default working directory if no path was supplied for card + if (cardPaths[i].empty()) { + cardPaths[i] = GetCardFullPath(cardWorkingDir, static_cast(i)); + } + + const auto& curPath = cardPaths[i]; + + std::error_code ec; + if (std::filesystem::exists(curPath, ec) && CardChannels[i]->open(curPath)) { + loadedCard = true; + Log.info("Loaded GC Card Image: {}", fs_path_to_string(curPath)); + } else if (ec) { + Log.warn("Failed to inspect GC Card path '{}': {}", fs_path_to_string(curPath), ec.message()); + } else if (std::filesystem::exists(curPath, ec)) { + Log.warn("Failed to load GC Card Image: {}", fs_path_to_string(curPath)); + } + } + + // create a SlotA card if no cards were loaded + if (!loadedCard) { + CardChannels[0]->open(cardPaths[0]); + CardChannels[0]->format(aurora::card::ECardSlot::SlotA); + CardChannels[0]->close(); + CardChannels[0]->open(cardPaths[0]); + } +} + +void CARDSetGameAndMaker(const s32 chan, const char* game, const char* maker) { + if (chan < 0 || chan >= 2) { + return; + } + + CardChannels[chan]->setCurrentGame(game); + CardChannels[chan]->setCurrentMaker(maker); +} + +// TODO: Investigate if this is necessary. Do some games throw an error if CARD's fast mode can't be use? +BOOL CARDGetFastMode() { return UseFastMode; } + +BOOL CARDSetFastMode(const BOOL enable) { + const bool oldFast = UseFastMode; + UseFastMode = enable; + return oldFast; +} + +s32 CARDCheck(const s32 chan) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + if (!CARD_READY(chan)) + return CARD_RESULT_NOCARD; + + const auto& card = GET_CARD(chan); + + return static_cast(card->getError()); +} + +s32 CARDCheckAsync(const s32 chan, const CARDCallback callback) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + if (!CARD_READY(chan)) + return CARD_RESULT_NOCARD; + + const auto& card = GET_CARD(chan); + const auto res = static_cast(card->getError()); + callback(chan, res); + return static_cast(card->getError()); +} + +s32 CARDCheckEx(const s32 chan, s32* xferBytes [[maybe_unused]]) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + if (!CARD_READY(chan)) + return CARD_RESULT_NOCARD; + + const auto& card = GET_CARD(chan); + + return static_cast(card->getError()); +} + +s32 CARDCheckExAsync(const s32 chan, s32* xferBytes [[maybe_unused]], const CARDCallback callback) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + const auto& card = GET_CARD(chan); + const auto res = static_cast(card->getError()); + callback(chan, res); + return static_cast(card->getError()); +} + +s32 CARDCreate(const s32 chan, const char* fileName, const u32 size, CARDFileInfo* fileInfo) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + if (!CARD_READY(chan)) + return CARD_RESULT_NOCARD; + const auto& card = GET_CARD(chan); + + aurora::card::FileHandle handle; + auto res = card->createFile(fileName, size, handle); + if (res == aurora::card::ECardResult::READY) { + CopyKabuFileHandleToDolphin(chan, handle, fileInfo); + card->commit(); + } else + Log.error("Failed to create file: {}", fileName); + + return static_cast(res); +} + +s32 CARDCreateAsync(const s32 chan, const char* fileName, const u32 size, CARDFileInfo* fileInfo, + const CARDCallback callback) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + const auto res = CARDCreate(chan, fileName, size, fileInfo); + callback(chan, res); + return res; +} + +s32 CARDDelete(const s32 chan, const char* fileName) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + if (!CARD_READY(chan)) + return CARD_RESULT_NOCARD; + + const auto& card = GET_CARD(chan); + const auto res = card->deleteFile(fileName); + + if (res != aurora::card::ECardResult::READY) { + Log.error("Failed to delete file: {}", fileName); + } else { + card->commit(); + } + + return static_cast(res); +} + +s32 CARDDeleteAsync(const s32 chan, const char* fileName, const CARDCallback callback) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + const auto res = CARDDelete(chan, fileName); + callback(chan, res); + return res; +} + +s32 CARDFastDelete(const s32 chan, const s32 fileNo) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + if (!CARD_READY(chan)) + return CARD_RESULT_NOCARD; + + const auto& card = GET_CARD(chan); + const auto res = card->deleteFile(fileNo); + if (res != aurora::card::ECardResult::READY) { + Log.error("Failed to delete file at idx: {}", fileNo); + } else { + card->commit(); + } + + return static_cast(res); +} + +s32 CARDFastDeleteAsync(const s32 chan, const s32 fileNo, const CARDCallback callback) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + const auto res = CARDFastDelete(chan, fileNo); + callback(chan, res); + return res; +} + +s32 CARDFastOpen(const s32 chan, const s32 fileNo, CARDFileInfo* fileInfo) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + if (!CARD_READY(chan)) + return CARD_RESULT_NOCARD; + + const auto& card = GET_CARD(chan); + + aurora::card::FileHandle handle; + const auto res = card->openFile(fileNo, handle); + if (res == aurora::card::ECardResult::READY) + CopyKabuFileHandleToDolphin(chan, handle, fileInfo); + else + Log.error("Failed to open file at idx: {}", fileNo); + + return static_cast(res); +} + +s32 CARDFormat(s32 chan) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + if (!CARD_READY(chan)) + return CARD_RESULT_NOCARD; + + const auto& card = GET_CARD(chan); + card->format(static_cast(chan)); + card->commit(); + return CARD_RESULT_READY; +} + +s32 CARDFormatAsync(const s32 chan, const CARDCallback callback) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + const auto res = CARDFormat(chan); + callback(chan, res); + return res; +} + +s32 CARDFreeBlocks(const s32 chan, s32* byteNotUsed, s32* filesNotUsed) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + if (!CARD_READY(chan)) + return CARD_RESULT_NOCARD; + + const auto& card = GET_CARD(chan); + card->getFreeBlocks(*byteNotUsed, *filesNotUsed); + return CARD_RESULT_READY; +} + +s32 CARDGetAttributes(const s32 chan, const s32 fileNo [[maybe_unused]], u8* attr [[maybe_unused]]) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + // TODO: + CARD_STUB + return CARD_RESULT_READY; +} + +s32 CARDGetEncoding(const s32 chan, u16* encode) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + if (!CARD_READY(chan)) + return CARD_RESULT_NOCARD; + + const auto& card = GET_CARD(chan); + card->getEncoding(*encode); + return CARD_RESULT_READY; +} + +s32 CARDGetMemSize(const s32 chan, u16* size [[maybe_unused]]) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + // TODO: + CARD_STUB + return CARD_RESULT_READY; +} + +s32 CARDGetResultCode(const s32 chan) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + if (!CARD_READY(chan)) + return CARD_RESULT_NOCARD; + + const auto& card = GET_CARD(chan); + return static_cast(card->getError()); +} + +s32 CARDGetSectorSize(const s32 chan, u32* size) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + if (!CARD_READY(chan)) + return CARD_RESULT_NOCARD; + + *size = CARD_SECTOR_SIZE; + return CARD_RESULT_READY; +} + +s32 CARDGetSerialNo(const s32 chan, u64* serialNo) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + if (!CARD_READY(chan)) + return CARD_RESULT_NOCARD; + + const auto& card = GET_CARD(chan); + card->getSerial(*serialNo); + return CARD_RESULT_READY; +} + +s32 __CARDGetStatusEx(const s32 chan, const s32 fileNo [[maybe_unused]], CARDDir* dirent [[maybe_unused]]) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + // TODO: + CARD_STUB + return CARD_RESULT_READY; +} + +s32 CARDGetStatus(const s32 chan, s32 fileNo, CARDStat* stat) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + if (!CARD_READY(chan)) + return CARD_RESULT_NOCARD; + const auto& card = GET_CARD(chan); + + aurora::card::CardStat kabuStat; + const auto res = card->getStatus(fileNo, kabuStat); + if (res == aurora::card::ECardResult::READY) + CopyKabuStatsToDolphin(kabuStat, stat); + else + Log.error("Failed to get status of file at idx: {}", fileNo); + + return static_cast(res); +} + +s32 CARDGetXferredBytes(const s32 chan) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + if (!CARD_READY(chan)) + return CARD_RESULT_NOCARD; + // TODO: + CARD_STUB + return CARD_RESULT_READY; +} +// these two funcs are out of scope for aurora::card. stubbed for now +s32 CARDMount(const s32 chan, void* workArea [[maybe_unused]], CARDCallback detachCallback [[maybe_unused]]) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + + return CARD_RESULT_READY; +} +s32 CARDMountAsync(const s32 chan, void* workArea [[maybe_unused]], const CARDCallback detachCallback [[maybe_unused]], + const CARDCallback attachCallback [[maybe_unused]]) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + return CARD_RESULT_READY; +} + +s32 CARDOpen(const s32 chan, const char* fileName, CARDFileInfo* fileInfo) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + if (!CARD_READY(chan)) + return CARD_RESULT_NOCARD; + const auto& card = GET_CARD(chan); + + aurora::card::FileHandle handle; + const auto res = card->openFile(fileName, handle); + if (res == aurora::card::ECardResult::READY) + CopyKabuFileHandleToDolphin(chan, handle, fileInfo); + else + Log.error("Failed to open file: {}", fileName); + + return static_cast(res); +} + +BOOL CARDProbe(const s32 chan) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + const auto& card = GET_CARD(chan); + + aurora::card::ProbeResults probeData = card->probeCardFile(cardPaths[chan]); + return probeData.x0_error != aurora::card::ECardResult::NOCARD ? TRUE : FALSE; +} + +s32 CARDProbeEx(const s32 chan, s32* memSize, s32* sectorSize) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + const auto& card = GET_CARD(chan); + + // ReSharper disable once CppUseStructuredBinding + const aurora::card::ProbeResults probeData = card->probeCardFile(cardPaths[chan]); + *memSize = probeData.x4_cardSize; + *sectorSize = probeData.x8_sectorSize; + + return static_cast(probeData.x0_error); +} + +s32 CARDRename(const s32 chan, const char* oldName, const char* newName) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + if (!CARD_READY(chan)) + return CARD_RESULT_NOCARD; + const auto& card = GET_CARD(chan); + + return static_cast(card->renameFile(oldName, newName)); +} + +s32 CARDRenameAsync(const s32 chan, const char* oldName, const char* newName, const CARDCallback callback) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + const auto res = CARDRename(chan, oldName, newName); + callback(chan, res); + return res; +} + +s32 CARDSetAttributesAsync(const s32 chan, s32 fileNo [[maybe_unused]], u8 attr [[maybe_unused]], + CARDCallback callback [[maybe_unused]]) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + // TODO: + CARD_STUB + return CARD_RESULT_READY; +} + +s32 CARDSetAttributes(const s32 chan, s32 fileNo [[maybe_unused]], u8 attr [[maybe_unused]]) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + // TODO: + CARD_STUB + return CARD_RESULT_READY; +} + +s32 CARDSetStatus(const s32 chan, s32 fileNo, const CARDStat* stat) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + if (!CARD_READY(chan)) + return CARD_RESULT_NOCARD; + const auto& card = GET_CARD(chan); + + aurora::card::CardStat kabuStat; + CopyDolphinStatsToKabu(kabuStat, stat); + auto res = card->setStatus(fileNo, kabuStat); + if (res != aurora::card::ECardResult::READY) { + Log.error("Failed to set status of file at idx: {}", fileNo); + } else { + card->commit(); + } + + return static_cast(res); +} + +s32 __CARDSetStatusEx(const s32 chan, s32 fileNo [[maybe_unused]], CARDDir* dirent [[maybe_unused]]) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + // TODO: + CARD_STUB + return CARD_RESULT_READY; +} + +s32 CARDSetStatusAsync(const s32 chan, const s32 fileNo, const CARDStat* stat, const CARDCallback callback) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + const auto res = CARDSetStatus(chan, fileNo, stat); + callback(chan, res); + return res; +} + +s32 CARDUnmount(const s32 chan) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + // TODO: + return CARD_RESULT_NOCARD; +} + +s32 CARDGetCurrentMode(const s32 chan, u32* mode [[maybe_unused]]) { + if (chan < 0 || chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + // TODO: + return CARD_RESULT_NOCARD; +} + +s32 CARDCancel(CARDFileInfo* fileInfo [[maybe_unused]]) { + // TODO:ge + return CARD_RESULT_NOCARD; +} + +s32 CARDClose(CARDFileInfo* fileInfo) { + if (fileInfo->chan < 0 || fileInfo->chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + if (!CARD_READY(fileInfo->chan)) + return CARD_RESULT_NOCARD; + const auto& card = GET_CARD(fileInfo->chan); + + auto handle = CreateKabuFileHandleFromDolphin(fileInfo); + const auto res = card->closeFile(handle); + + if (res != aurora::card::ECardResult::READY) + Log.error("Failed to close file at idx: {}", fileInfo->fileNo); + + return static_cast(res); +} + +s32 CARDRead(const CARDFileInfo* fileInfo, void* addr, s32 length, const s32 offset) { + if (fileInfo->chan < 0 || fileInfo->chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + if (!CARD_READY(fileInfo->chan)) + return CARD_RESULT_NOCARD; + const auto& card = GET_CARD(fileInfo->chan); + + aurora::card::FileHandle handle = CreateKabuFileHandleFromDolphin(fileInfo); + + card->seek(handle, offset, aurora::card::SeekOrigin::Begin); + auto res = card->fileRead(handle, addr, length); + + if (res != aurora::card::ECardResult::READY) + Log.error("Failed to read {} bytes from card", length); + + return static_cast(res); +} + +s32 CARDReadAsync(const CARDFileInfo* fileInfo, void* addr, const s32 length, const s32 offset, + const CARDCallback callback) { + const auto res = CARDRead(fileInfo, addr, length, offset); + callback(fileInfo->chan, res); + return res; +} + +s32 CARDWrite(const CARDFileInfo* fileInfo, const void* addr, const s32 length, const s32 offset) { + if (fileInfo->chan < 0 || fileInfo->chan >= 2) { + return CARD_RESULT_FATAL_ERROR; + } + if (!CARD_READY(fileInfo->chan)) + return CARD_RESULT_NOCARD; + const auto& card = GET_CARD(fileInfo->chan); + + aurora::card::FileHandle handle = CreateKabuFileHandleFromDolphin(fileInfo); + + card->seek(handle, offset, aurora::card::SeekOrigin::Begin); + auto res = card->fileWrite(handle, addr, length); + + if (res != aurora::card::ECardResult::READY) { + Log.error("Failed to write {} bytes to card", length); + } else { + card->commit(); + } + + return static_cast(res); +} + +s32 CARDWriteAsync(const CARDFileInfo* fileInfo, const void* addr, const s32 length, const s32 offset, + const CARDCallback callback) { + const auto res = CARDWrite(fileInfo, addr, length, offset); + callback(fileInfo->chan, res); + return res; +} +} diff --git a/aurora-main/lib/dolphin/dvd/dvd.cpp b/aurora-main/lib/dolphin/dvd/dvd.cpp new file mode 100644 index 0000000..9a054b5 --- /dev/null +++ b/aurora-main/lib/dolphin/dvd/dvd.cpp @@ -0,0 +1,1100 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../../internal.hpp" + +namespace { + +struct FSTEntry { + std::string name; + bool isDir = false; + u32 parent = 0; + u32 nextOrLength = 0; +}; + +struct IterateContext { + std::vector* entries = nullptr; + std::vector> dirStack; +}; + +NodHandle* s_disc = nullptr; +NodHandle* s_partition = nullptr; +std::vector s_fstEntries; +s32 s_currentDir = 0; +std::string s_currentPath = "/"; +BOOL s_autoInvalidation = FALSE; +BOOL s_autoFatalMessaging = FALSE; +DVDDiskID s_diskID = {}; +DVDLowCallback s_resetCoverCallback = nullptr; +bool s_initialized = false; + +void clearState() { + if (s_partition != nullptr) { + nod_free(s_partition); + s_partition = nullptr; + } + if (s_disc != nullptr) { + nod_free(s_disc); + s_disc = nullptr; + } + s_fstEntries.clear(); + s_currentDir = 0; + s_currentPath = "/"; + s_diskID = {}; + s_initialized = false; +} + +bool isValidEntryIndex(s32 entry) { return entry >= 0 && static_cast(entry) < s_fstEntries.size(); } + +bool isAligned(const void* addr, uintptr_t align) { + return (reinterpret_cast(addr) & (align - 1)) == 0; +} + +int64_t sdlStreamReadAt(void* userData, uint64_t offset, void* out, size_t len) { + auto* io = static_cast(userData); + if (io == nullptr || out == nullptr || + offset > static_cast(std::numeric_limits::max())) { + return -1; + } + + if (SDL_SeekIO(io, static_cast(offset), SDL_IO_SEEK_SET) < 0) { + return -1; + } + + size_t total = 0; + auto* dst = static_cast(out); + while (total < len) { + const size_t read = SDL_ReadIO(io, dst + total, len - total); + if (read == 0) { + break; + } + total += read; + } + return static_cast(total); +} + +int64_t sdlStreamLen(void* userData) { + auto* io = static_cast(userData); + if (io == nullptr) { + return -1; + } + + const Sint64 size = SDL_GetIOSize(io); + return size < 0 ? -1 : static_cast(size); +} + +void sdlStreamClose(void* userData) { + auto* io = static_cast(userData); + if (io == nullptr) { + return; + } + SDL_CloseIO(io); +} + +u32 fstCallback(u32 index, NodNodeKind kind, const char* name, u32 size, void* userData) { + auto* ctx = static_cast(userData); + + while (!ctx->dirStack.empty() && index >= ctx->dirStack.back().second) { + ctx->dirStack.pop_back(); + } + + if (ctx->entries->size() <= index) { + ctx->entries->resize(index + 1); + } + + FSTEntry& entry = (*ctx->entries)[index]; + entry.name = (name != nullptr) ? name : ""; + entry.isDir = (kind == NOD_NODE_KIND_DIRECTORY); + entry.parent = ctx->dirStack.empty() ? 0 : ctx->dirStack.back().first; + entry.nextOrLength = size; + + if (entry.isDir) { + ctx->dirStack.emplace_back(index, size); + } + + return index + 1; +} + +bool rebuildFST() { + if (s_partition == nullptr) { + return false; + } + + s_fstEntries.clear(); + IterateContext ctx{}; + ctx.entries = &s_fstEntries; + nod_partition_iterate_fst(s_partition, fstCallback, &ctx); + + if (s_fstEntries.empty()) { + FSTEntry root; + root.name = ""; + root.isDir = true; + root.parent = 0; + root.nextOrLength = 1; + s_fstEntries.push_back(std::move(root)); + } + + s_fstEntries[0].name.clear(); + s_fstEntries[0].isDir = true; + s_fstEntries[0].parent = 0; + if (s_fstEntries[0].nextOrLength < 1 || s_fstEntries[0].nextOrLength > s_fstEntries.size()) { + s_fstEntries[0].nextOrLength = static_cast(s_fstEntries.size()); + } + return true; +} + +bool nameEqualsIgnoreCase(const std::string& lhs, const char* rhs, size_t rhsLen) { + if (lhs.size() != rhsLen) { + return false; + } + for (size_t i = 0; i < rhsLen; ++i) { + char lc = lhs[i]; + char rc = rhs[i]; + if (lc >= 'a' && lc <= 'z') { + lc = static_cast(lc - 'a' + 'A'); + } + if (rc >= 'a' && rc <= 'z') { + rc = static_cast(rc - 'a' + 'A'); + } + if (lc != rc) { + return false; + } + } + return true; +} + +s32 findInDir(s32 dirEntry, const char* name, size_t nameLen) { + if (!isValidEntryIndex(dirEntry) || !s_fstEntries[dirEntry].isDir) { + return -1; + } + + u32 childEnd = s_fstEntries[dirEntry].nextOrLength; + u32 i = static_cast(dirEntry) + 1; + while (i < childEnd && i < s_fstEntries.size()) { + if (nameEqualsIgnoreCase(s_fstEntries[i].name, name, nameLen)) { + return static_cast(i); + } + + if (s_fstEntries[i].isDir) { + u32 next = s_fstEntries[i].nextOrLength; + i = (next > i) ? next : i + 1; + } else { + ++i; + } + } + return -1; +} + +std::string buildDirPath(s32 entryNum) { + if (entryNum <= 0 || !isValidEntryIndex(entryNum)) { + return "/"; + } + + std::vector parts; + s32 cur = entryNum; + while (cur > 0 && isValidEntryIndex(cur)) { + parts.push_back(s_fstEntries[cur].name); + s32 parent = static_cast(s_fstEntries[cur].parent); + if (parent == cur) { + break; + } + cur = parent; + } + + std::string out = "/"; + for (auto it = parts.rbegin(); it != parts.rend(); ++it) { + out += *it; + out += '/'; + } + return out; +} + +s32 readFromHandle(NodHandle* handle, void* out, s32 length, s32 offset, u32* transferredOut) { + if (transferredOut != nullptr) { + *transferredOut = 0; + } + if (handle == nullptr || out == nullptr || length < 0 || offset < 0) { + return DVD_RESULT_FATAL_ERROR; + } + if (length == 0) { + return 0; + } + if (nod_seek(handle, offset, 0) < 0) { + return DVD_RESULT_FATAL_ERROR; + } + + u8* writePtr = static_cast(out); + s32 totalRead = 0; + s32 remaining = length; + while (remaining > 0) { + const int64_t read = nod_read(handle, writePtr + totalRead, static_cast(remaining)); + if (read < 0) { + return DVD_RESULT_FATAL_ERROR; + } + if (read == 0) { + break; + } + totalRead += static_cast(read); + remaining -= static_cast(read); + } + + if (transferredOut != nullptr) { + *transferredOut = static_cast(totalRead); + } + return totalRead; +} + +void setCommandResult(DVDCommandBlock* block, s32 state, u32 transferred) { + if (block == nullptr) { + return; + } + block->state = state; + block->transferredSize = transferred; +} + +s32 stateForResult(s32 result) { + if (result == DVD_RESULT_CANCELED) { + return DVD_STATE_CANCELED; + } + if (result == DVD_RESULT_IGNORED) { + return DVD_STATE_IGNORED; + } + return result >= 0 ? DVD_STATE_END : DVD_STATE_FATAL_ERROR; +} + +bool isCommandBlockIdle(const DVDCommandBlock* block) { + return block != nullptr && block->state != DVD_STATE_BUSY && block->state != DVD_STATE_WAITING; +} + +NodHandle* getCommandHandle(DVDCommandBlock* block) { + if (block != nullptr && block->userData != nullptr) { + return static_cast(block->userData); + } + return s_disc; +} + +void beginCommand(DVDCommandBlock* block, u32 command, void* addr, u32 length, u32 offset, DVDCBCallback callback) { + if (block == nullptr) { + return; + } + block->command = command; + block->addr = addr; + block->length = length; + block->offset = offset; + block->transferredSize = 0; + block->callback = callback; + block->state = DVD_STATE_BUSY; +} + +void finishCommand(DVDCommandBlock* block, s32 result, u32 transferred) { + setCommandResult(block, stateForResult(result), transferred); +} + +int completeImmediateCommand(DVDCommandBlock* block, u32 command, s32 result, u32 transferred, DVDCBCallback callback) { + beginCommand(block, command, nullptr, 0, 0, callback); + finishCommand(block, result, transferred); + if (callback != nullptr) { + callback(result, block); + } + return TRUE; +} + +void cbForReadAsync(s32 result, DVDCommandBlock* block) { + auto* fileInfo = reinterpret_cast(reinterpret_cast(block) - offsetof(DVDFileInfo, cb)); + ASSERTLINE(0x2ED, &fileInfo->cb == block); + if (fileInfo->callback != nullptr) { + fileInfo->callback(result, fileInfo); + } +} + +void cbForSeekAsync(s32 result, DVDCommandBlock* block) { + auto* fileInfo = reinterpret_cast(reinterpret_cast(block) - offsetof(DVDFileInfo, cb)); + ASSERTLINE(0x383, &fileInfo->cb == block); + if (fileInfo->callback != nullptr) { + fileInfo->callback(result, fileInfo); + } +} + +void cbForPrepareStreamAsync(s32 result, DVDCommandBlock* block) { + auto* fileInfo = reinterpret_cast(reinterpret_cast(block) - offsetof(DVDFileInfo, cb)); + ASSERTLINE(0x497, &fileInfo->cb == block); + if (fileInfo->callback != nullptr) { + fileInfo->callback(result, fileInfo); + } +} + +} // namespace + +extern "C" { + +bool aurora_dvd_open(const char* disc_path) { + if (disc_path == nullptr) { + return false; + } + + clearState(); + + SDL_IOStream* io = SDL_IOFromFile(disc_path, "rb"); + if (io == nullptr) { + return false; + } + + const NodDiscStream stream{ + .user_data = io, + .read_at = sdlStreamReadAt, + .stream_len = sdlStreamLen, + .close = sdlStreamClose, + }; + const NodDiscOptions options{ + .preloader_threads = 1, + }; + + NodResult result = nod_disc_open_stream(&stream, &options, &s_disc); + if (result != NOD_RESULT_OK || s_disc == nullptr) { + clearState(); + return false; + } + + result = nod_disc_open_partition_kind(s_disc, NOD_PARTITION_KIND_DATA, nullptr, &s_partition); + if (result != NOD_RESULT_OK || s_partition == nullptr) { + clearState(); + return false; + } + + NodDiscHeader header{}; + if (nod_disc_header(s_disc, &header) == NOD_RESULT_OK) { + std::memcpy(s_diskID.gameName, header.game_id, sizeof(s_diskID.gameName)); + std::memcpy(s_diskID.company, header.game_id + sizeof(s_diskID.gameName), sizeof(s_diskID.company)); + s_diskID.diskNumber = header.disc_num; + s_diskID.gameVersion = header.disc_version; + s_diskID.streaming = header.audio_streaming; + s_diskID.streamingBufSize = header.audio_stream_buf_size; + + std::memcpy(aurora::g_gameName, s_diskID.gameName, sizeof(s_diskID.gameName)); + } + + if (!rebuildFST()) { + clearState(); + return false; + } + + s_currentDir = 0; + s_currentPath = "/"; + s_initialized = true; + return true; +} + +void aurora_dvd_close(void) { clearState(); } + +void DVDInit(void) {} + +const u8* DVDGetDOLLocation(s32* out_size) { + if (s_partition == nullptr) { + *out_size = 0; + return nullptr; + } + + NodPartitionMeta meta{}; + + if (nod_partition_meta(s_partition, &meta) != NOD_RESULT_OK) { + *out_size = 0; + return nullptr; + } + + *out_size = meta.raw_dol.size; + return meta.raw_dol.data; +} + +int DVDReadAbsAsyncPrio(DVDCommandBlock* block, void* addr, s32 length, s32 offset, DVDCBCallback callback, s32 prio) { + (void)prio; + ASSERTMSGLINE(0x780, block, "DVDReadAbsAsync(): null pointer is specified to command block address."); + ASSERTMSGLINE(0x781, addr, "DVDReadAbsAsync(): null pointer is specified to addr."); + ASSERTMSGLINE(0x783, isAligned(addr, 32), "DVDReadAbsAsync(): address must be aligned with 32 byte boundary."); + ASSERTMSGLINE(0x785, !(length & (32 - 1)), "DVDReadAbsAsync(): length must be a multiple of 32."); + ASSERTMSGLINE(0x787, !(offset & (4 - 1)), "DVDReadAbsAsync(): offset must be a multiple of 4."); + ASSERTMSGLINE(0x789, length >= 0, "DVD read: negative value was specified to length of the read\n"); + + beginCommand(block, DVD_COMMAND_READ, addr, static_cast(length), static_cast(offset), callback); + u32 transferred = 0; + s32 result = readFromHandle(getCommandHandle(block), addr, length, offset, &transferred); + finishCommand(block, result, transferred); + if (callback != nullptr) { + callback(result, block); + } + const bool idle = isCommandBlockIdle(block); + ASSERTMSGLINE(0x793, idle, "DVDReadAbsAsync(): command block is used for processing previous request."); + return TRUE; +} + +int DVDSeekAbsAsyncPrio(DVDCommandBlock* block, s32 offset, DVDCBCallback callback, s32 prio) { + (void)prio; + ASSERTMSGLINE(0x7AA, block, "DVDSeekAbs(): null pointer is specified to command block address."); + ASSERTMSGLINE(0x7AC, !(offset & (4 - 1)), "DVDSeekAbs(): offset must be a multiple of 4."); + + beginCommand(block, DVD_COMMAND_SEEK, nullptr, 0, static_cast(offset), callback); + NodHandle* handle = getCommandHandle(block); + const int64_t seek = handle != nullptr ? nod_seek(handle, static_cast(offset), 0) : -1; + const s32 result = (seek < 0) ? DVD_RESULT_FATAL_ERROR : DVD_RESULT_GOOD; + finishCommand(block, result, 0); + if (callback != nullptr) { + callback(result, block); + } + const bool idle = isCommandBlockIdle(block); + ASSERTMSGLINE(0x7B3, idle, "DVDSeekAbs(): command block is used for processing previous request."); + return TRUE; +} + +int DVDReadAbsAsyncForBS(DVDCommandBlock* block, void* addr, s32 length, s32 offset, DVDCBCallback callback) { + const int result = DVDReadAbsAsyncPrio(block, addr, length, offset, callback, 2); + if (result != FALSE && block != nullptr) { + block->command = DVD_COMMAND_BSREAD; + } + return result; +} + +int DVDReadDiskID(DVDCommandBlock* block, DVDDiskID* diskID, DVDCBCallback callback) { + if (diskID != nullptr) { + *diskID = s_diskID; + } + if (block != nullptr) { + setCommandResult(block, DVD_STATE_END, 0); + } + if (callback != nullptr) { + callback(DVD_RESULT_GOOD, block); + } + return TRUE; +} + +int DVDPrepareStreamAbsAsync(DVDCommandBlock* block, u32 length, u32 offset, DVDCBCallback callback) { + const bool idle = isCommandBlockIdle(block); + if (block == nullptr || !idle) { + return FALSE; + } + beginCommand(block, DVD_COMMAND_INITSTREAM, nullptr, length, offset, callback); + finishCommand(block, DVD_RESULT_IGNORED, 0); + if (callback != nullptr) { + callback(DVD_RESULT_IGNORED, block); + } + return TRUE; +} + +int DVDCancelStreamAsync(DVDCommandBlock* block, DVDCBCallback callback) { + if (block != nullptr) { + block->state = DVD_STATE_CANCELED; + } + if (callback != nullptr) { + callback(DVD_RESULT_CANCELED, block); + } + return TRUE; +} + +s32 DVDCancelStream(DVDCommandBlock* block) { + if (block != nullptr) { + block->state = DVD_STATE_CANCELED; + } + return DVD_RESULT_GOOD; +} + +int DVDStopStreamAtEndAsync(DVDCommandBlock* block, DVDCBCallback callback) { + if (block != nullptr) { + setCommandResult(block, DVD_STATE_END, 0); + } + if (callback != nullptr) { + callback(DVD_RESULT_GOOD, block); + } + return TRUE; +} + +s32 DVDStopStreamAtEnd(DVDCommandBlock* block) { + if (block != nullptr) { + setCommandResult(block, DVD_STATE_END, 0); + } + return DVD_RESULT_GOOD; +} + +int DVDGetStreamErrorStatusAsync(DVDCommandBlock* block, DVDCBCallback callback) { + if (block == nullptr || !isCommandBlockIdle(block)) { + return FALSE; + } + return completeImmediateCommand(block, DVD_COMMAND_REQUEST_AUDIO_ERROR, DVD_RESULT_IGNORED, 0, callback); +} + +s32 DVDGetStreamErrorStatus(DVDCommandBlock* block) { + (void)block; + return DVD_RESULT_IGNORED; +} + +int DVDGetStreamPlayAddrAsync(DVDCommandBlock* block, DVDCBCallback callback) { + if (block == nullptr || !isCommandBlockIdle(block)) { + return FALSE; + } + return completeImmediateCommand(block, DVD_COMMAND_REQUEST_PLAY_ADDR, DVD_RESULT_IGNORED, 0, callback); +} + +s32 DVDGetStreamPlayAddr(DVDCommandBlock* block) { + (void)block; + return 0; +} + +int DVDGetStreamStartAddrAsync(DVDCommandBlock* block, DVDCBCallback callback) { + if (block == nullptr || !isCommandBlockIdle(block)) { + return FALSE; + } + return completeImmediateCommand(block, DVD_COMMAND_REQUEST_START_ADDR, DVD_RESULT_IGNORED, 0, callback); +} + +s32 DVDGetStreamStartAddr(DVDCommandBlock* block) { + (void)block; + return 0; +} + +int DVDGetStreamLengthAsync(DVDCommandBlock* block, DVDCBCallback callback) { + if (block == nullptr || !isCommandBlockIdle(block)) { + return FALSE; + } + return completeImmediateCommand(block, DVD_COMMAND_REQUEST_LENGTH, DVD_RESULT_IGNORED, 0, callback); +} + +s32 DVDGetStreamLength(DVDCommandBlock* block) { + (void)block; + return 0; +} + +int DVDChangeDiskAsyncForBS(DVDCommandBlock* block, DVDCBCallback callback) { + ASSERTMSGLINE(0xA1F, block, "DVDChangeDiskAsyncForBS(): null pointer is specified to command block address."); + const bool idle = isCommandBlockIdle(block); + ASSERTMSGLINE(0xA25, idle, "DVDChangeDiskAsyncForBS(): command block is used for processing previous request."); + if (block == nullptr || !idle) { + return FALSE; + } + return completeImmediateCommand(block, DVD_COMMAND_BS_CHANGE_DISK, DVD_RESULT_IGNORED, 0, callback); +} + +int DVDChangeDiskAsync(DVDCommandBlock* block, DVDDiskID* id, DVDCBCallback callback) { + (void)id; + return DVDChangeDiskAsyncForBS(block, callback); +} + +s32 DVDChangeDisk(DVDCommandBlock* block, DVDDiskID* id) { + (void)block; + (void)id; + return DVD_RESULT_IGNORED; +} + +int DVDStopMotorAsync(DVDCommandBlock* block, DVDCBCallback callback) { + if (block != nullptr) { + setCommandResult(block, DVD_STATE_END, 0); + } + if (callback != nullptr) { + callback(DVD_RESULT_GOOD, block); + } + return TRUE; +} + +s32 DVDStopMotor(DVDCommandBlock* block) { + (void)block; + return DVD_RESULT_GOOD; +} + +int DVDInquiryAsync(DVDCommandBlock* block, DVDDriveInfo* info, DVDCBCallback callback) { + if (info != nullptr) { + std::memset(info, 0, sizeof(*info)); + } + if (block != nullptr) { + setCommandResult(block, DVD_STATE_END, 0); + } + if (callback != nullptr) { + callback(DVD_RESULT_GOOD, block); + } + return TRUE; +} + +s32 DVDInquiry(DVDCommandBlock* block, DVDDriveInfo* info) { + DVDInquiryAsync(block, info, nullptr); + return DVD_RESULT_GOOD; +} + +void DVDReset(void) {} + +int DVDResetRequired(void) { return FALSE; } + +s32 DVDGetCommandBlockStatus(const DVDCommandBlock* block) { + if (block == nullptr) { + return DVD_STATE_END; + } + return block->state; +} + +s32 DVDGetDriveStatus(void) { return s_initialized ? DVD_STATE_END : DVD_STATE_NO_DISK; } + +BOOL DVDSetAutoInvalidation(BOOL autoInval) { + BOOL prev = s_autoInvalidation; + s_autoInvalidation = autoInval; + return prev; +} + +void DVDPause(void) {} + +void DVDResume(void) {} + +int DVDCancelAsync(DVDCommandBlock* block, DVDCBCallback callback) { + if (block != nullptr) { + block->state = DVD_STATE_CANCELED; + } + if (callback != nullptr) { + callback(DVD_RESULT_GOOD, block); + } + return TRUE; +} + +s32 DVDCancel(volatile DVDCommandBlock* block) { + if (block != nullptr) { + block->state = DVD_STATE_CANCELED; + } + return DVD_RESULT_GOOD; +} + +int DVDCancelAllAsync(DVDCBCallback callback) { + if (callback != nullptr) { + callback(DVD_RESULT_CANCELED, nullptr); + } + return TRUE; +} + +s32 DVDCancelAll(void) { return DVD_RESULT_GOOD; } + +DVDDiskID* DVDGetCurrentDiskID(void) { return &s_diskID; } + +BOOL DVDCheckDisk(void) { return s_initialized ? TRUE : FALSE; } + +int DVDSetAutoFatalMessaging(BOOL enable) { + const BOOL prev = s_autoFatalMessaging; + s_autoFatalMessaging = enable; + return prev; +} + +s32 DVDConvertPathToEntrynum(const char* pathPtr) { + if (!s_initialized || pathPtr == nullptr || s_fstEntries.empty()) { + return -1; + } + + s32 current = 0; + const char* p = pathPtr; + if (*p == '/') { + ++p; + } else { + current = s_currentDir; + } + + while (*p != '\0') { + while (*p == '/') { + ++p; + } + if (*p == '\0') { + break; + } + + if (!isValidEntryIndex(current) || !s_fstEntries[current].isDir) { + return -1; + } + + const char* compEnd = p; + while (*compEnd != '\0' && *compEnd != '/') { + ++compEnd; + } + size_t compLen = static_cast(compEnd - p); + + if (compLen == 1 && p[0] == '.') { + // no-op + } else if (compLen == 2 && p[0] == '.' && p[1] == '.') { + current = static_cast(s_fstEntries[current].parent); + } else { + const s32 found = findInDir(current, p, compLen); + if (found < 0) { + return -1; + } + current = found; + } + p = compEnd; + } + + return current; +} + +BOOL DVDFastOpen(s32 entrynum, DVDFileInfo* fileInfo) { + if (!s_initialized || fileInfo == nullptr || !isValidEntryIndex(entrynum) || s_partition == nullptr) { + return FALSE; + } + if (s_fstEntries[entrynum].isDir) { + return FALSE; + } + + std::memset(fileInfo, 0, sizeof(*fileInfo)); + fileInfo->startAddr = 0; + fileInfo->length = s_fstEntries[entrynum].nextOrLength; + + NodHandle* handle = nullptr; + NodResult result = nod_partition_open_file(s_partition, static_cast(entrynum), &handle); + if (result != NOD_RESULT_OK || handle == nullptr) { + return FALSE; + } + + fileInfo->cb.userData = handle; + fileInfo->cb.state = DVD_STATE_END; + return TRUE; +} + +BOOL DVDOpen(const char* fileName, DVDFileInfo* fileInfo) { + s32 entrynum = DVDConvertPathToEntrynum(fileName); + if (entrynum < 0) { + return FALSE; + } + return DVDFastOpen(entrynum, fileInfo); +} + +BOOL DVDClose(DVDFileInfo* fileInfo) { + if (fileInfo == nullptr) { + return FALSE; + } + if (fileInfo->cb.userData != nullptr) { + nod_free(static_cast(fileInfo->cb.userData)); + fileInfo->cb.userData = nullptr; + } + fileInfo->cb.state = DVD_STATE_END; + return TRUE; +} + +BOOL DVDGetCurrentDir(char* path, u32 maxlen) { + if (path == nullptr || maxlen == 0) { + return FALSE; + } + const size_t len = s_currentPath.size(); + const size_t copyLen = (len >= maxlen) ? (maxlen - 1) : len; + std::memcpy(path, s_currentPath.c_str(), copyLen); + path[copyLen] = '\0'; + return TRUE; +} + +BOOL DVDChangeDir(const char* dirName) { + s32 entry = DVDConvertPathToEntrynum(dirName); + if (!isValidEntryIndex(entry) || !s_fstEntries[entry].isDir) { + return FALSE; + } + s_currentDir = entry; + s_currentPath = buildDirPath(entry); + return TRUE; +} + +BOOL DVDReadAsyncPrio(DVDFileInfo* fileInfo, void* addr, s32 length, s32 offset, DVDCallback callback, s32 prio) { + ASSERTMSGLINE(0x2C7, fileInfo, "DVDReadAsync(): null pointer is specified to file info address "); + ASSERTMSGLINE(0x2C8, addr, "DVDReadAsync(): null pointer is specified to addr "); + + ASSERTMSGLINE(0x2D5, (0 <= offset) && (offset <= static_cast(fileInfo->length)), + "DVDReadAsync(): specified area is out of the file "); + ASSERTMSGLINE(0x2DB, (0 <= offset + length) && (offset + length < static_cast(fileInfo->length) + DVD_MIN_TRANSFER_SIZE), + "DVDReadAsync(): specified area is out of the file "); + + fileInfo->callback = callback; + DVDReadAbsAsyncPrio(&fileInfo->cb, addr, length, offset, cbForReadAsync, prio); + return TRUE; +} + +s32 DVDReadPrio(DVDFileInfo* fileInfo, void* addr, s32 length, s32 offset, s32 prio) { + if (!DVDReadAsyncPrio(fileInfo, addr, length, offset, nullptr, prio)) { + return DVD_RESULT_FATAL_ERROR; + } + if (fileInfo->cb.state == DVD_STATE_END) { + return static_cast(fileInfo->cb.transferredSize); + } + if (fileInfo->cb.state == DVD_STATE_CANCELED) { + return DVD_RESULT_CANCELED; + } + return DVD_RESULT_FATAL_ERROR; +} + +int DVDSeekAsyncPrio(DVDFileInfo* fileInfo, s32 offset, void (*callback)(s32, DVDFileInfo*), s32 prio) { + ASSERTMSGLINE(0x368, fileInfo, "DVDSeek(): null pointer is specified to file info address "); + ASSERTMSGLINE(0x36C, !(offset & 3), "DVDSeek(): offset must be multiple of 4 byte "); + + ASSERTMSGLINE(0x371, (0 <= offset) && (offset <= static_cast(fileInfo->length)), + "DVDSeek(): offset is out of the file "); + + fileInfo->callback = callback; + DVDSeekAbsAsyncPrio(&fileInfo->cb, offset, cbForSeekAsync, prio); + return 1; +} + +s32 DVDSeekPrio(DVDFileInfo* fileInfo, s32 offset, s32 prio) { + if (!DVDSeekAsyncPrio(fileInfo, offset, nullptr, prio)) { + return DVD_RESULT_FATAL_ERROR; + } + if (fileInfo->cb.state == DVD_STATE_END) { + return DVD_RESULT_GOOD; + } + if (fileInfo->cb.state == DVD_STATE_CANCELED) { + return DVD_RESULT_CANCELED; + } + return DVD_RESULT_FATAL_ERROR; +} + +s32 DVDGetFileInfoStatus(const DVDFileInfo* fileInfo) { + if (fileInfo == nullptr) { + return DVD_STATE_END; + } + return fileInfo->cb.state; +} + +BOOL DVDFastOpenDir(s32 entrynum, DVDDir* dir) { + if (!isValidEntryIndex(entrynum) || dir == nullptr || !s_fstEntries[entrynum].isDir) { + return FALSE; + } + dir->entryNum = static_cast(entrynum); + dir->location = static_cast(entrynum) + 1; + dir->next = s_fstEntries[entrynum].nextOrLength; + return TRUE; +} + +int DVDOpenDir(const char* dirName, DVDDir* dir) { + s32 entrynum = DVDConvertPathToEntrynum(dirName); + if (entrynum < 0) { + return FALSE; + } + return DVDFastOpenDir(entrynum, dir); +} + +int DVDReadDir(DVDDir* dir, DVDDirEntry* dirent) { + if (dir == nullptr || dirent == nullptr) { + return FALSE; + } + if (dir->location >= dir->next || dir->location >= s_fstEntries.size()) { + return FALSE; + } + + const u32 index = dir->location; + FSTEntry& entry = s_fstEntries[index]; + dirent->entryNum = index; + dirent->isDir = entry.isDir ? TRUE : FALSE; + dirent->name = entry.name.empty() ? nullptr : entry.name.data(); + + if (entry.isDir) { + const u32 next = entry.nextOrLength; + dir->location = (next > index) ? next : index + 1; + } else { + dir->location = index + 1; + } + return TRUE; +} + +int DVDCloseDir(DVDDir* dir) { + (void)dir; + return TRUE; +} + +void DVDRewindDir(DVDDir* dir) { + if (dir == nullptr) { + return; + } + dir->location = dir->entryNum + 1; +} + +void* DVDGetFSTLocation(void) { + if (s_fstEntries.empty()) { + return nullptr; + } + return s_fstEntries.data(); +} + +BOOL DVDPrepareStreamAsync(DVDFileInfo* fileInfo, u32 length, u32 offset, DVDCallback callback) { + ASSERTMSGLINE(0x46C, fileInfo, "DVDPrepareStreamAsync(): NULL file info was specified"); + if (fileInfo == nullptr || fileInfo->cb.userData == nullptr) { + return FALSE; + } + if (length == 0) { + length = fileInfo->length - offset; + } + const uint64_t end = static_cast(offset) + static_cast(length); + if (!(offset < fileInfo->length && end <= fileInfo->length)) { + OSPanic(__FILE__, 0x484, + "DVDPrepareStreamAsync(): The area specified (offset(0x%x), length(0x%x)) is out of the file", offset, + length); + return FALSE; + } + fileInfo->callback = callback; + return DVDPrepareStreamAbsAsync(&fileInfo->cb, length, offset, cbForPrepareStreamAsync); +} + +s32 DVDPrepareStream(DVDFileInfo* fileInfo, u32 length, u32 offset) { + (void)fileInfo; + (void)length; + (void)offset; + return DVD_RESULT_IGNORED; +} + +s32 DVDGetTransferredSize(DVDFileInfo* fileinfo) { + if (fileinfo == nullptr) { + return 0; + } + return static_cast(fileinfo->cb.transferredSize); +} + +int DVDCompareDiskID(const DVDDiskID* id1, const DVDDiskID* id2) { + if (id1 == nullptr || id2 == nullptr) { + return FALSE; + } + + if (id1->gameName[0] != 0 && id2->gameName[0] != 0 && std::memcmp(id1->gameName, id2->gameName, 4) != 0) { + return 0; + } + + if (id1->company[0] == 0 || id2->company[0] == 0 || std::memcmp(id1->company, id2->company, 2) != 0) { + return 0; + } + + if (id1->diskNumber != 0xFF && id2->diskNumber != 0xFF && id1->diskNumber != id2->diskNumber) { + return 0; + } + + if (id1->gameVersion != 0xFF && id2->gameVersion != 0xFF && id1->gameVersion != id2->gameVersion) { + return 0; + } + + return 1; +} + +DVDDiskID* DVDGenerateDiskID(DVDDiskID* id, const char* game, const char* company, u8 diskNum, u8 version) { + if (id == nullptr) { + return nullptr; + } + std::memset(id, 0, sizeof(*id)); + if (game != nullptr) { + std::memcpy(id->gameName, game, 4); + } + if (company != nullptr) { + std::memcpy(id->company, company, 2); + } + id->diskNumber = diskNum; + id->gameVersion = version; + return id; +} + +BOOL DVDLowRead(void* addr, u32 length, u32 offset, DVDLowCallback callback) { + u32 transferred = 0; + s32 result = readFromHandle(s_disc, addr, static_cast(length), static_cast(offset), &transferred); + if (callback != nullptr) { + callback(static_cast((result >= 0) ? DVD_RESULT_GOOD : DVD_RESULT_FATAL_ERROR)); + } + return TRUE; +} + +BOOL DVDLowSeek(u32 offset, DVDLowCallback callback) { + const int64_t seek = s_disc != nullptr ? nod_seek(s_disc, static_cast(offset), 0) : -1; + if (callback != nullptr) { + callback(static_cast((seek >= 0) ? DVD_RESULT_GOOD : DVD_RESULT_FATAL_ERROR)); + } + return TRUE; +} + +BOOL DVDLowWaitCoverClose(DVDLowCallback callback) { + if (callback != nullptr) { + callback(0); + } + return TRUE; +} + +BOOL DVDLowReadDiskID(DVDDiskID* diskID, DVDLowCallback callback) { + if (diskID != nullptr) { + *diskID = s_diskID; + } + if (callback != nullptr) { + callback(0); + } + return TRUE; +} + +BOOL DVDLowStopMotor(DVDLowCallback callback) { + if (callback != nullptr) { + callback(0); + } + return TRUE; +} + +BOOL DVDLowRequestError(DVDLowCallback callback) { + if (callback != nullptr) { + callback(0); + } + return TRUE; +} + +BOOL DVDLowInquiry(DVDDriveInfo* info, DVDLowCallback callback) { + if (info != nullptr) { + std::memset(info, 0, sizeof(*info)); + } + if (callback != nullptr) { + callback(0); + } + return TRUE; +} + +BOOL DVDLowAudioStream(u32 subcmd, u32 length, u32 offset, DVDLowCallback callback) { + (void)subcmd; + (void)length; + (void)offset; + if (callback != nullptr) { + callback(0); + } + return TRUE; +} + +BOOL DVDLowRequestAudioStatus(u32 subcmd, DVDLowCallback callback) { + (void)subcmd; + if (callback != nullptr) { + callback(0); + } + return TRUE; +} + +BOOL DVDLowAudioBufferConfig(BOOL enable, u32 size, DVDLowCallback callback) { + (void)enable; + (void)size; + if (callback != nullptr) { + callback(0); + } + return TRUE; +} + +void DVDLowReset(void) { + if (s_resetCoverCallback != nullptr) { + s_resetCoverCallback(0); + } +} + +DVDLowCallback DVDLowSetResetCoverCallback(DVDLowCallback callback) { + DVDLowCallback previous = s_resetCoverCallback; + s_resetCoverCallback = callback; + return previous; +} + +BOOL DVDLowBreak(void) { return TRUE; } + +DVDLowCallback DVDLowClearCallback(void) { + DVDLowCallback previous = s_resetCoverCallback; + s_resetCoverCallback = nullptr; + return previous; +} + +u32 DVDLowGetCoverStatus(void) { return s_initialized ? 0 : 1; } + +void DVDDumpWaitingQueue(void) {} + +} // extern "C" diff --git a/aurora-main/lib/dolphin/gd/GDAurora.cpp b/aurora-main/lib/dolphin/gd/GDAurora.cpp new file mode 100644 index 0000000..14bbbee --- /dev/null +++ b/aurora-main/lib/dolphin/gd/GDAurora.cpp @@ -0,0 +1,32 @@ +#include + +#include "gd.hpp" +#include "dolphin/gd/GDAurora.h" +#include "dolphin/gd/GDBase.h" +#include "dolphin/gx/GXAurora.h" + +static void GDWriteString(const char* label) { + auto length = strlen(label); + + if (length > std::numeric_limits::max()) { + Log.warn("Debug marker size over u16 max, truncating"); + length = std::numeric_limits::max(); + } + + GDWrite_u16(length); + GDWrite_data(label, length); +} + +void GDPushDebugGroup(const char* label) { + GDWriteAuroraCmd(GX_LOAD_AURORA_DEBUG_GROUP_PUSH); + GDWriteString(label); +} + +void GDPopDebugGroup() { + GDWriteAuroraCmd(GX_LOAD_AURORA_DEBUG_GROUP_POP); +} + +void GDInsertDebugMarker(const char* label) { + GDWriteAuroraCmd(GX_LOAD_AURORA_DEBUG_MARKER_INSERT); + GDWriteString(label); +} diff --git a/aurora-main/lib/dolphin/gd/GDBase.cpp b/aurora-main/lib/dolphin/gd/GDBase.cpp new file mode 100644 index 0000000..da0bdb5 --- /dev/null +++ b/aurora-main/lib/dolphin/gd/GDBase.cpp @@ -0,0 +1,46 @@ +#include +#include +#include "gd.hpp" + +GDLObj* __GDCurrentDL = NULL; +static GDOverflowCb overflowcb = NULL; + +void GDInitGDLObj(GDLObj* dl, void* start, u32 length) { + ASSERTMSGLINE(40, !((u32)start & 0x1F), "start must be aligned to 32 bytes"); + ASSERTMSGLINE(41, !((u32)length & 0x1F), "length must be aligned to 32 bytes"); + dl->start = (u8*)start; + dl->ptr = (u8*)start; + dl->top = (u8*)start + length; + dl->length = length; +} + +void GDFlushCurrToMem(void) { + DCFlushRange(__GDCurrentDL->start, __GDCurrentDL->length); +} + +void GDPadCurr32(void) { + uintptr_t n; + + n = (uintptr_t)__GDCurrentDL->ptr & 0x1F; + if (n != 0) { + for (; n < 32; n = n + 1) { + __GDWrite(0); + } + } +} + +void GDOverflowed(void) { + if (overflowcb) { + (*overflowcb)(); + } else { + Log.fatal("GDWrite: display list overflowed"); + } +} + +void GDSetOverflowCallback(GDOverflowCb callback) { + overflowcb = callback; +} + +GDOverflowCb GDGetOverflowCallback(void) { + return overflowcb; +} diff --git a/aurora-main/lib/dolphin/gd/GDGeometry.cpp b/aurora-main/lib/dolphin/gd/GDGeometry.cpp new file mode 100644 index 0000000..194a1b5 --- /dev/null +++ b/aurora-main/lib/dolphin/gd/GDGeometry.cpp @@ -0,0 +1,435 @@ +#include +#include +#include + +#include "dolphin/gx/GXAurora.h" + +void GDSetVtxDescv(const GXVtxDescList* attrPtr) { + u32 nnorms = 0; + u32 ncols = 0; + u32 ntexs = 0; + u32 pnMtxIdx = GX_NONE; + u32 txMtxIdxMask = 0; + u32 posn = GX_DIRECT; + u32 norm = GX_NONE; + u32 col0 = GX_NONE; + u32 col1 = GX_NONE; + u32 tex0 = GX_NONE; + u32 tex1 = GX_NONE; + u32 tex2 = GX_NONE; + u32 tex3 = GX_NONE; + u32 tex4 = GX_NONE; + u32 tex5 = GX_NONE; + u32 tex6 = GX_NONE; + u32 tex7 = GX_NONE; + + for (; attrPtr->attr != GX_VA_NULL; ++attrPtr) { + ASSERTMSGLINE(91, attrPtr->attr >= GX_VA_PNMTXIDX && attrPtr->attr <= GX_VA_MAX_ATTR, "GDSetVtxDescv: invalid attribute"); + ASSERTMSGLINE(95, attrPtr->type >= GX_NONE && attrPtr->type <= GX_INDEX16, "GDSetVtxDescv: invalid type"); + + ASSERTMSGLINE(101, attrPtr->attr >= GX_VA_PNMTXIDX && attrPtr->attr <= GX_VA_TEX7MTXIDX ? (attrPtr->type == 0 || attrPtr->type == 1) : TRUE, "GDSetVtxDescv: invalid type for given attribute"); + + switch (attrPtr->attr) { + case GX_VA_PNMTXIDX: + pnMtxIdx = attrPtr->type; + break; + case GX_VA_TEX0MTXIDX: + txMtxIdxMask = (txMtxIdxMask & ~0x1) | (attrPtr->type << 0); + break; + case GX_VA_TEX1MTXIDX: + txMtxIdxMask = (txMtxIdxMask & ~0x2) | (attrPtr->type << 1); + break; + case GX_VA_TEX2MTXIDX: + txMtxIdxMask = (txMtxIdxMask & ~0x4) | (attrPtr->type << 2); + break; + case GX_VA_TEX3MTXIDX: + txMtxIdxMask = (txMtxIdxMask & ~0x8) | (attrPtr->type << 3); + break; + case GX_VA_TEX4MTXIDX: + txMtxIdxMask = (txMtxIdxMask & ~0x10) | (attrPtr->type << 4); + break; + case GX_VA_TEX5MTXIDX: + txMtxIdxMask = (txMtxIdxMask & ~0x20) | (attrPtr->type << 5); + break; + case GX_VA_TEX6MTXIDX: + txMtxIdxMask = (txMtxIdxMask & ~0x40) | (attrPtr->type << 6); + break; + case GX_VA_TEX7MTXIDX: + txMtxIdxMask = (txMtxIdxMask & ~0x80) | (attrPtr->type << 7); + break; + case GX_VA_POS: + posn = attrPtr->type; + break; + case GX_VA_NRM: + if (attrPtr->type != GX_NONE) { + norm = attrPtr->type; + nnorms = 1; + } + break; + case GX_VA_NBT: + if (attrPtr->type != GX_NONE) { + norm = attrPtr->type; + nnorms = 2; + } + break; + case GX_VA_CLR0: + col0 = attrPtr->type; + ncols += (col0 != GX_NONE); + break; + case GX_VA_CLR1: + col1 = attrPtr->type; + ncols += (col1 != GX_NONE); + break; + case GX_VA_TEX0: + tex0 = attrPtr->type; + ntexs += (tex0 != GX_NONE); + break; + case GX_VA_TEX1: + tex1 = attrPtr->type; + ntexs += (tex1 != GX_NONE); + break; + case GX_VA_TEX2: + tex2 = attrPtr->type; + ntexs += (tex2 != GX_NONE); + break; + case GX_VA_TEX3: + tex3 = attrPtr->type; + ntexs += (tex3 != GX_NONE); + break; + case GX_VA_TEX4: + tex4 = attrPtr->type; + ntexs += (tex4 != GX_NONE); + break; + case GX_VA_TEX5: + tex5 = attrPtr->type; + ntexs += (tex5 != GX_NONE); + break; + case GX_VA_TEX6: + tex6 = attrPtr->type; + ntexs += (tex6 != GX_NONE); + break; + case GX_VA_TEX7: + tex7 = attrPtr->type; + ntexs += (tex7 != GX_NONE); + break; + } + } + + GDWriteCPCmd(CP_REG_VCD_LO_ID, CP_REG_VCD_LO(pnMtxIdx, txMtxIdxMask, posn, norm, col0, col1)); + GDWriteCPCmd(CP_REG_VCD_HI_ID, CP_REG_VCD_HI(tex0, tex1, tex2, tex3, tex4, tex5, tex6, tex7)); + GDWriteXFCmd(XF_REG_INVERTEXSPEC_ID, XF_REG_INVTXSPEC(ncols, nnorms, ntexs)); +} + +void GDSetVtxAttrFmtv(GXVtxFmt vtxfmt, const GXVtxAttrFmtList* list) { + u32 posCnt = GX_POS_XYZ; + u32 posType = GX_F32; + u32 posFrac = 0; + + u32 nrmCnt = GX_NRM_XYZ; + u32 nrmType = GX_F32; + u32 nrmIdx3 = 0; + + u32 c0Cnt = GX_CLR_RGBA; + u32 c0Type = GX_RGBA8; + + u32 c1Cnt = GX_CLR_RGBA; + u32 c1Type = GX_RGBA8; + + u32 tx0Cnt = GX_TEX_ST; + u32 tx0Type = GX_F32; + u32 tx0Frac = 0; + + u32 tx1Cnt = GX_TEX_ST; + u32 tx1Type = GX_F32; + u32 tx1Frac = 0; + + u32 tx2Cnt = GX_TEX_ST; + u32 tx2Type = GX_F32; + u32 tx2Frac = 0; + + u32 tx3Cnt = GX_TEX_ST; + u32 tx3Type = GX_F32; + u32 tx3Frac = 0; + + u32 tx4Cnt = GX_TEX_ST; + u32 tx4Type = GX_F32; + u32 tx4Frac = 0; + + u32 tx5Cnt = GX_TEX_ST; + u32 tx5Type = GX_F32; + u32 tx5Frac = 0; + + u32 tx6Cnt = GX_TEX_ST; + u32 tx6Type = GX_F32; + u32 tx6Frac = 0; + + u32 tx7Cnt = GX_TEX_ST; + u32 tx7Type = GX_F32; + u32 tx7Frac = 0; + + ASSERTMSGLINE(221, vtxfmt < GX_MAX_VTXFMT, "GDSetVtxAttrFmtv: invalid vtx fmt"); + + for (; list->attr != GX_VA_NULL; ++list) { + ASSERTMSGLINE(226, list->attr >= GX_VA_POS && list->attr <= GX_VA_TEX7, "GDSetVtxAttrFmtv: invalid attribute"); + ASSERTMSGLINE(227, list->frac < 32, "GDSetVtxAttrFmtv: invalid frac value"); + + switch (list->attr) { + case GX_VA_POS: + posCnt = list->cnt; + posType = list->type; + posFrac = list->frac; + break; + case GX_VA_NRM: + case GX_VA_NBT: + nrmType = list->type; + if (list->cnt == GX_NRM_NBT3) { + nrmCnt = GX_NRM_NBT; + nrmIdx3 = 1; + } else { + nrmCnt = list->cnt; + nrmIdx3 = 0; + } + break; + case GX_VA_CLR0: + c0Cnt = list->cnt; + c0Type = list->type; + break; + case GX_VA_CLR1: + c1Cnt = list->cnt; + c1Type = list->type; + break; + case GX_VA_TEX0: + tx0Cnt = list->cnt; + tx0Type = list->type; + tx0Frac = list->frac; + break; + case GX_VA_TEX1: + tx1Cnt = list->cnt; + tx1Type = list->type; + tx1Frac = list->frac; + break; + case GX_VA_TEX2: + tx2Cnt = list->cnt; + tx2Type = list->type; + tx2Frac = list->frac; + break; + case GX_VA_TEX3: + tx3Cnt = list->cnt; + tx3Type = list->type; + tx3Frac = list->frac; + break; + case GX_VA_TEX4: + tx4Cnt = list->cnt; + tx4Type = list->type; + tx4Frac = list->frac; + break; + case GX_VA_TEX5: + tx5Cnt = list->cnt; + tx5Type = list->type; + tx5Frac = list->frac; + break; + case GX_VA_TEX6: + tx6Cnt = list->cnt; + tx6Type = list->type; + tx6Frac = list->frac; + break; + case GX_VA_TEX7: + tx7Cnt = list->cnt; + tx7Type = list->type; + tx7Frac = list->frac; + } + } + + GDWriteCPCmd(vtxfmt + CP_REG_VAT_GRP0_ID, CP_REG_VAT_GRP0(posCnt, posType, posFrac, nrmCnt, nrmType, c0Cnt, c0Type, c1Cnt, c1Type, tx0Cnt, tx0Type, tx0Frac, 1, nrmIdx3)); + GDWriteCPCmd(vtxfmt + CP_REG_VAT_GRP1_ID, CP_REG_VAT_GRP1(tx1Cnt, tx1Type, tx1Frac, tx2Cnt, tx2Type, tx2Frac, tx3Cnt, tx3Type, tx3Frac, tx4Cnt, tx4Type, 1)); + GDWriteCPCmd(vtxfmt + CP_REG_VAT_GRP2_ID, CP_REG_VAT_GRP2(tx4Frac, tx5Cnt, tx5Type, tx5Frac, tx6Cnt, tx6Type, tx6Frac, tx7Cnt, tx7Type, tx7Frac)); +} + +void GDSetArraySized(GXAttr attr, void* base_ptr, u32 size, u8 stride, bool le) { + s32 cpAttr; + if (attr == GX_VA_NBT) { + cpAttr = 1; + } else { + cpAttr = attr - GX_VA_POS; + } + + assert((cpAttr & ~0xF) == 0); + + GDWriteAuroraCmd(cpAttr + GX_LOAD_AURORA_ARRAYBASE); + GDWrite_u64((u64)base_ptr); + GDWrite_u32(size); + GDWrite_u8(le ? 1 : 0); + + GDWriteCPCmd(cpAttr + CP_REG_ARRAYSTRIDE_ID, stride); +} + +void GDSetArray(GXAttr, void*, u8) { + OSPanic(__FILE__, __LINE__, "GDSetArray is not supported on Aurora"); +} + +void GDSetArrayRaw(GXAttr, u32, u8) { + OSPanic(__FILE__, __LINE__, "GDSetArrayRaw is not supported on Aurora"); +} + +void GDPatchArrayPtr(void* base_ptr) { + GDWrite_u32(OSCachedToPhysical(base_ptr)); +} + +void GDSetTexCoordGen(GXTexCoordID dst_coord, GXTexGenType func, GXTexGenSrc src_param, u8 normalize, u32 postmtx) { + u32 form; + u32 tgType; + u32 proj; + u32 row; + u32 embossRow; + u32 embossLit; + + form = 0; + proj = 0; + row = 5; + embossRow = 5; + embossLit = 0; + + ASSERTMSGLINE(432, dst_coord < GX_MAX_TEXCOORD, "GDSetTexCoordGen: invalid texcoord ID"); + + switch(src_param) { + case GX_TG_POS: + row = 0; + form = 1; + break; + case GX_TG_NRM: + row = 1; + form = 1; + break; + case GX_TG_BINRM: + row = 3; + form = 1; + break; + case GX_TG_TANGENT: + row = 4; + form = 1; + break; + case GX_TG_COLOR0: + row = 2; + break; + case GX_TG_COLOR1: + row = 2; + break; + case GX_TG_TEX0: + row = 5; + break; + case GX_TG_TEX1: + row = 6; + break; + case GX_TG_TEX2: + row = 7; + break; + case GX_TG_TEX3: + row = 8; + break; + case GX_TG_TEX4: + row = 9; + break; + case GX_TG_TEX5: + row = 10; + break; + case GX_TG_TEX6: + row = 11; + break; + case GX_TG_TEX7: + row = 12; + break; + case GX_TG_TEXCOORD0: + embossRow = 0; + break; + case GX_TG_TEXCOORD1: + embossRow = 1; + break; + case GX_TG_TEXCOORD2: + embossRow = 2; + break; + case GX_TG_TEXCOORD3: + embossRow = 3; + break; + case GX_TG_TEXCOORD4: + embossRow = 4; + break; + case GX_TG_TEXCOORD5: + embossRow = 5; + break; + case GX_TG_TEXCOORD6: + embossRow = 6; + break; + default: + ASSERTMSGLINE(457, 0, "GDSetTexCoordGen: invalid texgen source"); + break; + } + + switch (func) { + case GX_TG_MTX2x4: + tgType = 0; + break; + case GX_TG_MTX3x4: + tgType = 0; + proj = 1; + break; + case GX_TG_BUMP0: + case GX_TG_BUMP1: + case GX_TG_BUMP2: + case GX_TG_BUMP3: + case GX_TG_BUMP4: + case GX_TG_BUMP5: + case GX_TG_BUMP6: + case GX_TG_BUMP7: + ASSERTMSGLINE(481, src_param >= GX_TG_TEXCOORD0 && src_param <= GX_TG_TEXCOORD6, "GDSetTexCoordGen: invalid emboss source"); + tgType = 1; + embossLit = func - GX_TG_BUMP0; + break; + case GX_TG_SRTG: + if (src_param == GX_TG_COLOR0) { + tgType = 2; + } else { + tgType = 3; + } + break; + default: + ASSERTMSGLINE(495, 0, "GDSetTexCoordGen: invalid texgen function"); + break; + } + + GDWriteXFCmd(dst_coord + XF_REG_TEX0_ID, XF_REG_TEX(proj, form, tgType, row, embossRow, embossLit)); + GDWriteXFCmd(dst_coord + XF_REG_DUALTEX0_ID, XF_REG_DUALTEX(postmtx - 0x40, normalize)); +} + +void GDSetCullMode(GXCullMode mode) { + static u8 cm2hw[4] = { 0, 2, 1, 3 }; + + GDWriteBPCmd(0xFE00C000); + GDWriteBPCmd(cm2hw[mode] << 14); +} + +void GDSetGenMode(u8 nTexGens, u8 nChans, u8 nTevs) { + GDWriteBPCmd(0xFE003C3F); + GDWriteBPCmd(BP_GEN_MODE(nTexGens, nChans, nTevs - 1, 0, 0)); + + GDWriteXFCmd(XF_REG_NUMCOLORS_ID, nChans); + GDWriteXFCmd(XF_REG_NUMTEX_ID, nTexGens); +} + +void GDSetGenMode2(u8 nTexGens, u8 nChans, u8 nTevs, u8 nInds, GXCullMode cm) { + static u8 cm2hw[4] = { 0, 2, 1, 3 }; + + GDWriteBPCmd(0xFE07FC3F); + GDWriteBPCmd(BP_GEN_MODE(nTexGens, nChans, nTevs - 1, cm2hw[cm], nInds)); + + GDWriteXFCmd(XF_REG_NUMCOLORS_ID, nChans); + GDWriteXFCmd(XF_REG_NUMTEX_ID, nTexGens); +} + +void GDSetLPSize(u8 lineWidth, u8 pointSize, GXTexOffset lineOffset, GXTexOffset pointOffset, u8 lineHalfAspect) { + GDWriteBPCmd(BP_LP_SIZE(lineWidth, pointSize, lineOffset, pointOffset, lineHalfAspect, 0x22)); +} + +void GDSetCoPlanar(u8 enable) { + GDWriteBPCmd(0xFE080000); + GDWriteBPCmd(enable << 19); +} diff --git a/aurora-main/lib/dolphin/gd/GDIndirect.cpp b/aurora-main/lib/dolphin/gd/GDIndirect.cpp new file mode 100644 index 0000000..50add72 --- /dev/null +++ b/aurora-main/lib/dolphin/gd/GDIndirect.cpp @@ -0,0 +1,270 @@ +#include +#include + +void GDSetTevIndirect(GXTevStageID tev_stage, GXIndTexStageID ind_stage, + GXIndTexFormat format, GXIndTexBiasSel bias_sel, + GXIndTexMtxID matrix_sel, GXIndTexWrap wrap_s, + GXIndTexWrap wrap_t, u8 add_prev, u8 utc_lod, + GXIndTexAlphaSel alpha_sel) { + GDWriteBPCmd(BP_TEV_INDIRECT( + ind_stage & 3, + format & 3, + bias_sel & 7, + alpha_sel & 3, + matrix_sel & 0xF, + wrap_s & 7, + wrap_t & 7, + utc_lod & 1, + add_prev & 1, + 0x10 + (tev_stage & 0xF) + )); +} + +void GDSetIndTexMtx(GXIndTexMtxID mtx_id, const f32 offset[2][3], s8 scale_exp) { + u32 id_offset; + + switch (mtx_id) { + case GX_ITM_0: + id_offset = 0; + break; + case GX_ITM_1: + id_offset = 3; + break; + case GX_ITM_2: + id_offset = 6; + break; + default: + ASSERTMSGLINE(111, 0, "GDSetIndTexMtx: Invalid matrix id"); + break; + } + + scale_exp += 17; + + GDWriteBPCmd(BP_IND_MTX( + (s32)(offset[0][0] * 0x400) & 0x7FF, + (s32)(offset[1][0] * 0x400) & 0x7FF, + scale_exp & 3, + 6 + id_offset + )); + + GDWriteBPCmd(BP_IND_MTX( + (s32)(offset[0][1] * 0x400) & 0x7FF, + (s32)(offset[1][1] * 0x400) & 0x7FF, + (scale_exp >> 2) & 3, + 7 + id_offset + )); + + GDWriteBPCmd(BP_IND_MTX( + (s32)(offset[0][2] * 0x400) & 0x7FF, + (s32)(offset[1][2] * 0x400) & 0x7FF, + (scale_exp >> 4) & 3, + 8 + id_offset + )); +} + +void GDSetIndTexCoordScale(GXIndTexStageID indStageEven, GXIndTexScale scaleS0, + GXIndTexScale scaleT0, GXIndTexScale scaleS1, + GXIndTexScale scaleT1) { + GDWriteBPCmd(BP_IND_TEXCOORD_SCALE( + scaleS0 & 0xF, + scaleT0 & 0xF, + scaleS1 & 0xF, + scaleT1 & 0xF, + 0x25 + ((indStageEven >> 1) & 1) + )); +} + +void GDSetIndTexOrder(GXTexCoordID texCoord0, GXTexMapID texMap0, + GXTexCoordID texCoord1, GXTexMapID texMap1, + GXTexCoordID texCoord2, GXTexMapID texMap2, + GXTexCoordID texCoord3, GXTexMapID texMap3) { + GDWriteBPCmd(BP_IND_TEX_ORDER( + texMap0 & 7, + texCoord0 & 7, + texMap1 & 7, + texCoord1 & 7, + texMap2 & 7, + texCoord2 & 7, + texMap3 & 7, + texCoord3 & 7, + 0x27 + )); +} + +void GDSetTevDirect(GXTevStageID tev_stage) { + GDSetTevIndirect(tev_stage, GX_INDTEXSTAGE0, GX_ITF_8, GX_ITB_NONE, GX_ITM_OFF, GX_ITW_OFF, GX_ITW_OFF, 0, 0, GX_ITBA_OFF); +} + +void GDSetTevIndWarp(GXTevStageID tev_stage, GXIndTexStageID ind_stage, u8 signed_offset, u8 replace_mode, GXIndTexMtxID matrix_sel) { + GXIndTexWrap wrap; + wrap = replace_mode ? GX_ITW_0 : GX_ITW_OFF; + + GDSetTevIndirect(tev_stage, + ind_stage, + GX_ITF_8, + signed_offset ? GX_ITB_STU : GX_ITB_NONE, + matrix_sel, + wrap, + wrap, + 0, + 0, + GX_ITBA_OFF); +} + +void GDSetTevIndTile(GXTevStageID tev_stage, GXIndTexStageID ind_stage, + u16 tilesize_s, u16 tilesize_t, u16 tilespacing_s, + u16 tilespacing_t, GXIndTexFormat format, + GXIndTexMtxID matrix_sel, GXIndTexBiasSel bias_sel, + GXIndTexAlphaSel alpha_sel) { + GXIndTexWrap wrap_s; + GXIndTexWrap wrap_t; + f32 mtx[2][3]; + + switch (tilesize_s) { + case 256: + wrap_s = GX_ITW_256; + break; + case 128: + wrap_s = GX_ITW_128; + break; + case 64: + wrap_s = GX_ITW_64; + break; + case 32: + wrap_s = GX_ITW_32; + break; + case 16: + wrap_s = GX_ITW_16; + break; + default: + ASSERTMSGLINE(268, 0, "GDSetTevIndTile: Invalid tilesize for S coordinate"); + wrap_s = GX_ITW_OFF; + break; + } + + switch (tilesize_t) { + case 256: + wrap_t = GX_ITW_256; + break; + case 128: + wrap_t = GX_ITW_128; + break; + case 64: + wrap_t = GX_ITW_64; + break; + case 32: + wrap_t = GX_ITW_32; + break; + case 16: + wrap_t = GX_ITW_16; + break; + default: + ASSERTMSGLINE(280, 0, "GDSetTevIndTile: Invalid tilesize for T coordinate"); + wrap_t = GX_ITW_OFF; + break; + } + + mtx[0][0] = (f32)tilespacing_s / 0x400; + mtx[0][1] = mtx[0][2] = 0.0f; + + mtx[1][1] = (f32)tilespacing_t / 0x400; + mtx[1][0] = mtx[1][2] = 0.0f; + GDSetIndTexMtx(matrix_sel, mtx, 10); + + GDSetTevIndirect(tev_stage, + ind_stage, + format, + bias_sel, + matrix_sel, + wrap_s, + wrap_t, + 0, + 1, + alpha_sel); +} + +void GDSetTevIndBumpST(GXTevStageID tev_stage, GXIndTexStageID ind_stage, GXIndTexMtxID matrix_sel) { + GXIndTexMtxID sm; + GXIndTexMtxID tm; + + switch(matrix_sel) { + case GX_ITM_0: + sm = GX_ITM_S0; + tm = GX_ITM_T0; + break; + case GX_ITM_1: + sm = GX_ITM_S1; + tm = GX_ITM_T1; + break; + case GX_ITM_2: + sm = GX_ITM_S2; + tm = GX_ITM_T2; + break; + default: + ASSERTMSGLINE(332, 0, "GDSetTevIndBumpST: Invalid matrix selection"); + break; + } + + GDSetTevIndirect(tev_stage, + ind_stage, + GX_ITF_8, + GX_ITB_ST, + sm, + GX_ITW_0, + GX_ITW_0, + 0, + 0, + GX_ITBA_OFF); + + GDSetTevIndirect((GXTevStageID)(tev_stage + 1), + ind_stage, + GX_ITF_8, + GX_ITB_ST, + tm, + GX_ITW_0, + GX_ITW_0, + 1, + 0, + GX_ITBA_OFF); + + GDSetTevIndirect((GXTevStageID)(tev_stage + 2), + ind_stage, + GX_ITF_8, + GX_ITB_NONE, + GX_ITM_OFF, + GX_ITW_OFF, + GX_ITW_OFF, + 1, + 0, + GX_ITBA_OFF); +} + +void GDSetTevIndBumpXYZ(GXTevStageID tev_stage, GXIndTexStageID ind_stage, GXIndTexMtxID matrix_sel) { + GDSetTevIndirect(tev_stage, + ind_stage, + GX_ITF_8, + GX_ITB_STU, + matrix_sel, + GX_ITW_OFF, + GX_ITW_OFF, + 0, + 0, + GX_ITBA_OFF); +} + +void GDSetTevIndRepeat(GXTevStageID tev_stage) { + GDSetTevIndirect(tev_stage, + GX_INDTEXSTAGE0, + GX_ITF_8, + GX_ITB_NONE, + GX_ITM_OFF, + GX_ITW_0, + GX_ITW_0, + 1, + 0, + GX_ITBA_OFF); +} + +void __GDSetIndTexMask(u32 mask) { + GDWriteBPCmd(BP_IND_MASK(mask & 0xFF, 0xF)); +} diff --git a/aurora-main/lib/dolphin/gd/GDLight.cpp b/aurora-main/lib/dolphin/gd/GDLight.cpp new file mode 100644 index 0000000..c036032 --- /dev/null +++ b/aurora-main/lib/dolphin/gd/GDLight.cpp @@ -0,0 +1,216 @@ +#include +#include +#include +#include + +void GDSetLightAttn(GXLightID light, f32 a0, f32 a1, f32 a2, f32 k0, f32 k1, f32 k2) { + GDWriteXFCmdHdr(__GDLightID2Offset(light) + XF_LIGHT_ATTN_ID, 6); + GDWrite_f32(a0); + GDWrite_f32(a1); + GDWrite_f32(a2); + GDWrite_f32(k0); + GDWrite_f32(k1); + GDWrite_f32(k2); +} + +void GDSetLightSpot(GXLightID light, f32 cutoff, GXSpotFn spot_func) { + f32 a0; + f32 a1; + f32 a2; + f32 r; + f32 d; + f32 cr; + + if (cutoff <= 0.0f || cutoff > 90.0f) { + spot_func = GX_SP_OFF; + } + + r = std::numbers::pi_v * cutoff / 180.0f; + cr = cosf(r); + + switch(spot_func) { + case GX_SP_FLAT: + a0 = -1000.0f * cr; + a1 = 1000.0f; + a2 = 0.0f; + break; + case GX_SP_COS: + a0 = -cr / (1.0f - cr); + a1 = 1.0f / (1.0f - cr); + a2 = 0.0f; + break; + case GX_SP_COS2: + a0 = 0.0f; + a1 = -cr / (1.0f - cr); + a2 = 1.0f / (1.0f - cr); + break; + case GX_SP_SHARP: + d = (1.0f - cr) * (1.0f - cr); + a0 = (cr * (cr - 2.0f)) / d; + a1 = 2.0f / d; + a2 = -1.0f / d; + break; + case GX_SP_RING1: + d = (1.0f - cr) * (1.0f - cr); + a0 = (-4.0f * cr) / d; + a1 = (4.0f * (1.0f + cr)) / d; + a2 = -4.0f / d; + break; + case GX_SP_RING2: + d = (1.0f - cr) * (1.0f - cr); + a0 = 1.0f - (2.0f * cr * cr) / d; + a1 = (4.0f * cr) / d; + a2 = -2.0f / d; + break; + case GX_SP_OFF: + default: + a0 = 1.0f; + a1 = 0.0f; + a2 = 0.0f; + break; + } + + GDWriteXFCmdHdr(__GDLightID2Offset(light) + XF_LIGHT_ATTN_ID, 3); + GDWrite_f32(a0); + GDWrite_f32(a1); + GDWrite_f32(a2); +} + +void GDSetLightDistAttn(GXLightID light, f32 ref_dist, f32 ref_br, GXDistAttnFn dist_func) { + f32 k0; + f32 k1; + f32 k2; + + if (ref_dist < 0.0f || ref_br <= 0.0f || ref_br >= 1.0f) { + dist_func = GX_DA_OFF; + } + + switch (dist_func) { + case GX_DA_GENTLE: + k0 = 1.0f; + k1 = (1.0f - ref_br) / (ref_br * ref_dist); + k2 = 0.0f; + break; + case GX_DA_MEDIUM: + k0 = 1.0f; + k1 = (0.5f * (1.0f - ref_br)) / (ref_br * ref_dist); + k2 = (0.5f * (1.0f - ref_br)) / (ref_dist * (ref_br * ref_dist)); + break; + case GX_DA_STEEP: + k0 = 1.0f; + k1 = 0.0f; + k2 = (1.0f - ref_br) / (ref_dist * (ref_br * ref_dist)); + break; + case GX_DA_OFF: + default: + k0 = 1.0f; + k1 = 0.0f; + k2 = 0.0f; + break; + } + + GDWriteXFCmdHdr(__GDLightID2Offset(light) + XF_LIGHT_DISTATTN_ID, 3); + GDWrite_f32(k0); + GDWrite_f32(k1); + GDWrite_f32(k2); +} + +void GDSetLightColor(GXLightID light, GXColor color) { + GDWriteXFCmd(__GDLightID2Offset(light) + XF_LIGHT_COLOR_ID, color.r << 24 | color.g << 16 | color.b << 8 | color.a); +} + +void GDSetLightPos(GXLightID light, f32 x, f32 y, f32 z) { + GDWriteXFCmdHdr(__GDLightID2Offset(light) + XF_LIGHT_POS_ID, 3); + GDWrite_f32(x); + GDWrite_f32(y); + GDWrite_f32(z); +} + +void GDSetLightDir(GXLightID light, f32 nx, f32 ny, f32 nz) { + GDWriteXFCmdHdr(__GDLightID2Offset(light) + XF_LIGHT_DIR_ID, 3); + GDWrite_f32(nx); + GDWrite_f32(ny); + GDWrite_f32(nz); +} + +void GDSetSpecularDirHA(GXLightID light, f32 nx, f32 ny, f32 nz, f32 hx, f32 hy, f32 hz) { + f32 px; + f32 py; + f32 pz; + px = 1000000000000000000.0f * -nx; + py = 1000000000000000000.0f * -ny; + pz = 1000000000000000000.0f * -nz; + + GDWriteXFCmdHdr(__GDLightID2Offset(light) + XF_LIGHT_SPEC_DIR_ID, 6); + GDWrite_f32(px); + GDWrite_f32(py); + GDWrite_f32(pz); + GDWrite_f32(hx); + GDWrite_f32(hy); + GDWrite_f32(hz); +} + +void GDSetSpecularDir(GXLightID light, f32 nx, f32 ny, f32 nz) { + f32 px; + f32 py; + f32 pz; + f32 hx; + f32 hy; + f32 hz; + f32 mag; + + hx = -nx; + hy = -ny; + hz = 1.0f + -nz; + + mag = hx * hx + hy * hy + hz * hz; + if (mag != 0.0f) { + mag = 1.0f / sqrtf(mag); + } + + hx = hx * mag; + hy = hy * mag; + hz = hz * mag; + px = 1000000000000000000.0f * -nx; + py = 1000000000000000000.0f * -ny; + pz = 1000000000000000000.0f * -nz; + + GDWriteXFCmdHdr(__GDLightID2Offset(light) + XF_LIGHT_SPEC_DIR_ID, 6); + GDWrite_f32(px); + GDWrite_f32(py); + GDWrite_f32(pz); + GDWrite_f32(hx); + GDWrite_f32(hy); + GDWrite_f32(hz); +} + +void GDLoadLightObjIndx(u32 lt_obj_indx, GXLightID light) { + GDWriteXFIndxDCmd(__GDLightID2Offset(light) + XF_LIGHT_ID, 0x10, lt_obj_indx); +} + +void GDSetChanAmbColor(GXChannelID chan, GXColor color) { + GDWriteXFCmd((chan & 1) + XF_REG_AMBIENT0_ID, color.r << 24 | color.g << 16 | color.b << 8 | color.a); +} + +void GDSetChanMatColor(GXChannelID chan, GXColor color) { + GDWriteXFCmd((chan & 1) + XF_REG_MATERIAL0_ID, color.r << 24 | color.g << 16 | color.b << 8 | color.a); +} + +void GDSetChanCtrl(GXChannelID chan, u8 enable, GXColorSrc amb_src, GXColorSrc mat_src, u32 light_mask, GXDiffuseFn diff_fn, GXAttnFn attn_fn) { + u32 reg; + + reg = XF_REG_CHAN_CTRL(mat_src, enable, light_mask & 0xF, amb_src, attn_fn == GX_AF_SPEC ? GX_DF_NONE : diff_fn, attn_fn != GX_AF_NONE, attn_fn != GX_AF_SPEC, (light_mask >> 4) & 0xF); + GDWriteXFCmd((chan & 3) + XF_REG_COLOR0CNTRL_ID, reg); + + if (chan == 4 || chan == 5) { + GDWriteXFCmd(chan + XF_REG_MATERIAL0_ID, reg); + } +} + +u16 __GDLightID2Index(GXLightID id) { + u16 idx = 0x1F - std::countl_zero((u32) id); + if (idx > 7) { + idx = 0; + } + return idx; +} diff --git a/aurora-main/lib/dolphin/gd/GDPixel.cpp b/aurora-main/lib/dolphin/gd/GDPixel.cpp new file mode 100644 index 0000000..c18cf77 --- /dev/null +++ b/aurora-main/lib/dolphin/gd/GDPixel.cpp @@ -0,0 +1,118 @@ +#include +#include + +void GDSetFog(GXFogType type, f32 startz, f32 endz, f32 nearz, f32 farz, GXColor color) { + f32 A; + f32 B; + f32 B_mant; + f32 C; + f32 A_f; + u32 b_expn; + u32 b_m; + u32 a_hex; + u32 c_hex; + u32 fsel; + u32 proj; + + ASSERTMSGLINE(62, farz >= 0.0f, "GDSetFog: The farz should be positive value"); + ASSERTMSGLINE(63, farz >= nearz, "GDSetFog: The farz should be larger than nearz"); + + fsel = type & 7; + proj = (type >> 3) & 1; + + if (proj != 0) { + if (farz == nearz || endz == startz) { + A_f = 0.0f; + C = 0.0f; + } else { + A = 1.0f / (endz - startz); + A_f = (farz - nearz) * A; + C = (startz - nearz) * A; + } + + b_expn = 0; + b_m = 0; + } else { + if (farz == nearz || endz == startz) { + A = 0.0f; + B = 0.5f; + C = 0.0f; + } else { + A = (farz * nearz) / ((farz - nearz) * (endz - startz)); + B = farz / (farz - nearz); + C = startz / (endz - startz); + } + + B_mant = B; + b_expn = 1; + + while (B_mant > 1.0) { + B_mant *= 0.5f; + b_expn++; + } + + while (B_mant > 0.0f && B_mant < 0.5) { + B_mant *= 2.0f; + b_expn--; + } + + A_f = A / (1 << b_expn); + b_m = (u32) (8388638.0f * B_mant); + } + + a_hex = *(u32*)&A_f; + c_hex = *(u32*)&C; + + GDWriteBPCmd(BP_FOG_UNK0(a_hex >> 12, 0xEE)); + GDWriteBPCmd(BP_FOG_UNK1(b_m, 0xEF)); + GDWriteBPCmd(BP_FOG_UNK2(b_expn, 0xF0)); + GDWriteBPCmd(BP_FOG_UNK3(c_hex >> 12, proj, fsel, 0xF1)); + GDWriteBPCmd(BP_FOG_COLOR(color.r, color.g, color.b, 0xF2)); +} + +void GDSetBlendMode(GXBlendMode type, GXBlendFactor src_factor, GXBlendFactor dst_factor, GXLogicOp logic_op) { + GDWriteBPCmd(0xFE00FFE3); + GDWriteBPCmd(BP_BLEND_MODE( + type == GX_BM_BLEND || type == GX_BM_SUBTRACT, + type == GX_BM_LOGIC, + 0, + 0, + 0, + dst_factor, + src_factor, + type == GX_BM_SUBTRACT, + logic_op, + 0x41 + )); +} + +void GDSetBlendModeEtc(GXBlendMode type, GXBlendFactor src_factor, + GXBlendFactor dst_factor, GXLogicOp logic_op, + u8 color_update_enable, u8 alpha_update_enable, + u8 dither_enable) { + GDWriteBPCmd(BP_BLEND_MODE( + type == GX_BM_BLEND || type == GX_BM_SUBTRACT, + type == GX_BM_LOGIC, + dither_enable, + color_update_enable, + alpha_update_enable, + dst_factor, + src_factor, + type == GX_BM_SUBTRACT, + logic_op, + 0x41 + )); +} + +void GDSetZMode(u8 compare_enable, GXCompare func, u8 update_enable) { + GDWriteBPCmd(BP_Z_MODE(compare_enable, func, update_enable, 0x40)); +} + +void GDSetDstAlpha(u8 enable, u8 alpha) { + GDWriteBPCmd(BP_DST_ALPHA(alpha, enable, 0x42)); +} + +void GDSetDrawSync(u16 token) { + GDWriteBPCmd(BP_TOKEN(token, 0x48)); + GDWriteBPCmd(BP_TOKEN(token, 0x47)); +} diff --git a/aurora-main/lib/dolphin/gd/GDTev.cpp b/aurora-main/lib/dolphin/gd/GDTev.cpp new file mode 100644 index 0000000..69803e8 --- /dev/null +++ b/aurora-main/lib/dolphin/gd/GDTev.cpp @@ -0,0 +1,159 @@ +#include +#include + +void GDSetTevOp(GXTevStageID stage, GXTevMode mode) { + GXTevColorArg carg = GX_CC_RASC; + GXTevAlphaArg aarg = GX_CA_RASA; + + if (stage != GX_TEVSTAGE0) { + carg = GX_CC_CPREV; + aarg = GX_CA_APREV; + } + + switch (mode) { + case GX_MODULATE: + GDSetTevColorCalc(stage, GX_CC_ZERO, GX_CC_TEXC, carg, GX_CC_ZERO, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, 1, GX_TEVPREV); + GDSetTevAlphaCalcAndSwap(stage, GX_CA_ZERO, GX_CA_TEXA, aarg, GX_CA_ZERO, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, 1, GX_TEVPREV, GX_TEV_SWAP0, GX_TEV_SWAP0); + break; + case GX_DECAL: + GDSetTevColorCalc(stage, carg, GX_CC_TEXC, GX_CC_TEXA, GX_CC_ZERO, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, 1, GX_TEVPREV); + GDSetTevAlphaCalcAndSwap(stage, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, aarg, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, 1, GX_TEVPREV, GX_TEV_SWAP0, GX_TEV_SWAP0); + break; + case GX_BLEND: + GDSetTevColorCalc(stage, carg, GX_CC_ONE, GX_CC_TEXC, GX_CC_ZERO, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, 1, GX_TEVPREV); + GDSetTevAlphaCalcAndSwap(stage, GX_CA_ZERO, GX_CA_TEXA, aarg, GX_CA_ZERO, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, 1, GX_TEVPREV, GX_TEV_SWAP0, GX_TEV_SWAP0); + break; + case GX_REPLACE: + GDSetTevColorCalc(stage, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, GX_CC_TEXC, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, 1, GX_TEVPREV); + GDSetTevAlphaCalcAndSwap(stage, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_TEXA, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, 1, GX_TEVPREV, GX_TEV_SWAP0, GX_TEV_SWAP0); + break; + case GX_PASSCLR: + GDSetTevColorCalc(stage, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, carg, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, 1, GX_TEVPREV); + GDSetTevAlphaCalcAndSwap(stage, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, aarg, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, 1, GX_TEVPREV, GX_TEV_SWAP0, GX_TEV_SWAP0); + break; + default: + ASSERTMSGLINE(110, 0, "GDSetTevOp: Invalid Tev Mode"); + break; + } +} + +void GDSetTevColorCalc(GXTevStageID stage, GXTevColorArg a, GXTevColorArg b, + GXTevColorArg c, GXTevColorArg d, GXTevOp op, + GXTevBias bias, GXTevScale scale, u8 clamp, + GXTevRegID out_reg) { + if (op <= GX_TEV_SUB) { + GDWriteBPCmd(BP_TEV_COLOR(d, c, b, a, bias, op & 1, clamp, scale, out_reg, stage * 2 + 0xC0)); + } else { + GDWriteBPCmd(BP_TEV_COLOR(d, c, b, a, 3, op & 1, clamp, (op >> 1) & 3, out_reg, stage * 2 + 0xC0)); + } +} + +void GDSetTevAlphaCalcAndSwap(GXTevStageID stage, GXTevAlphaArg a, + GXTevAlphaArg b, GXTevAlphaArg c, GXTevAlphaArg d, + GXTevOp op, GXTevBias bias, GXTevScale scale, + u8 clamp, GXTevRegID out_reg, + GXTevSwapSel ras_sel, GXTevSwapSel tex_sel) { + if (op <= GX_TEV_SUB) { + GDWriteBPCmd(BP_TEV_ALPHA(ras_sel, tex_sel, d, c, b, a, bias, op & 1, clamp, scale, out_reg, stage * 2 + 0xC1)); + } else { + GDWriteBPCmd(BP_TEV_ALPHA(ras_sel, tex_sel, d, c, b, a, 3, op & 1, clamp, (op >> 1) & 3, out_reg, stage * 2 + 0xC1)); + } +} + +void GDSetTevColor(GXTevRegID reg, GXColor color) { + u32 regRA; + u32 regBG; + + regRA = BP_TEV_COLOR_REG_RA(color.r, color.a, 0, 0xE0 + reg * 2); + regBG = BP_TEV_COLOR_REG_BG(color.b, color.g, 0, 0xE1 + reg * 2); + + GDWriteBPCmd(regRA); + GDWriteBPCmd(regBG); + GDWriteBPCmd(regBG); + GDWriteBPCmd(regBG); +} + +void GDSetTevColorS10(GXTevRegID reg, GXColorS10 color) { + u32 regRA; + u32 regBG; + + regRA = BP_TEV_COLOR_REG_RA(color.r & 0x7FF, color.a & 0x7FF, 0, 0xE0 + reg * 2); + regBG = BP_TEV_COLOR_REG_BG(color.b & 0x7FF, color.g & 0x7FF, 0, 0xE1 + reg * 2); + + GDWriteBPCmd(regRA); + GDWriteBPCmd(regBG); + GDWriteBPCmd(regBG); + GDWriteBPCmd(regBG); +} + +void GDSetTevKColor(GXTevKColorID reg, GXColor color) { + u32 regRA; + u32 regBG; + + regRA = BP_TEV_COLOR_REG_RA(color.r, color.a, 1, 0xE0 + reg * 2); + regBG = BP_TEV_COLOR_REG_BG(color.b, color.g, 1, 0xE1 + reg * 2); + + GDWriteBPCmd(regRA); + GDWriteBPCmd(regBG); +} + +void GDSetTevKonstantSel(GXTevStageID evenStage, GXTevKColorSel kcsel0, + GXTevKAlphaSel kasel0, GXTevKColorSel kcsel1, + GXTevKAlphaSel kasel1) { + GDWriteBPCmd(0xFEFFFFF0); + GDWriteBPCmd(BP_TEV_KSEL(0, 0, kcsel0, kasel0, kcsel1, kasel1, evenStage / 2 + 0xF6)); +} + +void GDSetTevSwapModeTable(GXTevSwapSel table, GXTevColorChan red, + GXTevColorChan green, GXTevColorChan blue, + GXTevColorChan alpha) { + GDWriteBPCmd(0xFE00000F); + GDWriteBPCmd(BP_TEV_KSEL(red, green, 0, 0, 0, 0, table * 2 + 0xF6)); + + GDWriteBPCmd(0xFE00000F); + GDWriteBPCmd(BP_TEV_KSEL(blue, alpha, 0, 0, 0, 0, table * 2 + 0xF7)); +} + +void GDSetAlphaCompare(GXCompare comp0, u8 ref0, GXAlphaOp op, GXCompare comp1, u8 ref1) { + GDWriteBPCmd(BP_ALPHA_COMPARE(ref0, ref1, comp0, comp1, op, 0xF3)); +} + +void GDSetZTexture(GXZTexOp op, GXTexFmt fmt, u32 bias) { + u32 zfmt; + + switch (fmt) { + case GX_TF_Z8: + zfmt = 0; + break; + case GX_TF_Z16: + zfmt = 1; + break; + case GX_TF_Z24X8: + zfmt = 2; + break; + default: + ASSERTMSGLINE(399, 0, "GDSetZTexture: Invalid format"); + zfmt = 2; + break; + } + + GDWriteBPCmd(BP_ZTEX_PARAMS_0(bias, 0xF4)); + GDWriteBPCmd(BP_ZTEX_PARAMS_1(zfmt, op, 0xF5)); +} + +void GDSetTevOrder(GXTevStageID evenStage, GXTexCoordID coord0, GXTexMapID map0, + GXChannelID color0, GXTexCoordID coord1, GXTexMapID map1, + GXChannelID color1) { + static u8 c2r[] = { 0, 1, 0, 1, 0, 1, 7, 5, 6, 0, 0, 0, 0, 0, 0, 7 }; + GDWriteBPCmd(BP_TEV_ORDER( + map0 & 7, + coord0 & 7, + map0 != GX_TEXMAP_NULL && !(map0 & GX_TEX_DISABLE), + c2r[color0 & 0xF], + map1 & 7, + coord1 & 7, + map1 != GX_TEXMAP_NULL && !(map1 & GX_TEX_DISABLE), + c2r[color1 & 0xF], + evenStage / 2 + 0x28 + )); +} diff --git a/aurora-main/lib/dolphin/gd/GDTexture.cpp b/aurora-main/lib/dolphin/gd/GDTexture.cpp new file mode 100644 index 0000000..c3c1a6c --- /dev/null +++ b/aurora-main/lib/dolphin/gd/GDTexture.cpp @@ -0,0 +1,104 @@ +#include +#include + +u8 GD2HWFiltConv[] = {0, 4, 1, 5, 2, 6}; + +u8 GDTexMode0Ids[8] = {128, 129, 130, 131, 160, 161, 162, 163}; +u8 GDTexMode1Ids[8] = {132, 133, 134, 135, 164, 165, 166, 167}; +u8 GDTexImage0Ids[8] = {136, 137, 138, 139, 168, 169, 170, 171}; +u8 GDTexImage1Ids[8] = {140, 141, 142, 143, 172, 173, 174, 175}; +u8 GDTexImage2Ids[8] = {144, 145, 146, 147, 176, 177, 178, 179}; +u8 GDTexImage3Ids[8] = {148, 149, 150, 151, 180, 181, 182, 183}; +u8 GDTexTlutIds[8] = {152, 153, 154, 155, 184, 185, 186, 187}; + +void GDSetTexLookupMode(GXTexMapID id, GXTexWrapMode wrap_s, + GXTexWrapMode wrap_t, GXTexFilter min_filt, + GXTexFilter mag_filt, f32 min_lod, f32 max_lod, + f32 lod_bias, u8 bias_clamp, u8 do_edge_lod, + GXAnisotropy max_aniso) { + GDWriteBPCmd(BP_TEX_MODE0(wrap_s, wrap_t, mag_filt == TRUE, GD2HWFiltConv[min_filt], !do_edge_lod, (u8)(32.0f * lod_bias), max_aniso, bias_clamp, GDTexMode0Ids[id])); + GDWriteBPCmd(BP_TEX_MODE1((u8)(16.0f * min_lod), (u8)(16.0f * max_lod), GDTexMode1Ids[id])); +} + +void GDSetTexImgAttr(GXTexMapID id, u16 width, u16 height, GXTexFmt format) { + GDWriteBPCmd(BP_IMAGE_ATTR(width - 1, height - 1, format, GDTexImage0Ids[id])); +} + +void GDSetTexImgPtr(GXTexMapID id, void* image_ptr) { + GDWriteBPCmd(BP_IMAGE_PTR(OSCachedToPhysical(image_ptr) >> 5, GDTexImage3Ids[id])); +} + +void GDSetTexImgPtrRaw(GXTexMapID id, u32 image_ptr_raw) { + GDWriteBPCmd(BP_IMAGE_PTR(image_ptr_raw, GDTexImage3Ids[id])); +} + +void GDPatchTexImgPtr(void* image_ptr) { + GDWrite_u24(OSCachedToPhysical(image_ptr) >> 5); +} + +void GDSetTexCached(GXTexMapID id, u32 tmem_even, GXTexCacheSize size_even, + u32 tmem_odd, GXTexCacheSize size_odd) { + GDWriteBPCmd(BP_TEX_CACHE_EVEN(tmem_even >> 5, size_even + 3, size_even + 3, 0, GDTexImage1Ids[id])); + + if (size_odd != 3 && tmem_odd < 0x100000) { + GDWriteBPCmd(BP_TEX_CACHE_ODD(tmem_odd >> 5, size_odd + 3, size_odd + 3, GDTexImage2Ids[id])); + } +} + + +void GDSetTexPreLoaded(GXTexMapID id, u32 tmem_even, u32 tmem_odd) { + GDWriteBPCmd(BP_TEX_CACHE_EVEN(tmem_even >> 5, 0, 0, 1, GDTexImage1Ids[id])); + + if (tmem_odd < 0x100000) { + GDWriteBPCmd(BP_TEX_CACHE_ODD(tmem_odd >> 5, 0, 0, GDTexImage2Ids[id])); + } +} + +void GDSetTexTlut(GXTexMapID id, u32 tmem_addr, GXTlutFmt format) { + GDWriteBPCmd(BP_TEX_TLUT((tmem_addr - 0x80000) >> 9, format, GDTexTlutIds[id])); +} + +void GDSetTexCoordScale(GXTexCoordID coord, u16 s_scale, u16 t_scale) { + GDWriteBPCmd(0xFE00FFFF); + GDWriteBPCmd(BP_TEXCOORD_S_SCALE(s_scale - 1, 0, 0, 0, 0, coord * 2 + 0x30)); + + GDWriteBPCmd(0xFE00FFFF); + GDWriteBPCmd(BP_TEXCOORD_T_SCALE(t_scale - 1, 0, 0, coord * 2 + 0x31)); +} + +void GDSetTexCoordScale2(GXTexCoordID coord, u16 s_scale, u8 s_bias, + u8 s_wrap, u16 t_scale, u8 t_bias, u8 t_wrap) { + GDWriteBPCmd(0xFE03FFFF); + GDWriteBPCmd(BP_TEXCOORD_S_SCALE(s_scale - 1, s_bias, s_wrap, 0, 0, coord * 2 + 0x30)); + GDWriteBPCmd(BP_TEXCOORD_T_SCALE(t_scale - 1, t_bias, t_wrap, coord * 2 + 0x31)); +} + +void GDSetTexCoordScaleAndTOEs(GXTexCoordID coord, u16 s_scale, u8 s_bias, + u8 s_wrap, u16 t_scale, u8 t_bias, u8 t_wrap, + u8 line_offset, u8 point_offset) { + GDWriteBPCmd(BP_TEXCOORD_S_SCALE(s_scale - 1, s_bias, s_wrap, line_offset, point_offset, coord * 2 + 0x30)); + GDWriteBPCmd(BP_TEXCOORD_T_SCALE(t_scale - 1, t_bias, t_wrap, coord * 2 + 0x31)); +} + +void GDLoadTlut(void* tlut_ptr, u32 tmem_addr, GXTlutSize size) { + ASSERTMSGLINE(488, !(tmem_addr & 0x1ff), "GDLoadTlut: invalid TMEM pointer"); + ASSERTMSGLINE(489, size <= 0x400, "GDLoadTlut: invalid TLUT size"); + + GDWriteBPCmd(0xFEFFFF00); + GDWriteBPCmd(0xF000000); + GDWriteBPCmd(BP_LOAD_TLUT0(OSCachedToPhysical(tlut_ptr) >> 5, 0x64)); + GDWriteBPCmd(BP_LOAD_TLUT1((tmem_addr - 0x80000) >> 9, size, 0x65)); + GDWriteBPCmd(0xFEFFFF00); + GDWriteBPCmd(0xF000000); +} + +void GDLoadTlutRaw(u32 tlut_ptr_raw, u32 tmem_addr, GXTlutSize size) { + ASSERTMSGLINE(527, size <= 0x400, "GDLoadTlut: invalid TLUT size"); + + GDWriteBPCmd(0xFEFFFF00); + GDWriteBPCmd(0xF000000); + GDWriteBPCmd(BP_LOAD_TLUT0(tlut_ptr_raw, 0x64)); + GDWriteBPCmd(BP_LOAD_TLUT1((tmem_addr - 0x80000) >> 9, size, 0x65)); + GDWriteBPCmd(0xFEFFFF00); + GDWriteBPCmd(0xF000000); +} diff --git a/aurora-main/lib/dolphin/gd/GDTransform.cpp b/aurora-main/lib/dolphin/gd/GDTransform.cpp new file mode 100644 index 0000000..9ac9c66 --- /dev/null +++ b/aurora-main/lib/dolphin/gd/GDTransform.cpp @@ -0,0 +1,127 @@ +#include +#include + +void GDLoadPosMtxImm(const Mtx mtx, u32 id) { + GDWriteXFCmdHdr(4 * id, 12); + GDWrite_f32(mtx[0][0]); + GDWrite_f32(mtx[0][1]); + GDWrite_f32(mtx[0][2]); + GDWrite_f32(mtx[0][3]); + GDWrite_f32(mtx[1][0]); + GDWrite_f32(mtx[1][1]); + GDWrite_f32(mtx[1][2]); + GDWrite_f32(mtx[1][3]); + GDWrite_f32(mtx[2][0]); + GDWrite_f32(mtx[2][1]); + GDWrite_f32(mtx[2][2]); + GDWrite_f32(mtx[2][3]); +} + +void GDLoadPosMtxIndx(u16 mtx_indx, u32 id) { + GDWriteXFIndxACmd(4 * id, 12, mtx_indx); +} + +void GDLoadNrmMtxImm(const Mtx mtx, u32 id) { + GDWriteXFCmdHdr(id * 3 + 0x400, 9); + GDWrite_f32(mtx[0][0]); + GDWrite_f32(mtx[0][1]); + GDWrite_f32(mtx[0][2]); + GDWrite_f32(mtx[1][0]); + GDWrite_f32(mtx[1][1]); + GDWrite_f32(mtx[1][2]); + GDWrite_f32(mtx[2][0]); + GDWrite_f32(mtx[2][1]); + GDWrite_f32(mtx[2][2]); +} + +void GDLoadNrmMtxImm3x3(const f32 mtx[3][3], u32 id) { + GDWriteXFCmdHdr(id * 3 + 0x400, 9); + GDWrite_f32(mtx[0][0]); + GDWrite_f32(mtx[0][1]); + GDWrite_f32(mtx[0][2]); + GDWrite_f32(mtx[1][0]); + GDWrite_f32(mtx[1][1]); + GDWrite_f32(mtx[1][2]); + GDWrite_f32(mtx[2][0]); + GDWrite_f32(mtx[2][1]); + GDWrite_f32(mtx[2][2]); +} + +void GDLoadNrmMtxIndx3x3(u16 mtx_indx, u32 id) { + GDWriteXFIndxBCmd(id * 3 + 0x400, 9, mtx_indx); +} + +void GDLoadTexMtxImm(const Mtx mtx, u32 id, GXTexMtxType type) { + u16 addr; + u8 count; + + if (id >= 0x40) { + ASSERTMSGLINE(178, type == GX_MTX3x4, "GDLoadTexMtxImm: invalid matrix type"); + addr = ((id - 0x40) << 2) + 0x500; + count = 12; + } else { + addr = 4 * id; + count = type == GX_MTX2x4 ? 8 : 12; + } + + GDWriteXFCmdHdr(addr,count); + GDWrite_f32(mtx[0][0]); + GDWrite_f32(mtx[0][1]); + GDWrite_f32(mtx[0][2]); + GDWrite_f32(mtx[0][3]); + GDWrite_f32(mtx[1][0]); + GDWrite_f32(mtx[1][1]); + GDWrite_f32(mtx[1][2]); + GDWrite_f32(mtx[1][3]); + + if (type == GX_MTX3x4) { + GDWrite_f32(mtx[2][0]); + GDWrite_f32(mtx[2][1]); + GDWrite_f32(mtx[2][2]); + GDWrite_f32(mtx[2][3]); + } +} + +void GDLoadTexMtxIndx(u16 mtx_indx, u32 id, GXTexMtxType type) { + u16 addr; + u8 count; + + if (id >= 0x40) { + ASSERTMSGLINE(227, type == GX_MTX3x4, "GDLoadTexMtxIndx: invalid matrix type"); + addr = ((id - 0x40) << 2) + 0x500; + count = 12; + } else { + addr = 4 * id; + count = type == GX_MTX2x4 ? 8 : 12; + } + + GDWriteXFIndxCCmd(addr, count, mtx_indx); +} + +void GDSetCurrentMtx(u32 pn, u32 t0, u32 t1, u32 t2, u32 t3, u32 t4, u32 t5, u32 t6, u32 t7) { + u32 regA; + u32 regB; + + regA = CP_MTX_REG_A(pn, t0, t1, t2, t3); + regB = CP_MTX_REG_B(t4, t5, t6, t7); + + GDWriteCPCmd(CP_MTX_REG_A_ID, regA); + GDWriteCPCmd(CP_MTX_REG_B_ID, regB); + GDWriteXFCmdHdr(XF_REG_MATRIXINDEX0_ID, 2); + GDWrite_u32(regA); + GDWrite_u32(regB); +} + +void GDSetProjection(const Mtx44 mtx, GXProjectionType type) { + u32 c; + c = type == GX_ORTHOGRAPHIC ? 3 : 2; + + GDWriteXFCmdHdr(XF_REG_PROJECTIONA_ID, 7); + GDWrite_f32(mtx[0][0]); + GDWrite_f32(mtx[0][c]); + GDWrite_f32(mtx[1][1]); + GDWrite_f32(mtx[1][c]); + GDWrite_f32(mtx[2][2]); + GDWrite_f32(mtx[2][3]); + GDWrite_u32(type); +} diff --git a/aurora-main/lib/dolphin/gd/gd.hpp b/aurora-main/lib/dolphin/gd/gd.hpp new file mode 100644 index 0000000..0f69c24 --- /dev/null +++ b/aurora-main/lib/dolphin/gd/gd.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include "../../internal.hpp" + +static aurora::Module Log("aurora::gd"); diff --git a/aurora-main/lib/dolphin/gx/GXAurora.cpp b/aurora-main/lib/dolphin/gx/GXAurora.cpp new file mode 100644 index 0000000..11b87c1 --- /dev/null +++ b/aurora-main/lib/dolphin/gx/GXAurora.cpp @@ -0,0 +1,175 @@ +#include "dolphin/gx/GXAurora.h" + +#include +#include +#include + +#include "__gx.h" +#include "gx.hpp" +#include "../../window.hpp" + +#include "../../gfx/common.hpp" +#include "../../gx/fifo.hpp" + +// Single definition for the `Log` that gx.hpp declares for this directory. +aurora::Module Log("aurora::gx"); + +static void GXWriteString(const char* label) { + auto length = strlen(label); + + if (length > std::numeric_limits::max()) { + Log.warn("Debug marker size over u16 max, truncating"); + length = std::numeric_limits::max(); + } + + GX_WRITE_U16(length); + GX_WRITE_DATA(label, length); +} + +void GXPushDebugGroup(const char* label) { + GX_WRITE_AURORA(GX_LOAD_AURORA_DEBUG_GROUP_PUSH); + GXWriteString(label); +} + +void GXPopDebugGroup() { GX_WRITE_AURORA(GX_LOAD_AURORA_DEBUG_GROUP_POP); } + +void GXInsertDebugMarker(const char* label) { + GX_WRITE_AURORA(GX_LOAD_AURORA_DEBUG_MARKER_INSERT); + GXWriteString(label); +} + +void AuroraSetViewportPolicy(AuroraViewportPolicy policy) { + const bool changed = g_gxState.viewportPolicy != policy; + if (changed) { + // Finish commands using the old framebuffer mapping before changing it. + aurora::gx::fifo::drain(); + } + g_gxState.viewportPolicy = policy; + aurora::window::set_frame_buffer_aspect_fit(policy == AURORA_VIEWPORT_FIT); + aurora::window::set_present_surface_fill(policy == AURORA_VIEWPORT_STRETCH); + if (changed) { + // Reapply the guest viewport and scissor after a resize. + aurora::gx::set_logical_viewport(g_gxState.logicalViewport); + aurora::gx::set_logical_scissor(g_gxState.logicalScissor); + } +} + +void AuroraGetRenderSize(u32* width, u32* height) { + // The guest CPU thread uses this safe render-size value outside render passes. + const auto renderSize = aurora::gfx::get_frame_buffer_size(); + if (width != nullptr) { + *width = renderSize.x; + } + if (height != nullptr) { + *height = renderSize.y; + } +} + +void AuroraGetSurfaceSize(u32* width, u32* height) { + const auto windowSize = aurora::window::get_window_size(); + if (width != nullptr) { + *width = windowSize.native_fb_width; + } + if (height != nullptr) { + *height = windowSize.native_fb_height; + } +} + +void GXSetViewportRender(f32 left, f32 top, f32 wd, f32 ht, f32 nearz, f32 farz) { + GX_WRITE_AURORA(GX_LOAD_AURORA_VIEWPORT_RENDER); + GX_WRITE_F32(left); + GX_WRITE_F32(top); + GX_WRITE_F32(wd); + GX_WRITE_F32(ht); + GX_WRITE_F32(nearz); + GX_WRITE_F32(farz); +} + +void GXSetScissorRender(u32 left, u32 top, u32 wd, u32 ht) { + GX_WRITE_AURORA(GX_LOAD_AURORA_SCISSOR_RENDER); + GX_WRITE_U32(left); + GX_WRITE_U32(top); + GX_WRITE_U32(wd); + GX_WRITE_U32(ht); +} + +namespace { +void WriteMappedRenderState(const aurora::gx::MappedRenderState& mapped) { + GXSetViewportRender(mapped.viewport.left, mapped.viewport.top, mapped.viewport.width, mapped.viewport.height, + mapped.viewport.znear, mapped.viewport.zfar); + GXSetScissorRender(static_cast(std::max(mapped.scissor.x, 0)), + static_cast(std::max(mapped.scissor.y, 0)), + static_cast(std::max(mapped.scissor.width, 0)), + static_cast(std::max(mapped.scissor.height, 0))); +} +} // namespace + +void GXSetViewportScissorRenderSafeArea(f32 aspect) { + const auto [targetWidth, targetHeight] = aurora::gfx::get_render_target_size(); + if (targetWidth == 0 || targetHeight == 0 || !std::isfinite(aspect) || aspect <= 0.0f) { + return; + } + + // Apply queued viewport changes before direct layout draws use the safe area. + aurora::gx::fifo::drain(); + auto mapped = aurora::gx::map_logical_render_state(); + const float targetAspect = static_cast(targetWidth) / static_cast(targetHeight); + + float safeLeft = 0.0f; + float safeTop = 0.0f; + float safeWidth = static_cast(targetWidth); + float safeHeight = static_cast(targetHeight); + if (targetAspect > aspect) { + safeWidth = safeHeight * aspect; + safeLeft = (static_cast(targetWidth) - safeWidth) * 0.5f; + } else if (targetAspect < aspect) { + safeHeight = safeWidth / aspect; + safeTop = (static_cast(targetHeight) - safeHeight) * 0.5f; + } + + const float scaleX = safeWidth / static_cast(targetWidth); + const float scaleY = safeHeight / static_cast(targetHeight); + mapped.viewport.left = safeLeft + mapped.viewport.left * scaleX; + mapped.viewport.top = safeTop + mapped.viewport.top * scaleY; + mapped.viewport.width *= scaleX; + mapped.viewport.height *= scaleY; + + const float scissorLeft = safeLeft + static_cast(mapped.scissor.x) * scaleX; + const float scissorTop = safeTop + static_cast(mapped.scissor.y) * scaleY; + const float scissorRight = + safeLeft + static_cast(mapped.scissor.x + mapped.scissor.width) * scaleX; + const float scissorBottom = + safeTop + static_cast(mapped.scissor.y + mapped.scissor.height) * scaleY; + const int32_t left = std::clamp(static_cast(std::floor(scissorLeft)), 0, + static_cast(targetWidth)); + const int32_t top = std::clamp(static_cast(std::floor(scissorTop)), 0, + static_cast(targetHeight)); + const int32_t right = std::clamp(static_cast(std::ceil(scissorRight)), left, + static_cast(targetWidth)); + const int32_t bottom = std::clamp(static_cast(std::ceil(scissorBottom)), top, + static_cast(targetHeight)); + mapped.scissor = {left, top, right - left, bottom - top}; + + WriteMappedRenderState(mapped); +} + +void GXRestoreViewportScissorRender() { + // Run queued GX draws before leaving the direct layout safe area. + aurora::gx::fifo::drain(); + WriteMappedRenderState(aurora::gx::map_logical_render_state()); +} + +void GXSetTexCopySrcRender(u16 left, u16 top, u16 wd, u16 ht) { + aurora::gx::g_gxState.texCopySrc = {left, top, wd, ht}; + aurora::gx::g_gxState.texCopySrcRenderSpace = true; +} + +void GXCreateFrameBuffer(u32 width, u32 height) { + aurora::gx::fifo::drain(); + aurora::gfx::begin_offscreen(width, height); +} + +void GXRestoreFrameBuffer() { + aurora::gx::fifo::drain(); + aurora::gfx::end_offscreen(); +} diff --git a/aurora-main/lib/dolphin/gx/GXBump.cpp b/aurora-main/lib/dolphin/gx/GXBump.cpp new file mode 100644 index 0000000..511bcfe --- /dev/null +++ b/aurora-main/lib/dolphin/gx/GXBump.cpp @@ -0,0 +1,213 @@ +#include "gx.hpp" +#include "__gx.h" + +extern "C" { +void GXSetNumIndStages(u8 num) { + SET_REG_FIELD(0, __gx->genMode, 3, 16, num); + __gx->dirtyState |= 6; // gen mode dirty + BP mask dirty +} + +void GXSetIndTexOrder(GXIndTexStageID indStage, GXTexCoordID texCoord, GXTexMapID texMap) { + switch (indStage) { + case GX_INDTEXSTAGE0: + SET_REG_FIELD(0, __gx->iref, 3, 0, texMap); + SET_REG_FIELD(0, __gx->iref, 3, 3, texCoord); + break; + case GX_INDTEXSTAGE1: + SET_REG_FIELD(0, __gx->iref, 3, 6, texMap); + SET_REG_FIELD(0, __gx->iref, 3, 9, texCoord); + break; + case GX_INDTEXSTAGE2: + SET_REG_FIELD(0, __gx->iref, 3, 12, texMap); + SET_REG_FIELD(0, __gx->iref, 3, 15, texCoord); + break; + case GX_INDTEXSTAGE3: + SET_REG_FIELD(0, __gx->iref, 3, 18, texMap); + SET_REG_FIELD(0, __gx->iref, 3, 21, texCoord); + break; + } + GX_WRITE_RAS_REG(__gx->iref); + __gx->bpSent = 1; + __gx->dirtyState |= 3; // SU tex regs + BP mask dirty +} + +void GXSetIndTexCoordScale(GXIndTexStageID indStage, GXIndTexScale scaleS, GXIndTexScale scaleT) { + if (indStage < GX_INDTEXSTAGE2) { + switch (indStage) { + case GX_INDTEXSTAGE0: + SET_REG_FIELD(0, __gx->IndTexScale0, 4, 0, scaleS); + SET_REG_FIELD(0, __gx->IndTexScale0, 4, 4, scaleT); + break; + case GX_INDTEXSTAGE1: + SET_REG_FIELD(0, __gx->IndTexScale0, 4, 8, scaleS); + SET_REG_FIELD(0, __gx->IndTexScale0, 4, 12, scaleT); + break; + } + GX_WRITE_RAS_REG(__gx->IndTexScale0); + } else { + switch (indStage) { + case GX_INDTEXSTAGE2: + SET_REG_FIELD(0, __gx->IndTexScale1, 4, 0, scaleS); + SET_REG_FIELD(0, __gx->IndTexScale1, 4, 4, scaleT); + break; + case GX_INDTEXSTAGE3: + SET_REG_FIELD(0, __gx->IndTexScale1, 4, 8, scaleS); + SET_REG_FIELD(0, __gx->IndTexScale1, 4, 12, scaleT); + break; + } + GX_WRITE_RAS_REG(__gx->IndTexScale1); + } + __gx->bpSent = 1; +} + +void GXSetIndTexMtx(GXIndTexMtxID id, const void* offset, s8 scaleExp) { + u32 idx = 0; + if (id >= GX_ITM_0 && id <= GX_ITM_2) { + idx = id - GX_ITM_0; + } else if (id >= GX_ITM_S0 && id <= GX_ITM_S2) { + idx = id - GX_ITM_S0; + } else if (id >= GX_ITM_T0 && id <= GX_ITM_T2) { + idx = id - GX_ITM_T0; + } + + const auto* mtx = reinterpret_cast(offset); + s32 adjScale = scaleExp + 17; + + // Write 3 BP registers for the 2x3 matrix + + // Column 0: m[0][0] and m[1][0] + u32 reg0 = 0; + s32 m00 = static_cast(1024.0f * mtx[0]) & 0x7FF; + s32 m10 = static_cast(1024.0f * mtx[3]) & 0x7FF; + SET_REG_FIELD(0, reg0, 11, 0, m00); + SET_REG_FIELD(0, reg0, 11, 11, m10); + SET_REG_FIELD(0, reg0, 2, 22, adjScale & 3); + SET_REG_FIELD(0, reg0, 8, 24, idx * 3 + 6); + GX_WRITE_RAS_REG(reg0); + + // Column 1: m[0][1] and m[1][1] + u32 reg1 = 0; + s32 m01 = static_cast(1024.0f * mtx[1]) & 0x7FF; + s32 m11 = static_cast(1024.0f * mtx[4]) & 0x7FF; + SET_REG_FIELD(0, reg1, 11, 0, m01); + SET_REG_FIELD(0, reg1, 11, 11, m11); + SET_REG_FIELD(0, reg1, 2, 22, (adjScale >> 2) & 3); + SET_REG_FIELD(0, reg1, 8, 24, idx * 3 + 7); + GX_WRITE_RAS_REG(reg1); + + // Column 2: m[0][2] and m[1][2] + u32 reg2 = 0; + s32 m02 = static_cast(1024.0f * mtx[2]) & 0x7FF; + s32 m12 = static_cast(1024.0f * mtx[5]) & 0x7FF; + SET_REG_FIELD(0, reg2, 11, 0, m02); + SET_REG_FIELD(0, reg2, 11, 11, m12); + SET_REG_FIELD(0, reg2, 2, 22, (adjScale >> 4) & 3); + SET_REG_FIELD(0, reg2, 8, 24, idx * 3 + 8); + GX_WRITE_RAS_REG(reg2); + __gx->bpSent = 1; +} + +void GXSetTevIndirect(GXTevStageID tevStage, GXIndTexStageID indStage, GXIndTexFormat fmt, GXIndTexBiasSel biasSel, + GXIndTexMtxID matrixSel, GXIndTexWrap wrapS, GXIndTexWrap wrapT, GXBool addPrev, GXBool indLod, + GXIndTexAlphaSel alphaSel) { + // BP register: tev_stage + 0x10 + u32 reg = 0; + SET_REG_FIELD(0, reg, 2, 0, indStage); + SET_REG_FIELD(0, reg, 2, 2, fmt); + SET_REG_FIELD(0, reg, 3, 4, biasSel); + SET_REG_FIELD(0, reg, 2, 7, alphaSel); + SET_REG_FIELD(0, reg, 4, 9, matrixSel); + SET_REG_FIELD(0, reg, 3, 13, wrapS); + SET_REG_FIELD(0, reg, 3, 16, wrapT); + SET_REG_FIELD(0, reg, 1, 19, indLod); + SET_REG_FIELD(0, reg, 1, 20, addPrev); + SET_REG_FIELD(0, reg, 8, 24, tevStage + 16); + GX_WRITE_RAS_REG(reg); + __gx->bpSent = 1; +} + +void GXSetTevDirect(GXTevStageID stageId) { + GXSetTevIndirect(stageId, GX_INDTEXSTAGE0, GX_ITF_8, GX_ITB_NONE, GX_ITM_OFF, GX_ITW_OFF, GX_ITW_OFF, GX_FALSE, + GX_FALSE, GX_ITBA_OFF); +} + +void GXSetTevIndWarp(GXTevStageID tevStage, GXIndTexStageID indStage, GXBool signedOffsets, GXBool replaceMode, + GXIndTexMtxID matrixSel) { + const auto wrap = replaceMode ? GX_ITW_0 : GX_ITW_OFF; + const auto biasSel = signedOffsets ? GX_ITB_STU : GX_ITB_NONE; + GXSetTevIndirect(tevStage, indStage, GX_ITF_8, biasSel, matrixSel, wrap, wrap, false, false, GX_ITBA_OFF); +} + +void GXSetTevIndRepeat(GXTevStageID tevStage) { + GXSetTevIndirect(tevStage, GX_INDTEXSTAGE0, GX_ITF_8, GX_ITB_NONE, GX_ITM_OFF, GX_ITW_0, GX_ITW_0, GX_TRUE, + GX_FALSE, GX_ITBA_OFF); +} + +void GXSetTevIndBumpXYZ(GXTevStageID tevStage, GXIndTexStageID indStage, GXIndTexMtxID matrixSel) { + GXSetTevIndirect(tevStage, indStage, GX_ITF_8, GX_ITB_STU, matrixSel, GX_ITW_OFF, GX_ITW_OFF, GX_FALSE, GX_FALSE, + GX_ITBA_OFF); +} + +void GXSetTevIndBumpST(GXTevStageID tevStage, GXIndTexStageID indStage, GXIndTexMtxID matrixSel) { + GXIndTexMtxID sMtx, tMtx; + switch (matrixSel) { + case GX_ITM_0: + sMtx = GX_ITM_S0; + tMtx = GX_ITM_T0; + break; + case GX_ITM_1: + sMtx = GX_ITM_S1; + tMtx = GX_ITM_T1; + break; + case GX_ITM_2: + sMtx = GX_ITM_S2; + tMtx = GX_ITM_T2; + break; + default: + sMtx = GX_ITM_OFF; + tMtx = GX_ITM_OFF; + break; + } + // Stage 0: STU bias, S-dynamic matrix, wrap=0/0 + GXSetTevIndirect(tevStage, indStage, GX_ITF_8, GX_ITB_STU, sMtx, GX_ITW_0, GX_ITW_0, GX_FALSE, GX_FALSE, + GX_ITBA_OFF); + // Stage 1: STU bias, T-dynamic matrix, wrap=0/0, add prev + GXSetTevIndirect(static_cast(tevStage + 1), indStage, GX_ITF_8, GX_ITB_STU, tMtx, GX_ITW_0, GX_ITW_0, + GX_TRUE, GX_FALSE, GX_ITBA_OFF); + // Stage 2: no bias, matrix off, wrap off, add prev + GXSetTevIndirect(static_cast(tevStage + 2), indStage, GX_ITF_8, GX_ITB_NONE, GX_ITM_OFF, GX_ITW_OFF, + GX_ITW_OFF, GX_TRUE, GX_FALSE, GX_ITBA_OFF); +} + +void GXSetTevIndTile(GXTevStageID tevStage, GXIndTexStageID indStage, u16 tileSizeS, u16 tileSizeT, u16 tileSizeS_exp, + u16 tileSizeT_exp, GXIndTexFormat fmt, GXIndTexMtxID matrixSel, GXIndTexBiasSel biasSel, + GXIndTexAlphaSel alphaSel) { + // Convert tile size exponents to wrap modes + auto sizeToWrap = [](u16 sizeExp) -> GXIndTexWrap { + switch (sizeExp) { + case 8: return GX_ITW_256; + case 7: return GX_ITW_128; + case 6: return GX_ITW_64; + case 5: return GX_ITW_32; + case 4: return GX_ITW_16; + default: return GX_ITW_0; + } + }; + GXIndTexWrap wrapS = sizeToWrap(tileSizeS_exp); + GXIndTexWrap wrapT = sizeToWrap(tileSizeT_exp); + + // Set up the indirect matrix for tiling + f32 mtx[2][3] = {}; + s8 scaleExp = 0; + if (biasSel == GX_ITB_S || biasSel == GX_ITB_ST || biasSel == GX_ITB_SU || biasSel == GX_ITB_STU) { + mtx[0][0] = static_cast(tileSizeS); + scaleExp = tileSizeS_exp; + } + if (biasSel == GX_ITB_T || biasSel == GX_ITB_ST || biasSel == GX_ITB_TU || biasSel == GX_ITB_STU) { + mtx[1][1] = static_cast(tileSizeT); + scaleExp = tileSizeT_exp; + } + GXSetIndTexMtx(matrixSel, mtx, scaleExp); + GXSetTevIndirect(tevStage, indStage, fmt, biasSel, matrixSel, wrapS, wrapT, GX_FALSE, GX_FALSE, alphaSel); +} +} diff --git a/aurora-main/lib/dolphin/gx/GXCpu2Efb.cpp b/aurora-main/lib/dolphin/gx/GXCpu2Efb.cpp new file mode 100644 index 0000000..9815bd4 --- /dev/null +++ b/aurora-main/lib/dolphin/gx/GXCpu2Efb.cpp @@ -0,0 +1,20 @@ +#include "gx.hpp" + +#include "../../gfx/depth_peek.hpp" + +#include + +void GXPeekZ(u16 x, u16 y, u32* z) { + aurora::gfx::depth_peek::poll(); + + if (z != nullptr) { + u32 value = 0; + if (aurora::gfx::depth_peek::read_latest(x, y, value)) { + *z = value; + } else { + *z = g_gxState.clearDepth & 0x00ffffffu; + } + } + + aurora::gfx::depth_peek::request_snapshot(); +} diff --git a/aurora-main/lib/dolphin/gx/GXCull.cpp b/aurora-main/lib/dolphin/gx/GXCull.cpp new file mode 100644 index 0000000..0611778 --- /dev/null +++ b/aurora-main/lib/dolphin/gx/GXCull.cpp @@ -0,0 +1,60 @@ +#include "gx.hpp" +#include "__gx.h" + +extern "C" { + +void GXSetScissor(u32 left, u32 top, u32 width, u32 height) { + const u32 tp = top + 342; + const u32 lf = left + 342; + const u32 bm = tp + height - 1; + const u32 rt = lf + width - 1; + + SET_REG_FIELD(0, __gx->suScis0, 11, 0, tp); + SET_REG_FIELD(0, __gx->suScis0, 11, 12, lf); + SET_REG_FIELD(0, __gx->suScis1, 11, 0, bm); + SET_REG_FIELD(0, __gx->suScis1, 11, 12, rt); + + GX_WRITE_RAS_REG(__gx->suScis0); + GX_WRITE_RAS_REG(__gx->suScis1); + __gx->bpSent = 1; +} + +// TODO GXSetScissorBoxOffset + +void GXSetCullMode(GXCullMode mode) { + // Swap front/back to match hardware convention + GXCullMode hwMode; + switch (mode) { + case GX_CULL_FRONT: + hwMode = GX_CULL_BACK; + break; + case GX_CULL_BACK: + hwMode = GX_CULL_FRONT; + break; + default: + hwMode = mode; + break; + } + SET_REG_FIELD(0, __gx->genMode, 2, 14, hwMode); + __gx->dirtyState |= 4; // gen mode dirty +} + +void GXSetClipMode(GXClipMode mode) { + GX_WRITE_XF_REG(5, mode); + __gx->bpSent = 1; +} + +void GXSetCoPlanar(GXBool enable) { + u32 reg; + SET_REG_FIELD(0, __gx->genMode, 1, 19, enable); + + // This sets a mask that causes only one bit to be updated in genMode on the next write. + reg = 0x0008000; + SET_REG_FIELD(0, reg, 8, 24, 0xFE); + SET_REG_FIELD(0, reg, 24, 0, 1 << 19); + GX_WRITE_RAS_REG(reg); + + // immediately flush + GX_WRITE_RAS_REG(__gx->genMode); +} +} diff --git a/aurora-main/lib/dolphin/gx/GXDispList.cpp b/aurora-main/lib/dolphin/gx/GXDispList.cpp new file mode 100644 index 0000000..bf22212 --- /dev/null +++ b/aurora-main/lib/dolphin/gx/GXDispList.cpp @@ -0,0 +1,89 @@ +#include "gx.hpp" +#include "__gx.h" + +#include "../../gx/command_processor.hpp" +#include "../../gx/fifo.hpp" + +#include + +static __GXData_struct sSavedGXData; + +extern "C" { +void GXBeginDisplayList(void* list, u32 size) { + CHECK(!aurora::gx::fifo::in_display_list(), "Display list began twice!"); + + // Flush any pending dirty state before recording + if (__gx->dirtyState != 0) { + __GXSetDirtyState(); + } + + // Save current shadow register state if requested + if (__gx->dlSaveContext != 0) { + std::memcpy(&sSavedGXData, __gx, sizeof(sSavedGXData)); + } + + __gx->inDispList = 1; + + // Redirect FIFO writes to the user-provided buffer + aurora::gx::fifo::begin_display_list(static_cast(list), size); +} + +u32 GXEndDisplayList() { + // Flush any pending dirty state into the display list + if (__gx->dirtyState != 0) { + __GXSetDirtyState(); + } + + // End FIFO redirection and get the byte count (ROUNDUP32) + u32 bytesWritten = aurora::gx::fifo::end_display_list(); + + // Restore saved shadow register state + if (__gx->dlSaveContext != 0) { + std::memcpy(__gx, &sSavedGXData, sizeof(*__gx)); + } + + __gx->inDispList = 0; + + return bytesWritten; +} + +void GXCallDisplayList(const void* data, u32 nbytes) { + // Flush any pending dirty state before calling + if (__gx->dirtyState != 0) { + __GXSetDirtyState(); + } + + // Flush pending primitives + if (*reinterpret_cast(&__gx->vNum) != 0) { + __GXSendFlushPrim(); + } + + // Nested display-list calls add bytes instead of changing live renderer state. + if (aurora::gx::fifo::in_display_list()) { + aurora::gx::fifo::write_data(data, nbytes); + return; + } + + // Decode the display list immediately while its borrowed resources are valid. + aurora::gx::fifo::drain(); + aurora::gx::fifo::process(static_cast(data), nbytes, true); +} + +void GXCallDisplayListLE(const void* data, u32 nbytes) { + // Flush any pending dirty state before calling + if (__gx->dirtyState != 0) { + __GXSetDirtyState(); + } + + // Flush pending primitives + if (*reinterpret_cast(&__gx->vNum) != 0) { + __GXSendFlushPrim(); + } + + // Decode little-endian lists separately after finishing the normal FIFO work. + aurora::gx::fifo::drain(); + + // Process the display list through the command processor (little-endian) + aurora::gx::fifo::process(static_cast(data), nbytes, false); +} +} diff --git a/aurora-main/lib/dolphin/gx/GXDraw.cpp b/aurora-main/lib/dolphin/gx/GXDraw.cpp new file mode 100644 index 0000000..314104c --- /dev/null +++ b/aurora-main/lib/dolphin/gx/GXDraw.cpp @@ -0,0 +1,549 @@ +#include "gx.hpp" + +extern "C" { + +static GXVtxDescList vcd[27]; +static GXVtxAttrFmtList vat[27]; + +static void GetVertState(void) { + GXGetVtxDescv(vcd); + GXGetVtxAttrFmtv(GX_VTXFMT3, vat); + GXClearVtxDesc(); + GXSetVtxDesc(GX_VA_POS, GX_DIRECT); + GXSetVtxDesc(GX_VA_NRM, GX_DIRECT); + GXSetVtxAttrFmt(GX_VTXFMT3, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); + GXSetVtxAttrFmt(GX_VTXFMT3, GX_VA_NRM, GX_NRM_XYZ, GX_F32, 0); +} + +static void RestoreVertState(void) { + GXSetVtxDescv(vcd); + GXSetVtxAttrFmtv(GX_VTXFMT3, vat); +} + +static void vsub(f32 p1[3], f32 p2[3], f32 u[3]) { + u32 i; + + for (i = 0; i < 3; i++) { + u[i] = p2[i] - p1[i]; + } +} + +static void vcross(f32 u[3], f32 v[3], f32 n[3]) { + f32 n1[3]; + + n1[0] = (u[1] * v[2]) - (u[2] * v[1]); + n1[1] = (u[2] * v[0]) - (u[0] * v[2]); + n1[2] = (u[0] * v[1]) - (u[1] * v[0]); + n[0] = n1[0]; + n[1] = n1[1]; + n[2] = n1[2]; +} + +static void normalize(f32 v[3]) { + f32 d = sqrtf((v[0] * v[0]) + (v[1] * v[1]) + (v[2] * v[2])); + + CHECK(d != 0.0f, "normalize: zero length vector"); + v[0] /= d; + v[1] /= d; + v[2] /= d; +} + +static void myvertex(f32 v[3], f32 n[3]) { + GXPosition3f32(v[0], v[1], v[2]); + GXNormal3f32(n[0], n[1], n[2]); +} + +static void DumpTriangle(f32 v0[3], f32 v1[3], f32 v2[3]) { + GXBegin(GX_TRIANGLES, GX_VTXFMT3, 3); + myvertex(v0, v0); + myvertex(v1, v1); + myvertex(v2, v2); + GXEnd(); +} + +static void Subdivide(u8 depth, f32 v0[3], f32 v1[3], f32 v2[3]) { + f32 v01[3]; + f32 v12[3]; + f32 v20[3]; + u32 i; + + if (depth == 0) { + DumpTriangle(v0, v1, v2); + return; + } + + for (i = 0; i < 3; i++) { + v01[i] = v0[i] + v1[i]; + v12[i] = v1[i] + v2[i]; + v20[i] = v2[i] + v0[i]; + } + + normalize(v01); + normalize(v12); + normalize(v20); + Subdivide(depth - 1, v0, v01, v20); + Subdivide(depth - 1, v1, v12, v01); + Subdivide(depth - 1, v2, v20, v12); + Subdivide(depth - 1, v01, v12, v20); +} + +static void SubDivTriangle(u8 depth, u8 i, f32 (*data)[3], u8 (*ndx)[3]) { + f32 *x0 = data[ndx[i][0]]; + f32 *x1 = data[ndx[i][1]]; + f32 *x2 = data[ndx[i][2]]; + + Subdivide(depth, x0, x1, x2); +} + +void GXDrawCylinder(u8 numEdges) { + s32 i; + f32 top; + f32 bottom; + f32 x[100]; + f32 y[100]; + f32 angle; + + top = 1.0f; + bottom = -top; + CHECK(numEdges <= 99, "GXDrawCylinder: too many edges"); + + GetVertState(); + + for (i = 0; i <= numEdges; i++) { + angle = (3.1415927f * (2.0f * i)) / numEdges; + x[i] = cosf(angle); + y[i] = sinf(angle); + } + + GXBegin(GX_TRIANGLESTRIP, GX_VTXFMT3, (numEdges + 1) * 2); + for (i = 0; i <= numEdges; i++) { + GXPosition3f32(x[i], y[i], bottom); + GXNormal3f32(x[i], y[i], 0.0f); + GXPosition3f32(x[i], y[i], top); + GXNormal3f32(x[i], y[i], 0.0f); + + } + GXEnd(); + + GXBegin(GX_TRIANGLEFAN, GX_VTXFMT3, numEdges + 2); + GXPosition3f32(0.0f, 0.0f, top); + GXNormal3f32(0.0f, 0.0f, 1.0f); + for (i = 0; i <= numEdges; i++) { + GXPosition3f32(x[i], -y[i], top); + GXNormal3f32(0.0f, 0.0f, 1.0f); + + } + GXEnd(); + + GXBegin(GX_TRIANGLEFAN, GX_VTXFMT3, numEdges + 2); + GXPosition3f32(0.0f, 0.0f, bottom); + GXNormal3f32(0.0f, 0.0f, -1.0f); + for (i = 0; i <= numEdges; i++) { + GXPosition3f32(x[i], y[i], bottom); + GXNormal3f32(0.0f, 0.0f, -1.0f); + } + GXEnd(); + + RestoreVertState(); +} + +void GXDrawTorus(f32 rc, u8 numc, u8 numt) { + GXAttrType ttype; + s32 i, j, k; + f32 s, t; + f32 x, y, z; + f32 twopi = 6.2831855f; + f32 rt; + + CHECK(rc < 1.0f, "GXDrawTorus: doughnut too fat"); + + rt = 1.0f - rc; + GXGetVtxDesc(GX_VA_TEX0, &ttype); + GetVertState(); + + if (ttype != GX_NONE) { + GXSetVtxDesc(GX_VA_TEX0, GX_DIRECT); + GXSetVtxAttrFmt(GX_VTXFMT3, GX_VA_TEX0, GX_TEX_ST, GX_F32, 0); + } + + for (i = 0; i < numc; i++) { + GXBegin(GX_TRIANGLESTRIP, GX_VTXFMT3, (numt + 1) * 2); + for (j = 0; j <= numt; j++) { + for (k = 1; k >= 0; k--) { + s = (i + k) % numc; + t = j % numt; + x = (rt - rc * cosf(s * twopi / numc)) * cosf(t * twopi / numt); + y = (rt - rc * cosf(s * twopi / numc)) * sinf(t * twopi / numt); + z = rc * sinf(s * twopi / numc); + GXPosition3f32(x, y, z); + x = -cosf(t * twopi / numt) * cosf(s * twopi / numc); + y = -sinf(t * twopi / numt) * cosf(s * twopi / numc); + z = sinf(s * twopi / numc); + GXNormal3f32(x, y, z); + if (ttype != GX_NONE) { + GXTexCoord2f32((i + k) / (f32)numc, j / (f32)numt); + } + } + } + GXEnd(); + } + RestoreVertState(); +} + +void GXDrawSphere(u8 numMajor, u8 numMinor) { + GXAttrType ttype; + f32 radius = 1.0f; + f32 majorStep = 3.1415927f / numMajor; + f32 minorStep = 6.2831855f / numMinor; + s32 i, j; + f32 a, b; + f32 r0, r1; + f32 z0, z1; + f32 c; + f32 x, y; + + GXGetVtxDesc(GX_VA_TEX0, &ttype); + GetVertState(); + + if (ttype != GX_NONE) { + GXSetVtxDesc(GX_VA_TEX0, GX_DIRECT); + GXSetVtxAttrFmt(GX_VTXFMT3, GX_VA_TEX0, GX_TEX_ST, GX_RGBA6, 0); + } + + for (i = 0; i < numMajor; i++) { + a = i * majorStep; + b = a + majorStep; + r0 = radius * sinf(a); + r1 = radius * sinf(b); + z0 = radius * cosf(a); + z1 = radius * cosf(b); + GXBegin(GX_TRIANGLESTRIP, GX_VTXFMT3, (numMinor + 1) * 2); + for (j = 0; j <= numMinor; j++) { + c = j * minorStep; + x = cosf(c); + y = sinf(c); + GXPosition3f32(x * r1, y * r1, z1); + GXNormal3f32((x * r1) / radius, (y * r1) / radius, z1 / radius); + if (ttype != GX_NONE) { + GXTexCoord2f32((f32)j / (f32)numMinor, (f32)(i + 1) / (f32)numMajor); + } + GXPosition3f32(x * r0, y * r0, z0); + GXNormal3f32((x * r0) / radius, (y * r0) / radius, z0 / radius); + if (ttype != GX_NONE) { + GXTexCoord2f32((f32)j / (f32)numMinor, (f32)i / (f32)numMajor); + } + } + GXEnd(); + } + RestoreVertState(); +} + +static void GXDrawCubeFace(f32 nx, f32 ny, f32 nz, f32 tx, f32 ty, f32 tz, f32 bx, f32 by, f32 bz, GXAttrType binormal, GXAttrType texture) { + GXPosition3f32(0.57735026f * (nx + tx + bx), 0.57735026f * (ny + ty + by), 0.57735026f * (nz + tz + bz)); + GXNormal3f32(nx, ny, nz); + + if (binormal != GX_NONE) { + GXNormal3f32(tx, ty, tz); + GXNormal3f32(bx, by, bz); + } + + if (texture != GX_NONE) { + GXTexCoord2s8(1, 1); + } + + GXPosition3f32(0.57735026f * (nx - tx + bx), 0.57735026f * (ny - ty + by), 0.57735026f * (nz - tz + bz)); + GXNormal3f32(nx, ny, nz); + + if (binormal != GX_NONE) { + GXNormal3f32(tx, ty, tz); + GXNormal3f32(bx, by, bz); + } + + if (texture != GX_NONE) { + GXTexCoord2s8(0, 1); + } + + GXPosition3f32(0.57735026f * (nx - tx - bx), 0.57735026f * (ny - ty - by), 0.57735026f * (nz - tz - bz)); + GXNormal3f32(nx, ny, nz); + + if (binormal != GX_NONE) { + GXNormal3f32(tx, ty, tz); + GXNormal3f32(bx, by, bz); + } + + if (texture != GX_NONE) { + GXTexCoord2s8(0, 0); + } + + GXPosition3f32(0.57735026f * (nx + tx - bx), 0.57735026f * (ny + ty - by), 0.57735026f * (nz + tz - bz)); + GXNormal3f32(nx, ny, nz); + + if (binormal != GX_NONE) { + GXNormal3f32(tx, ty, tz); + GXNormal3f32(bx, by, bz); + } + + if (texture != GX_NONE) { + GXTexCoord2s8(1, 0); + } +} + +void GXDrawCube(void) { + GXAttrType ntype; + GXAttrType ttype; + + GXGetVtxDesc(GX_VA_NBT, &ntype); + GXGetVtxDesc(GX_VA_TEX0, &ttype); + GetVertState(); + if (ntype != GX_NONE) { + GXSetVtxDesc(GX_VA_NBT, GX_DIRECT); + GXSetVtxAttrFmt(GX_VTXFMT3, GX_VA_NBT, GX_TEX_ST, GX_RGBA6, 0); + } + if (ttype != GX_NONE) { + GXSetVtxDesc(GX_VA_TEX0, GX_DIRECT); + GXSetVtxAttrFmt(GX_VTXFMT3, GX_VA_TEX0, GX_TEX_ST, GX_RGB8, 0); + } + + GXBegin(GX_QUADS, GX_VTXFMT3, 24); + GXDrawCubeFace(-1.0f, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, ntype, ttype); + GXDrawCubeFace(1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, -1.0f, ntype, ttype); + GXDrawCubeFace(0.0f, -1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, ntype, ttype); + GXDrawCubeFace(0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, -1.0f, 0.0f, 0.0f, ntype, ttype); + GXDrawCubeFace(0.0f, 0.0f, -1.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, ntype, ttype); + GXDrawCubeFace(0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, ntype, ttype); + GXEnd(); + + RestoreVertState(); +} + +static u32 polygons[12][5] = { + { 0, 12, 10, 11, 16 }, + { 1, 17, 8, 9, 13 }, + { 2, 14, 9, 8, 18 }, + { 3, 19, 11, 10, 15 }, + { 4, 14, 2, 3, 15 }, + { 5, 12, 0, 1, 13 }, + { 6, 17, 1, 0, 16 }, + { 7, 19, 3, 2, 18 }, + { 8, 17, 6, 7, 18 }, + { 9, 14, 4, 5, 13 }, + { 10, 12, 5, 4, 15 }, + { 11, 19, 7, 6, 16 }, +}; + +static f32 verts[20][3] = { + { -0.809015f, 0.0f, 0.309015f }, + { -0.809015f, 0.0f, -0.309015f }, + { 0.809015f, 0.0f, -0.309015f }, + { 0.809015f, 0.0f, 0.309015f }, + { 0.309015f, -0.809015f, 0.0f }, + { -0.309015f, -0.809015f, 0.0f }, + { -0.309015f, 0.809015f, 0 }, + { 0.309015f, 0.809015f, 0 }, + { 0.0f, 0.309015f, -0.809015f }, + { 0.0f, -0.309015f, -0.809015f }, + { 0.0f, -0.309015f, 0.809015f }, + { 0.0f, 0.309015f, 0.809015f }, + { -0.5f, -0.5f, 0.5 }, + { -0.5f, -0.5f, -0.5 }, + { 0.5f, -0.5f, -0.5 }, + { 0.5f, -0.5f, 0.5 }, + { -0.5f, 0.5f, 0.5 }, + { -0.5f, 0.5f, -0.5 }, + { 0.5f, 0.5f, -0.5 }, + { 0.5f, 0.5f, 0.5 }, +}; + +void GXDrawDodeca(void) { + u32 i; + f32 *p0; + f32 *p1; + f32 *p2; + f32 u[3]; + f32 v[3]; + f32 n[3]; + + GetVertState(); + for (i = 0; i < 12; i++) { + p0 = verts[polygons[i][0]]; + p1 = verts[polygons[i][1]]; + p2 = verts[polygons[i][2]]; + vsub(p1, p2, u); + vsub(p1, p0, v); + vcross(u, v, n); + normalize(n); + GXBegin(GX_TRIANGLEFAN, GX_VTXFMT3, 5); + myvertex(verts[polygons[i][4]], n); + myvertex(verts[polygons[i][3]], n); + myvertex(p2, n); + myvertex(p1, n); + myvertex(p0, n); + GXEnd(); + } + RestoreVertState(); +} + +static f32 odata[6][3] = { + { 1.0f, 0.0f, 0.0f }, + { -1.0f, 0.0f, 0.0f }, + { 0.0f, 1.0f, 0.0f }, + { 0.0f, -1.0f, 0.0f }, + { 0.0f, 0.0f, 1.0f }, + { 0.0f, 0.0f, -1.0f }, +}; + +static u8 ondex[8][3] = { + { 0, 4, 2 }, + { 1, 2, 4 }, + { 0, 3, 4 }, + { 1, 4, 3 }, + { 0, 2, 5 }, + { 1, 5, 2 }, + { 0, 5, 3 }, + { 1, 3, 5 }, +}; + +void GXDrawOctahedron(void) { + s32 i; + + GetVertState(); + for (i = 7; i >= 0; i--) { + SubDivTriangle(0, i, odata, ondex); + } + RestoreVertState(); +} + +static f32 idata[12][3] = { + { -0.5257311f, 0.0f, 0.8506508f }, + { 0.5257311f, 0.0f, 0.8506508f }, + { -0.5257311f, 0.0f, -0.8506508f }, + { 0.5257311f, 0.0f, -0.8506508f }, + { 0.0f, 0.8506508f, 0.5257311f }, + { 0.0f, 0.8506508f, -0.5257311f }, + { 0.0f, -0.8506508f, 0.5257311f }, + { 0.0f, -0.8506508f, -0.5257311f }, + { 0.8506508f, 0.5257311f, 0.0f }, + { -0.8506508f, 0.5257311f, 0.0f }, + { 0.8506508f, -0.5257311f, 0.0f }, + { -0.8506508f, -0.5257311f, 0.0f }, +}; + +static u8 uindex[20][3] = { + { 0, 4, 1 }, + { 0, 9, 4 }, + { 9, 5, 4 }, + { 4, 5, 8 }, + { 4, 8, 1 }, + { 8, 10, 1 }, + { 8, 3, 10 }, + { 5, 3, 8 }, + { 5, 2, 3 }, + { 2, 7, 3 }, + { 7, 10, 3 }, + { 7, 6, 10 }, + { 7, 11, 6 }, + { 11, 0, 6 }, + { 0, 1, 6 }, + { 6, 1, 10 }, + { 9, 0, 11 }, + { 9, 11, 2 }, + { 9, 2, 5 }, + { 7, 2, 11 }, +}; + +void GXDrawIcosahedron(void) { + s32 i; + + GetVertState(); + for (i = 19; i >= 0; i--) { + SubDivTriangle(0, i, idata, uindex); + } + RestoreVertState(); +} + +void GXDrawSphere1(u8 depth) { + s32 i; + + GetVertState(); + for (i = 19; i >= 0; i--) { + SubDivTriangle(depth, i, idata, uindex); + } + RestoreVertState(); +} + +static u32 CmpNormal32(f32 n1[3], f32 n2[3]) { + u32 i; + + for (i = 0; i < 3; i++) { + if (n1[i] != n2[i]) + return FALSE; + } + return TRUE; +} + +static u32 nrm_cnt; +static f32* nrm_tab; + +static void AddNormal(f32 n[3]) { + u32 indx; + u32 i; + + for (i = 0; i < nrm_cnt; i++) { + if (CmpNormal32(n, &nrm_tab[i * 3])) + return; + } + indx = nrm_cnt * 3; + nrm_tab[indx + 0] = n[0]; + nrm_tab[indx + 1] = n[1]; + nrm_tab[indx + 2] = n[2]; + nrm_cnt++; +} + +static void SubdivideNrm(u8 depth, f32 v0[3], f32 v1[3], f32 v2[3]) { + f32 v01[3]; + f32 v12[3]; + f32 v20[3]; + u32 i; + + if (depth == 0) { + AddNormal(v0); + AddNormal(v1); + AddNormal(v2); + return; + } + + for (i = 0; i < 3; i++) { + v01[i] = v0[i] + v1[i]; + v12[i] = v1[i] + v2[i]; + v20[i] = v2[i] + v0[i]; + } + + normalize(v01); + normalize(v12); + normalize(v20); + SubdivideNrm(depth - 1, v0, v01, v20); + SubdivideNrm(depth - 1, v1, v12, v01); + SubdivideNrm(depth - 1, v2, v20, v12); + SubdivideNrm(depth - 1, v01, v12, v20); +} + +static void SubDivNrm(u8 depth, u8 i, f32 (*data)[3], u8 (*ndx)[3]) { + f32* x0 = data[ndx[i][0]]; + f32* x1 = data[ndx[i][1]]; + f32* x2 = data[ndx[i][2]]; + + SubdivideNrm(depth, x0, x1, x2); +} + +u32 GXGenNormalTable(u8 depth, f32* table) { + s32 i; + + nrm_cnt = 0; + nrm_tab = table; + for (i = 7; i >= 0; i--) { + SubDivNrm(depth, i, odata, ondex); + } + + return nrm_cnt; +} +} diff --git a/aurora-main/lib/dolphin/gx/GXExtra.cpp b/aurora-main/lib/dolphin/gx/GXExtra.cpp new file mode 100644 index 0000000..32c0a79 --- /dev/null +++ b/aurora-main/lib/dolphin/gx/GXExtra.cpp @@ -0,0 +1,27 @@ +#include "gx.hpp" +#include "__gx.h" +#include "dolphin/gx/GXAurora.h" + +extern "C" { +void GXDestroyTexObj(GXTexObj* obj_) { + auto* obj = reinterpret_cast(obj_); + // Destroying a GX texture descriptor must not discard cached texture pixels. + obj->texObjId = 0; +} + +void GXDestroyTlutObj(GXTlutObj* obj_) { + auto* obj = reinterpret_cast(obj_); + if (obj->tlutObjId != 0) { + GX_WRITE_AURORA(GX_LOAD_AURORA_DESTROY_TLUT); + GX_WRITE_U32(obj->tlutObjId); + } + obj->tlutObjId = 0; +} + +void GXDestroyCopyTex(void* dest) { + if (dest != nullptr) { + GX_WRITE_AURORA(GX_LOAD_AURORA_DESTROY_COPY_TEX); + GX_WRITE_U64(reinterpret_cast(dest)); + } +} +} diff --git a/aurora-main/lib/dolphin/gx/GXFifo.cpp b/aurora-main/lib/dolphin/gx/GXFifo.cpp new file mode 100644 index 0000000..ab86547 --- /dev/null +++ b/aurora-main/lib/dolphin/gx/GXFifo.cpp @@ -0,0 +1,50 @@ +#include "gx.hpp" + +extern "C" { + +static GXFifoObj* GPFifo; +static GXFifoObj* CPUFifo; + +void GXGetGPStatus(GXBool* overhi, GXBool* underlow, GXBool* readIdle, GXBool* cmdIdle, GXBool* brkpt) { + *overhi = *underlow = *readIdle = *cmdIdle = *brkpt = false; + *readIdle = true; +} + +// TODO GXGetFifoStatus + +void GXGetFifoPtrs(GXFifoObj* fifo, void** readPtr, void** writePtr) { + *readPtr = NULL; + *writePtr = NULL; +} + +GXFifoObj* GXGetCPUFifo() { return CPUFifo; } + +GXFifoObj* GXGetGPFifo() { return GPFifo; } + +// TODO GXGetFifoBase +// TODO GXGetFifoSize +// TODO GXGetFifoLimits +// TODO GXSetBreakPtCallback +// TODO GXEnableBreakPt +// TODO GXDisableBreakPt + +void GXInitFifoBase(GXFifoObj* fifo, void* base, u32 size) {} + +void GXInitFifoPtrs(GXFifoObj* fifo, void* readPtr, void* writePtr) {} + +// TODO GXInitFifoLimits + +void GXSetCPUFifo(GXFifoObj* fifo) { CPUFifo = fifo; } + +void GXSetGPFifo(GXFifoObj* fifo) { GPFifo = fifo; } + +void GXSaveCPUFifo(GXFifoObj* fifo) {} + +// TODO GXSaveGPFifo +// TODO GXRedirectWriteGatherPipe +// TODO GXRestoreWriteGatherPipe +// TODO GXSetCurrentGXThread +// TODO GXGetCurrentGXThread +// TODO GXGetOverflowCount +// TODO GXResetOverflowCount +} \ No newline at end of file diff --git a/aurora-main/lib/dolphin/gx/GXFrameBuffer.cpp b/aurora-main/lib/dolphin/gx/GXFrameBuffer.cpp new file mode 100644 index 0000000..abd9f6c --- /dev/null +++ b/aurora-main/lib/dolphin/gx/GXFrameBuffer.cpp @@ -0,0 +1,595 @@ +#include "gx.hpp" +#include "__gx.h" + +#include "../../gfx/tex_copy_conv.hpp" +#include "../../gfx/efb_ram_copy.hpp" +#include "../../gfx/texture.hpp" +#include "../../gx/fifo.hpp" +#include "../../internal.hpp" +#include "../../window.hpp" +#include "../../gfx/clear.hpp" +#include "../../webgpu/gpu.hpp" +#include "../vi/vi_internal.hpp" + +#include +#include +#include +#include + +namespace { +struct CopyClearState { + bool clearColor = false; + bool clearAlpha = false; + bool clearDepth = false; + aurora::Vec4 clearColorValue{0.f, 0.f, 0.f, 1.f}; +}; + +CopyClearState get_copy_clear_state(GXBool clear) { + if (!clear) { + return {}; + } + + CopyClearState state{ + .clearColor = g_gxState.colorUpdate, + .clearAlpha = g_gxState.alphaUpdate, + .clearDepth = g_gxState.depthUpdate, + .clearColorValue = g_gxState.clearColor, + }; + + if (!aurora::gx::render_target_has_alpha(g_gxState.pixelFmt)) { + // RGB and depth EFB formats do not have an alpha channel to clear. + state.clearAlpha = false; + } + + return state; +} + +struct CopySourceRect { + aurora::gfx::ClipRect clearRect; + aurora::Vec4 sampleRect; +}; + +CopySourceRect map_texture_copy_source(const aurora::gfx::ClipRect& source, bool renderSpace) { + if (renderSpace || g_gxState.viewportPolicy == AURORA_VIEWPORT_NATIVE) { + return { + .clearRect = source, + .sampleRect = {static_cast(source.x), static_cast(source.y), static_cast(source.width), + static_cast(source.height)}, + }; + } + + const auto [logicalWidth, logicalHeight] = aurora::gx::logical_fb_size(); + const auto [targetWidth, targetHeight] = aurora::gfx::get_render_target_size(); + if (logicalWidth == 0 || logicalHeight == 0 || targetWidth == 0 || targetHeight == 0) { + return { + .clearRect = source, + .sampleRect = {static_cast(source.x), static_cast(source.y), static_cast(source.width), + static_cast(source.height)}, + }; + } + + struct MappedEdge { + float sample; + int32_t nearest; + }; + const auto mapEdge = [](int32_t edge, uint32_t logicalSize, uint32_t targetSize) { + // Round scaled copy edges consistently to avoid seams and off-by-one pixels. + const int64_t numerator = static_cast(edge) * static_cast(targetSize); + const int64_t denominator = static_cast(logicalSize); + const int64_t quotient = numerator / denominator; + const int64_t remainder = numerator % denominator; + const int64_t remainderMagnitude = remainder < 0 ? -remainder : remainder; + const int64_t roundingThreshold = (denominator + 1) / 2; + const int64_t nearestValue = + quotient + (remainderMagnitude >= roundingThreshold ? (numerator < 0 ? -1 : 1) : 0); + return MappedEdge{ + .sample = static_cast(static_cast(numerator) / static_cast(denominator)), + .nearest = static_cast(nearestValue), + }; + }; + const auto left = mapEdge(source.x, logicalWidth, targetWidth); + const auto top = mapEdge(source.y, logicalHeight, targetHeight); + const auto right = mapEdge(source.x + source.width, logicalWidth, targetWidth); + const auto bottom = mapEdge(source.y + source.height, logicalHeight, targetHeight); + return { + .clearRect = + { + .x = left.nearest, + .y = top.nearest, + .width = std::max(right.nearest - left.nearest, 1), + .height = std::max(bottom.nearest - top.nearest, 1), + }, + .sampleRect = {left.sample, top.sample, right.sample - left.sample, bottom.sample - top.sample}, + }; +} + + + + + +u32 pack_copy_filter_samples(u8 reg, const std::array, 12>& samplePattern, size_t first) { + u32 value = static_cast(reg) << 24; + for (size_t i = 0; i < 6; ++i) { + const size_t sample = first + i; + const u8 component = samplePattern[sample / 2][sample % 2] & 0x0fu; + value |= static_cast(component) << (i * 4); + } + return value; +} + +u32 pack_copy_filter0(const std::array& vfilter) { + return 0x53000000u | (static_cast(vfilter[0] & 0x3fu) << 0) | + (static_cast(vfilter[1] & 0x3fu) << 6) | (static_cast(vfilter[2] & 0x3fu) << 12) | + (static_cast(vfilter[3] & 0x3fu) << 18); +} + +u32 pack_copy_filter1(const std::array& vfilter) { + return 0x54000000u | (static_cast(vfilter[4] & 0x3fu) << 0) | + (static_cast(vfilter[5] & 0x3fu) << 6) | (static_cast(vfilter[6] & 0x3fu) << 12); +} + +std::array combined_copy_filter_coefficients(const std::array& vfilter) { + if (!g_gxState.copyFilterVf) { + return {0, 64, 0}; + } + + return { + static_cast(vfilter[0]) + static_cast(vfilter[1]), + static_cast(vfilter[2]) + static_cast(vfilter[3]) + static_cast(vfilter[4]), + static_cast(vfilter[5]) + static_cast(vfilter[6]), + }; +} + +aurora::Vec2 scale_copy_dst(u32 logicalWidth, u32 logicalHeight) { + if (g_gxState.viewportPolicy == AURORA_VIEWPORT_NATIVE) { + return {logicalWidth, logicalHeight}; + } + + const auto [logicalFbWidth, logicalFbHeight] = aurora::gx::logical_fb_size(); + const auto [targetWidth, targetHeight] = aurora::gfx::get_render_target_size(); + if (logicalFbWidth == 0 || logicalFbHeight == 0 || targetWidth == 0 || targetHeight == 0) { + return {logicalWidth, logicalHeight}; + } + + const float scaleX = static_cast(targetWidth) / static_cast(logicalFbWidth); + const float scaleY = static_cast(targetHeight) / static_cast(logicalFbHeight); + const auto scaledWidth = std::max(static_cast(std::lround(static_cast(logicalWidth) * scaleX)), 1); + const auto scaledHeight = std::max(static_cast(std::lround(static_cast(logicalHeight) * scaleY)), 1); + return {scaledWidth, scaledHeight}; +} + +aurora::gfx::TextureHandle create_copy_texture(u32 width, u32 height, GXTexFmt texCopyFmt) { + if (aurora::gfx::tex_copy_conv::needs_conversion(texCopyFmt)) { + return aurora::gfx::new_conv_texture(width, height, texCopyFmt, "Copy Conv Texture"); + } + + const auto fmt = texCopyFmt == GX_TF_RGB565 ? GX_TF_RGB565 : GX_TF_RGBA8; + return aurora::gfx::new_render_texture(width, height, fmt, "Resolved Texture"); +} + + // Reuse retired copy targets to avoid unbounded GPU texture allocation. +struct CopyTexturePoolEntry { + aurora::gx::GXState::CopyTextureKey key; + u32 scaledWidth = 0; + u32 scaledHeight = 0; + aurora::gfx::TextureHandle handle; +}; +std::vector g_copyTexturePool; + // Keep only a few reusable copy targets per destination. +constexpr size_t kCopyTexturePoolPerKey = 3; + +aurora::gfx::TextureHandle acquire_copy_texture(const aurora::gx::GXState::CopyTextureKey& key, u32 width, u32 height, + GXTexFmt texCopyFmt) { + size_t sameKey = 0; + for (auto it = g_copyTexturePool.begin(); it != g_copyTexturePool.end();) { + if (!(it->key == key)) { + ++it; + continue; + } + // Discard retired targets when their internal resolution changes. + if ((it->scaledWidth != width || it->scaledHeight != height) && it->handle.use_count() <= 1) { + it = g_copyTexturePool.erase(it); + continue; + } + ++sameKey; + if (it->scaledWidth == width && it->scaledHeight == height && it->handle.use_count() == 1) { + return it->handle; + } + ++it; + } + + auto handle = create_copy_texture(width, height, texCopyFmt); + if (sameKey < kCopyTexturePoolPerKey) { + g_copyTexturePool.push_back({key, width, height, handle}); + } + return handle; +} + +u16 get_num_xfb_lines_internal(u16 efbHeight, u32 iScale) { + CHECK(efbHeight != 0, "GXGetNumXfbLines requires non-zero EFB height"); + CHECK(iScale != 0, "invalid XFB line scale"); + + const u32 count = static_cast(efbHeight - 1u) * 0x100u; + u32 realHeight = (count / iScale) + 1u; + + u32 reducedScale = iScale; + if (reducedScale > 0x80u && reducedScale < 0x100u) { + while ((reducedScale & 1u) == 0u) { + reducedScale >>= 1; + } + if (reducedScale != 0u && (efbHeight % reducedScale) == 0u) { + ++realHeight; + } + } + + return static_cast(std::min(realHeight, 0x400u)); +} + +u32 y_scale_to_integer(f32 yScale) { + CHECK(yScale > 0.f, "GX display copy y-scale must be positive"); + return static_cast(256.f / yScale) & 0x1ffu; +} + +} // namespace + +namespace aurora::gx { +void prune_copy_texture_pool(const void* dest) noexcept { + if (dest == nullptr) { + g_copyTexturePool.clear(); + return; + } + std::erase_if(g_copyTexturePool, [dest](const CopyTexturePoolEntry& entry) { return entry.key.dest == dest; }); +} +} // namespace aurora::gx + +extern "C" { +GXRenderModeObj GXNtsc480IntDf = { + VI_TVMODE_NTSC_INT, + 640, + 480, + 480, + 40, + 0, + 640, + 480, + VI_XFBMODE_DF, + 0, + 0, + {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, + {8, 8, 10, 12, 10, 8, 8}, +}; +GXRenderModeObj GXNtsc480Int = { + VI_TVMODE_NTSC_INT, + 640, + 480, + 480, + 40, + 0, + 640, + 480, + VI_XFBMODE_DF, + 0, + 0, + {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, + {0, 0, 21, 22, 21, 0, 0}, +}; +GXRenderModeObj GXPal528IntDf = { + VI_TVMODE_PAL_INT, + 704, + 528, + 480, + 40, + 0, + 640, + 480, + VI_XFBMODE_DF, + 0, + 0, + {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, + {8, 8, 10, 12, 10, 8, 8}, +}; +GXRenderModeObj GXMpal480IntDf = { + VI_TVMODE_PAL_INT, + 640, + 480, + 480, + 40, + 0, + 640, + 480, + VI_XFBMODE_DF, + 0, + 0, + {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, + {8, 8, 10, 12, 10, 8, 8}, +}; + +void GXAdjustForOverscan(GXRenderModeObj* rmin, GXRenderModeObj* rmout, u16 hor, u16 ver) { + *rmout = *rmin; + const auto renderSize = aurora::gfx::get_render_target_size(); + rmout->fbWidth = static_cast(std::min(renderSize.x, UINT16_MAX)); + rmout->efbHeight = static_cast(std::min(renderSize.y, UINT16_MAX)); + rmout->xfbHeight = static_cast(std::min(renderSize.y, UINT16_MAX)); +} + +void GXSetDispCopySrc(u16 left, u16 top, u16 wd, u16 ht) { + g_gxState.dispCopySrc = {left, top, wd, ht}; + GX_WRITE_RAS_REG(0x49000000u | ((static_cast(top) & 0x3ffu) << 10) | (static_cast(left) & 0x3ffu)); + GX_WRITE_RAS_REG(0x4a000000u | (((static_cast(ht) - 1u) * 0x400u) & 0x000ffc00u) | + ((static_cast(wd) - 1u) & 0x3ffu)); +} + +void GXSetTexCopySrc(u16 left, u16 top, u16 wd, u16 ht) { + g_gxState.texCopySrc = {left, top, wd, ht}; + g_gxState.texCopySrcRenderSpace = false; +} + +void GXSetDispCopyDst(u16 wd, u16 ht) { + g_gxState.dispCopyDstWidth = wd; + g_gxState.dispCopyDstHeight = ht; + GX_WRITE_RAS_REG(0x4d000000u | ((((static_cast(wd) & 0x7fffu) << 1) >> 5) & 0x3ffu)); +} + +void GXSetTexCopyDst(u16 wd, u16 ht, GXTexFmt fmt, GXBool mipmap) { + g_gxState.texCopyFmt = fmt; + g_gxState.texCopyDstWidth = wd; + g_gxState.texCopyDstHeight = ht; + g_gxState.texCopyHalfScale = mipmap != GX_FALSE; +} + +void GXSetDispCopyFrame2Field(u32 mode) { + g_gxState.dispCopyFrame2Field = mode & 3; +} + +void GXSetCopyClamp(GXFBClamp clamp) { + g_gxState.copyClamp = static_cast(static_cast(clamp) & 3); +} + +u32 GXSetDispCopyYScale(f32 vscale) { + const u32 iScale = y_scale_to_integer(vscale); + g_gxState.dispCopyYScale = vscale; + GX_WRITE_RAS_REG(0x4e000000u | iScale); + __gx->bpSent = 0; + return get_num_xfb_lines_internal(static_cast(g_gxState.dispCopySrc.height), iScale); +} + +void GXSetCopyClear(GXColor color, u32 depth) { + // BP 0x4F: clear color R + A + u32 reg0 = 0; + SET_REG_FIELD(0, reg0, 8, 0, color.r); + SET_REG_FIELD(0, reg0, 8, 8, color.a); + SET_REG_FIELD(0, reg0, 8, 24, 0x4F); + GX_WRITE_RAS_REG(reg0); + + // BP 0x50: clear color B + G + u32 reg1 = 0; + SET_REG_FIELD(0, reg1, 8, 0, color.b); + SET_REG_FIELD(0, reg1, 8, 8, color.g); + SET_REG_FIELD(0, reg1, 8, 24, 0x50); + GX_WRITE_RAS_REG(reg1); + + // BP 0x51: clear Z (24-bit) + u32 reg2 = 0; + SET_REG_FIELD(0, reg2, 24, 0, depth); + SET_REG_FIELD(0, reg2, 8, 24, 0x51); + GX_WRITE_RAS_REG(reg2); + __gx->bpSent = 1; +} + +void GXSetCopyFilter(GXBool aa, u8 sample_pattern[12][2], GXBool vf, u8 vfilter[7]) { + g_gxState.copyFilterAa = aa; + g_gxState.copyFilterVf = vf; + if (sample_pattern) { + for (size_t i = 0; i < g_gxState.copyFilterSamplePattern.size(); ++i) { + g_gxState.copyFilterSamplePattern[i][0] = sample_pattern[i][0]; + g_gxState.copyFilterSamplePattern[i][1] = sample_pattern[i][1]; + } + } + if (vfilter) { + for (size_t i = 0; i < g_gxState.copyFilterVFilter.size(); ++i) { + g_gxState.copyFilterVFilter[i] = vfilter[i]; + } + } + + if (!aa) { + for (auto& sample : g_gxState.copyFilterSamplePattern) { + sample = {6, 6}; + } + } + if (!vf) { + g_gxState.copyFilterVFilter = {0, 0, 21, 22, 21, 0, 0}; + } + + GX_WRITE_RAS_REG(pack_copy_filter_samples(0x01, g_gxState.copyFilterSamplePattern, 0)); + GX_WRITE_RAS_REG(pack_copy_filter_samples(0x02, g_gxState.copyFilterSamplePattern, 6)); + GX_WRITE_RAS_REG(pack_copy_filter_samples(0x03, g_gxState.copyFilterSamplePattern, 12)); + GX_WRITE_RAS_REG(pack_copy_filter_samples(0x04, g_gxState.copyFilterSamplePattern, 18)); + GX_WRITE_RAS_REG(pack_copy_filter0(g_gxState.copyFilterVFilter)); + GX_WRITE_RAS_REG(pack_copy_filter1(g_gxState.copyFilterVFilter)); + __gx->bpSent = 0; +} + +void GXSetDispCopyGamma(GXGamma gamma) { + g_gxState.dispCopyGamma = static_cast(static_cast(gamma) & 3u); + g_gxState.bpRegCache[0x52] = (g_gxState.bpRegCache[0x52] & ~(3u << 7)) | + ((static_cast(g_gxState.dispCopyGamma) & 3u) << 7); +} + +void GXCopyDisp(void* dest, GXBool clear) { + (void)dest; + // Finish queued commands before this copy reads live EFB state. + if (aurora::gx::fifo::get_buffer_size() != 0) { + aurora::gx::fifo::drain(); + } + const auto rect = aurora::gx::map_logical_scissor(g_gxState.dispCopySrc); + const auto logicalDstWidth = + std::max(g_gxState.dispCopyDstWidth != 0 ? g_gxState.dispCopyDstWidth : static_cast(g_gxState.dispCopySrc.width), 1); + const auto logicalDstHeight = + std::max(g_gxState.dispCopyDstHeight != 0 ? g_gxState.dispCopyDstHeight : static_cast(g_gxState.dispCopySrc.height), 1); + const auto [dstWidth, dstHeight] = scale_copy_dst(logicalDstWidth, logicalDstHeight); + + if (!g_gxState.displayCopyTexture || g_gxState.displayCopyWidth != dstWidth || + g_gxState.displayCopyHeight != dstHeight) { + g_gxState.displayCopyTexture = aurora::gfx::new_render_texture(dstWidth, dstHeight, GX_TF_RGBA8, "Display Copy"); + g_gxState.displayCopyWidth = dstWidth; + g_gxState.displayCopyHeight = dstHeight; + } + + const auto clearState = get_copy_clear_state(clear); + auto copyFilter = combined_copy_filter_coefficients(g_gxState.copyFilterVFilter); + if (aurora::g_config.disableCopyFilter) { + copyFilter = {0, copyFilter[0] + copyFilter[1] + copyFilter[2], 0}; + } + aurora::gfx::resolve_pass(g_gxState.displayCopyTexture, rect, clearState.clearColor, clearState.clearAlpha, + clearState.clearDepth, clearState.clearColorValue, aurora::gx::clear_depth_value(), + GX_TF_RGBA8, nullptr, false, ©Filter, false, + static_cast(rect.height) / std::max(g_gxState.dispCopySrc.height, 1.0f), + (g_gxState.copyClamp & GX_CLAMP_TOP) != 0, + (g_gxState.copyClamp & GX_CLAMP_BOTTOM) != 0); + aurora::gx::set_display_copy_present_source(); +} + +void GXCopyTex(void* dest, GXBool clear) { + // Texture copies must see all earlier draws and state changes. + if (aurora::gx::fifo::get_buffer_size() != 0) { + aurora::gx::fifo::drain(); + } + const auto sourceRect = map_texture_copy_source(g_gxState.texCopySrc, g_gxState.texCopySrcRenderSpace); + const auto rect = sourceRect.clearRect; + // Keep guest dimensions for cache identity while preserving scaled GPU detail. + const auto logicalDstWidth = std::max(g_gxState.texCopyDstWidth, 1); + const auto logicalDstHeight = std::max(g_gxState.texCopyDstHeight, 1); + const auto [scaledDstWidth, scaledDstHeight] = scale_copy_dst(logicalDstWidth, logicalDstHeight); + const auto texCopyFmt = g_gxState.texCopyFmt; + const bool sourceHasAlpha = aurora::gx::render_target_has_alpha(g_gxState.pixelFmt); + const bool forceOpaqueAlpha = !sourceHasAlpha && !aurora::gx::is_depth_format(texCopyFmt); + const auto resolveFmt = texCopyFmt; + + const aurora::gx::GXState::CopyTextureKey key{ + .dest = dest, + .width = logicalDstWidth, + .height = logicalDstHeight, + .format = texCopyFmt, + }; + // Keep one live copy per destination and retire stale GPU textures. + for (auto cacheIt = g_gxState.copyTextureCache.begin(); cacheIt != g_gxState.copyTextureCache.end();) { + if (cacheIt->first.dest == dest && !(cacheIt->first == key)) { + g_gxState.copyTextureCache.erase(cacheIt++); + } else { + ++cacheIt; + } + } + auto it = g_gxState.copyTextureCache.find(key); + if (it == g_gxState.copyTextureCache.end()) { + auto handle = acquire_copy_texture(key, scaledDstWidth, scaledDstHeight, texCopyFmt); + it = g_gxState.copyTextureCache.emplace(key, aurora::gx::GXState::CopyTextureRef{.handle = handle, .revision = 0}).first; + } + auto& handle = it->second; + const u32 currentFrame = aurora::gfx::current_frame(); + const bool sampledThisFrame = handle.sampledThisFrame && handle.lastSampledFrame == currentFrame; + const bool scaledSizeChanged = !handle.handle || handle.handle->size.width != scaledDstWidth || + handle.handle->size.height != scaledDstHeight; + auto clearState = get_copy_clear_state(clear); + if (sampledThisFrame || scaledSizeChanged) { + const u32 revision = handle.revision; + const u32 lastProducedFrame = handle.lastProducedFrame; + handle = aurora::gx::GXState::CopyTextureRef{ + .handle = acquire_copy_texture(key, scaledDstWidth, scaledDstHeight, texCopyFmt), + .revision = revision, + .lastProducedFrame = lastProducedFrame, + }; + } + + const bool alphaUpdate = g_gxState.alphaUpdate && aurora::gx::render_target_has_alpha(g_gxState.pixelFmt); + if (alphaUpdate && g_gxState.dstAlpha != UINT32_MAX) { + if (!clear) { + // TODO: Confirm how this copy should handle alpha without changing the EFB. + } + // Clear alpha with a pipeline that matches the pass sample count. + aurora::gfx::push_draw_command(aurora::gfx::clear::DrawData{ + .pipeline = aurora::gfx::pipeline_ref(aurora::gfx::clear::PipelineConfig{ + .msaaSamples = aurora::gfx::get_sample_count(), + .clearColor = false, + .clearAlpha = true, + .clearDepth = false, + }), + .color = wgpu::Color{0.f, 0.f, 0.f, g_gxState.dstAlpha / 255.f}, + }); + } + if (aurora::gx::render_target_has_alpha(g_gxState.pixelFmt)) { + clearState.clearAlpha = clear && alphaUpdate; + } + const auto copyFilter = combined_copy_filter_coefficients(g_gxState.copyFilterVFilter); + // Skip only recurring color copies so one-shot copies are never lost. + const bool producedConsecutively = handle.revision != 0 && currentFrame - handle.lastProducedFrame <= 1; + const bool persistentCopy = !aurora::gx::is_depth_format(texCopyFmt) && !producedConsecutively; + aurora::gfx::resolve_pass(handle.handle, rect, clearState.clearColor, clearState.clearAlpha, clearState.clearDepth, + clearState.clearColorValue, aurora::gx::clear_depth_value(), resolveFmt, + &sourceRect.sampleRect, g_gxState.texCopyHalfScale, ©Filter, forceOpaqueAlpha, + sourceRect.sampleRect.w() / std::max(g_gxState.texCopySrc.height, 1.0f), + (g_gxState.copyClamp & GX_CLAMP_TOP) != 0, + (g_gxState.copyClamp & GX_CLAMP_BOTTOM) != 0, persistentCopy); + ++handle.revision; + handle.lastProducedFrame = currentFrame; + handle.width = logicalDstWidth; + handle.height = logicalDstHeight; + handle.format = texCopyFmt; + handle.dataSize = GXGetTexBufferSize(static_cast(logicalDstWidth), static_cast(logicalDstHeight), texCopyFmt, GX_FALSE, 0); + aurora::gx::notify_copy_texture_created(); + g_gxState.copyTextures[dest] = handle; + // Keep the GPU copy and download it only if guest code reads the destination. + aurora::gfx::efb_ram::schedule(dest, logicalDstWidth, logicalDstHeight, texCopyFmt, handle.handle); +} + +void GXClearBoundingBox() { + g_gxState.boundingBox = {1023, 0, 1023, 0}; + GX_WRITE_RAS_REG(0x550003FFu); + GX_WRITE_RAS_REG(0x560003FFu); + __gx->bpSent = 0; +} + +void GXReadBoundingBox(u16* left, u16* right, u16* top, u16* bottom) { + if (left) { + *left = g_gxState.boundingBox[0]; + } + if (right) { + *right = g_gxState.boundingBox[1]; + } + if (top) { + *top = g_gxState.boundingBox[2]; + } + if (bottom) { + *bottom = g_gxState.boundingBox[3]; + } +} + +u16 GXGetNumXfbLines(u16 efbHeight, f32 yScale) { + return get_num_xfb_lines_internal(efbHeight, y_scale_to_integer(yScale)); +} + +f32 GXGetYScaleFactor(u16 efbHeight, u16 xfbHeight) { + CHECK(efbHeight != 0, "GXGetYScaleFactor requires non-zero EFB height"); + CHECK(xfbHeight != 0 && xfbHeight <= 1024, "GXGetYScaleFactor requires 1..1024 XFB lines"); + + u32 targetHeight = xfbHeight; + f32 yScale = static_cast(targetHeight) / static_cast(efbHeight); + u16 realHeight = GXGetNumXfbLines(efbHeight, yScale); + + while (realHeight > xfbHeight && targetHeight > 1) { + --targetHeight; + yScale = static_cast(targetHeight) / static_cast(efbHeight); + realHeight = GXGetNumXfbLines(efbHeight, yScale); + } + + f32 resultScale = yScale; + while (realHeight < xfbHeight && targetHeight < 1024) { + resultScale = yScale; + ++targetHeight; + yScale = static_cast(targetHeight) / static_cast(efbHeight); + realHeight = GXGetNumXfbLines(efbHeight, yScale); + } + + return resultScale; +} + +} diff --git a/aurora-main/lib/dolphin/gx/GXGeometry.cpp b/aurora-main/lib/dolphin/gx/GXGeometry.cpp new file mode 100644 index 0000000..2e54e10 --- /dev/null +++ b/aurora-main/lib/dolphin/gx/GXGeometry.cpp @@ -0,0 +1,455 @@ +#include "gx.hpp" +#include "__gx.h" +#include "dolphin/gd/GDGeometry.h" +#include "../../gx/fifo.hpp" + +static inline void SETVCDATTR(GXAttr attr, GXAttrType type) { + switch (attr) { + case GX_VA_PNMTXIDX: + SET_REG_FIELD(0, __gx->vcdLo, 1, 0, type); + break; + case GX_VA_TEX0MTXIDX: + SET_REG_FIELD(0, __gx->vcdLo, 1, 1, type); + break; + case GX_VA_TEX1MTXIDX: + SET_REG_FIELD(0, __gx->vcdLo, 1, 2, type); + break; + case GX_VA_TEX2MTXIDX: + SET_REG_FIELD(0, __gx->vcdLo, 1, 3, type); + break; + case GX_VA_TEX3MTXIDX: + SET_REG_FIELD(0, __gx->vcdLo, 1, 4, type); + break; + case GX_VA_TEX4MTXIDX: + SET_REG_FIELD(0, __gx->vcdLo, 1, 5, type); + break; + case GX_VA_TEX5MTXIDX: + SET_REG_FIELD(0, __gx->vcdLo, 1, 6, type); + break; + case GX_VA_TEX6MTXIDX: + SET_REG_FIELD(0, __gx->vcdLo, 1, 7, type); + break; + case GX_VA_TEX7MTXIDX: + SET_REG_FIELD(0, __gx->vcdLo, 1, 8, type); + break; + case GX_VA_POS: + SET_REG_FIELD(0, __gx->vcdLo, 2, 9, type); + break; + case GX_VA_NRM: + __gx->hasNrms = (type != 0); + if (type != GX_NONE) { + __gx->nrmType = type; + } + break; + case GX_VA_NBT: + __gx->hasBiNrms = (type != 0); + if (type != GX_NONE) { + __gx->nrmType = type; + } + break; + case GX_VA_CLR0: + SET_REG_FIELD(0, __gx->vcdLo, 2, 13, type); + break; + case GX_VA_CLR1: + SET_REG_FIELD(0, __gx->vcdLo, 2, 15, type); + break; + case GX_VA_TEX0: + SET_REG_FIELD(0, __gx->vcdHi, 2, 0, type); + break; + case GX_VA_TEX1: + SET_REG_FIELD(0, __gx->vcdHi, 2, 2, type); + break; + case GX_VA_TEX2: + SET_REG_FIELD(0, __gx->vcdHi, 2, 4, type); + break; + case GX_VA_TEX3: + SET_REG_FIELD(0, __gx->vcdHi, 2, 6, type); + break; + case GX_VA_TEX4: + SET_REG_FIELD(0, __gx->vcdHi, 2, 8, type); + break; + case GX_VA_TEX5: + SET_REG_FIELD(0, __gx->vcdHi, 2, 10, type); + break; + case GX_VA_TEX6: + SET_REG_FIELD(0, __gx->vcdHi, 2, 12, type); + break; + case GX_VA_TEX7: + SET_REG_FIELD(0, __gx->vcdHi, 2, 14, type); + break; + default: + break; + } +} + +static inline void SETVAT(u32* va, u32* vb, u32* vc, GXAttr attr, GXCompCnt cnt, GXCompType type, u8 frac) { + switch (attr) { + case GX_VA_POS: + SET_REG_FIELD(0, *va, 1, 0, cnt); + SET_REG_FIELD(0, *va, 3, 1, type); + SET_REG_FIELD(0, *va, 5, 4, frac); + break; + case GX_VA_NRM: + case GX_VA_NBT: + SET_REG_FIELD(0, *va, 3, 10, type); + if (cnt == GX_NRM_NBT3) { + SET_REG_FIELD(0, *va, 1, 9, 1); + SET_REG_FIELD(0, *va, 1, 31, 1); + } else { + SET_REG_FIELD(0, *va, 1, 9, cnt); + SET_REG_FIELD(0, *va, 1, 31, 0); + } + break; + case GX_VA_CLR0: + SET_REG_FIELD(0, *va, 1, 13, cnt); + SET_REG_FIELD(0, *va, 3, 14, type); + break; + case GX_VA_CLR1: + SET_REG_FIELD(0, *va, 1, 17, cnt); + SET_REG_FIELD(0, *va, 3, 18, type); + break; + case GX_VA_TEX0: + SET_REG_FIELD(0, *va, 1, 21, cnt); + SET_REG_FIELD(0, *va, 3, 22, type); + SET_REG_FIELD(0, *va, 5, 25, frac); + break; + case GX_VA_TEX1: + SET_REG_FIELD(0, *vb, 1, 0, cnt); + SET_REG_FIELD(0, *vb, 3, 1, type); + SET_REG_FIELD(0, *vb, 5, 4, frac); + break; + case GX_VA_TEX2: + SET_REG_FIELD(0, *vb, 1, 9, cnt); + SET_REG_FIELD(0, *vb, 3, 10, type); + SET_REG_FIELD(0, *vb, 5, 13, frac); + break; + case GX_VA_TEX3: + SET_REG_FIELD(0, *vb, 1, 18, cnt); + SET_REG_FIELD(0, *vb, 3, 19, type); + SET_REG_FIELD(0, *vb, 5, 22, frac); + break; + case GX_VA_TEX4: + SET_REG_FIELD(0, *vb, 1, 27, cnt); + SET_REG_FIELD(0, *vb, 3, 28, type); + SET_REG_FIELD(0, *vc, 5, 0, frac); + break; + case GX_VA_TEX5: + SET_REG_FIELD(0, *vc, 1, 5, cnt); + SET_REG_FIELD(0, *vc, 3, 6, type); + SET_REG_FIELD(0, *vc, 5, 9, frac); + break; + case GX_VA_TEX6: + SET_REG_FIELD(0, *vc, 1, 14, cnt); + SET_REG_FIELD(0, *vc, 3, 15, type); + SET_REG_FIELD(0, *vc, 5, 18, frac); + break; + case GX_VA_TEX7: + SET_REG_FIELD(0, *vc, 1, 23, cnt); + SET_REG_FIELD(0, *vc, 3, 24, type); + SET_REG_FIELD(0, *vc, 5, 27, frac); + break; + default: + break; + } +} + +static inline bool AuroraVtxDescDiffers(GXAttr attr, GXAttrType type) { + return attr >= GX_VA_PNMTXIDX && attr < GX_VA_MAX_ATTR && g_gxState.vtxDesc[attr] != type; +} + +static inline bool AuroraVtxAttrFmtDiffers(GXVtxFmt vtxfmt, GXAttr attr, GXCompCnt cnt, GXCompType type, u8 frac) { + if (vtxfmt < GX_VTXFMT0 || vtxfmt >= GX_MAX_VTXFMT || attr < GX_VA_POS || attr >= GX_VA_MAX_ATTR) { + return true; + } + const auto& fmt = g_gxState.vtxFmts[vtxfmt].attrs[attr]; + return fmt.cnt != cnt || fmt.type != type || fmt.frac != frac; +} + +extern "C" { + +void GXSetVtxDesc(GXAttr attr, GXAttrType type) { + const u32 oldVcdLo = __gx->vcdLo; + const u32 oldVcdHi = __gx->vcdHi; + const u8 oldHasNrms = __gx->hasNrms; + const u8 oldHasBiNrms = __gx->hasBiNrms; + const u32 oldNrmType = __gx->nrmType; + const bool auroraStateChanged = AuroraVtxDescDiffers(attr, type); + + SETVCDATTR(attr, type); + if (__gx->hasNrms || __gx->hasBiNrms) { + SET_REG_FIELD(0, __gx->vcdLo, 2, 11, __gx->nrmType); + } else { + SET_REG_FIELD(0, __gx->vcdLo, 2, 11, 0); + } + if (aurora::gx::fifo::in_display_list() || auroraStateChanged || __gx->vcdLo != oldVcdLo || + __gx->vcdHi != oldVcdHi || __gx->hasNrms != oldHasNrms || __gx->hasBiNrms != oldHasBiNrms || + __gx->nrmType != oldNrmType) { + __gx->dirtyState |= 8; + } +} + +void GXSetSourceVtxDesc(GXAttr attr, GXAttrType type) { + if (attr >= GX_VA_PNMTXIDX && attr < GX_VA_MAX_ATTR) { + g_gxState.sourceVtxDesc[attr] = type; + } +} + +void GXSetVtxDescv(GXVtxDescList* list) { + const u32 oldVcdLo = __gx->vcdLo; + const u32 oldVcdHi = __gx->vcdHi; + const u8 oldHasNrms = __gx->hasNrms; + const u8 oldHasBiNrms = __gx->hasBiNrms; + const u32 oldNrmType = __gx->nrmType; + bool auroraStateChanged = false; + + while (list->attr != GX_VA_NULL) { + auroraStateChanged = auroraStateChanged || AuroraVtxDescDiffers(list->attr, list->type); + SETVCDATTR(list->attr, list->type); + ++list; + } + if (__gx->hasNrms || __gx->hasBiNrms) { + SET_REG_FIELD(0, __gx->vcdLo, 2, 11, __gx->nrmType); + } else { + SET_REG_FIELD(0, __gx->vcdLo, 2, 11, 0); + } + if (aurora::gx::fifo::in_display_list() || auroraStateChanged || __gx->vcdLo != oldVcdLo || + __gx->vcdHi != oldVcdHi || __gx->hasNrms != oldHasNrms || __gx->hasBiNrms != oldHasBiNrms || + __gx->nrmType != oldNrmType) { + __gx->dirtyState |= 8; + } +} + +void GXClearVtxDesc() { + __gx->vcdLo = 0; + SET_REG_FIELD(0, __gx->vcdLo, 2, 9, 1); // GX_VA_POS = GX_DIRECT + __gx->vcdHi = 0; + __gx->hasNrms = 0; + __gx->hasBiNrms = 0; + __gx->dirtyState |= 8; +} + +void GXSetVtxAttrFmt(GXVtxFmt vtxfmt, GXAttr attr, GXCompCnt cnt, GXCompType type, u8 frac) { + CHECK(vtxfmt >= GX_VTXFMT0 && vtxfmt < GX_MAX_VTXFMT, "invalid vtxfmt {}", underlying(vtxfmt)); + CHECK(attr >= GX_VA_POS && attr < GX_VA_MAX_ATTR, "invalid attr {}", underlying(attr)); + + u32* va = &__gx->vatA[vtxfmt]; + u32* vb = &__gx->vatB[vtxfmt]; + u32* vc = &__gx->vatC[vtxfmt]; + const u32 oldVa = *va; + const u32 oldVb = *vb; + const u32 oldVc = *vc; + const bool auroraStateChanged = AuroraVtxAttrFmtDiffers(vtxfmt, attr, cnt, type, frac); + SETVAT(va, vb, vc, attr, cnt, type, frac); + if (aurora::gx::fifo::in_display_list() || auroraStateChanged || *va != oldVa || *vb != oldVb || + *vc != oldVc) { + __gx->dirtyState |= 0x10; + __gx->dirtyVAT |= static_cast(1 << vtxfmt); + } +} + +void GXSetVtxAttrFmtv(GXVtxFmt vtxfmt, const GXVtxAttrFmtList* list) { + CHECK(vtxfmt >= GX_VTXFMT0 && vtxfmt < GX_MAX_VTXFMT, "invalid vtxfmt {}", underlying(vtxfmt)); + CHECK(list != nullptr, "null vtx attr fmt list"); + + u32* va = &__gx->vatA[vtxfmt]; + u32* vb = &__gx->vatB[vtxfmt]; + u32* vc = &__gx->vatC[vtxfmt]; + const u32 oldVa = *va; + const u32 oldVb = *vb; + const u32 oldVc = *vc; + bool auroraStateChanged = false; + while (list->attr != GX_VA_NULL) { + CHECK(list->attr >= GX_VA_POS && list->attr < GX_VA_MAX_ATTR, "invalid attr {}", underlying(list->attr)); + auroraStateChanged = + auroraStateChanged || AuroraVtxAttrFmtDiffers(vtxfmt, list->attr, list->cnt, list->type, list->frac); + SETVAT(va, vb, vc, list->attr, list->cnt, list->type, list->frac); + ++list; + } + + if (aurora::gx::fifo::in_display_list() || auroraStateChanged || *va != oldVa || *vb != oldVb || + *vc != oldVc) { + __gx->dirtyState |= 0x10; + __gx->dirtyVAT |= static_cast(1 << vtxfmt); + } +} + +void GXSetArray(GXAttr attr, const void* data, u32 size, u8 stride, bool le) { + GXAttr cpAttr = static_cast(attr); + if (attr == GX_VA_NBT) { + cpAttr = GX_VA_NRM; + } + u32 cpIdx = cpAttr - GX_VA_POS; + + assert((cpIdx & ~0xF) == 0); + + // Write array base + GX_WRITE_AURORA(GX_LOAD_AURORA_ARRAYBASE | cpIdx); + GX_WRITE_U64(reinterpret_cast(data)); + GX_WRITE_U32(size); + GX_WRITE_U8(le ? 1 : 0); + + // Write array stride + GX_WRITE_CP_REG(CP_REG_ARRAYSTRIDE_ID | cpIdx, stride); +} + +void GXSetTexCoordGen2(GXTexCoordID dst, GXTexGenType type, GXTexGenSrc src, u32 mtx, GXBool normalize, u32 postMtx) { + CHECK(dst >= GX_TEXCOORD0 && dst <= GX_TEXCOORD7, "invalid tex coord {}", underlying(dst)); + + u32 reg = 0; + u32 row = 5; + u32 form = 0; + + switch (src) { + case GX_TG_POS: + row = 0; + form = 1; + break; + case GX_TG_NRM: + row = 1; + form = 1; + break; + case GX_TG_BINRM: + row = 3; + form = 1; + break; + case GX_TG_TANGENT: + row = 4; + form = 1; + break; + case GX_TG_COLOR0: + row = 2; + break; + case GX_TG_COLOR1: + row = 2; + break; + case GX_TG_TEX0: + row = 5; + break; + case GX_TG_TEX1: + row = 6; + break; + case GX_TG_TEX2: + row = 7; + break; + case GX_TG_TEX3: + row = 8; + break; + case GX_TG_TEX4: + row = 9; + break; + case GX_TG_TEX5: + row = 10; + break; + case GX_TG_TEX6: + row = 11; + break; + case GX_TG_TEX7: + row = 12; + break; + default: + break; + } + + switch (type) { + case GX_TG_MTX2x4: + SET_REG_FIELD(0, reg, 1, 1, 0); + SET_REG_FIELD(0, reg, 1, 2, form); + SET_REG_FIELD(0, reg, 3, 4, 0); + SET_REG_FIELD(0, reg, 5, 7, row); + break; + case GX_TG_MTX3x4: + SET_REG_FIELD(0, reg, 1, 1, 1); + SET_REG_FIELD(0, reg, 1, 2, form); + SET_REG_FIELD(0, reg, 3, 4, 0); + SET_REG_FIELD(0, reg, 5, 7, row); + break; + case GX_TG_SRTG: + SET_REG_FIELD(0, reg, 1, 1, 0); + SET_REG_FIELD(0, reg, 1, 2, form); + if (src == GX_TG_COLOR0) { + SET_REG_FIELD(0, reg, 3, 4, 2); + } else { + SET_REG_FIELD(0, reg, 3, 4, 3); + } + SET_REG_FIELD(0, reg, 5, 7, 2); + break; + default: + // Bump mapping types + if (type >= 2 && type <= 9) { + SET_REG_FIELD(0, reg, 1, 1, 0); + SET_REG_FIELD(0, reg, 1, 2, form); + SET_REG_FIELD(0, reg, 3, 4, 1); + SET_REG_FIELD(0, reg, 5, 7, row); + SET_REG_FIELD(0, reg, 3, 12, src - 12); + SET_REG_FIELD(0, reg, 3, 15, type - 2); + } + break; + } + + GX_WRITE_XF_REG(dst + 0x40, reg); + + // Post-transform register + u32 postReg = 0; + SET_REG_FIELD(0, postReg, 6, 0, postMtx - 64); + SET_REG_FIELD(0, postReg, 1, 8, normalize); + GX_WRITE_XF_REG(dst + 0x50, postReg); + + // Update matrix index shadows + switch (dst) { + case GX_TEXCOORD0: + SET_REG_FIELD(0, __gx->matIdxA, 6, 6, mtx); + break; + case GX_TEXCOORD1: + SET_REG_FIELD(0, __gx->matIdxA, 6, 12, mtx); + break; + case GX_TEXCOORD2: + SET_REG_FIELD(0, __gx->matIdxA, 6, 18, mtx); + break; + case GX_TEXCOORD3: + SET_REG_FIELD(0, __gx->matIdxA, 6, 24, mtx); + break; + case GX_TEXCOORD4: + SET_REG_FIELD(0, __gx->matIdxB, 6, 0, mtx); + break; + case GX_TEXCOORD5: + SET_REG_FIELD(0, __gx->matIdxB, 6, 6, mtx); + break; + case GX_TEXCOORD6: + SET_REG_FIELD(0, __gx->matIdxB, 6, 12, mtx); + break; + default: + SET_REG_FIELD(0, __gx->matIdxB, 6, 18, mtx); + break; + } + __GXSetMatrixIndex(static_cast(dst + 1)); +} + +void GXSetNumTexGens(u8 num) { + SET_REG_FIELD(0, __gx->genMode, 4, 0, num); + GX_WRITE_XF_REG(0x3F, num); + __gx->dirtyState |= 4; +} + +void GXInvalidateVtxCache() { GX_WRITE_U8(0x48); } + +void GXSetLineWidth(u8 width, GXTexOffset offs) { + SET_REG_FIELD(0, __gx->lpSize, 8, 0, width); + SET_REG_FIELD(0, __gx->lpSize, 3, 16, offs); + GX_WRITE_RAS_REG(__gx->lpSize); + __gx->bpSent = 1; +} + +void GXSetPointSize(u8 pointSize, GXTexOffset texOffsets) { + SET_REG_FIELD(0, __gx->lpSize, 8, 8, pointSize); + SET_REG_FIELD(0, __gx->lpSize, 3, 19, texOffsets); + GX_WRITE_RAS_REG(__gx->lpSize); + __gx->bpSent = 1; +} + +void GXEnableTexOffsets(GXTexCoordID coord, GXBool line_enable, GXBool point_enable) { + SET_REG_FIELD(0, __gx->suTs0[coord], 1, 18, line_enable); + SET_REG_FIELD(0, __gx->suTs0[coord], 1, 19, point_enable); + GX_WRITE_RAS_REG(__gx->suTs0[coord]); + __gx->bpSent = 1; +} +} diff --git a/aurora-main/lib/dolphin/gx/GXGet.cpp b/aurora-main/lib/dolphin/gx/GXGet.cpp new file mode 100644 index 0000000..ccefbbc --- /dev/null +++ b/aurora-main/lib/dolphin/gx/GXGet.cpp @@ -0,0 +1,337 @@ +#include "gx.hpp" +#include "__gx.h" + +#include "../../gfx/common.hpp" +#include "../../gfx/texture.hpp" + +extern "C" { + +static inline u8 GetFracForNrm(GXCompType type) { + switch (type) { + case GX_S8: + return 6; + case GX_S16: + return 14; + default: + return 0; + } +} + +void GXGetVtxDesc(GXAttr attr, GXAttrType* type) { + CHECK(type != nullptr, "null vtx desc output"); + + u32 cpType = 0; + switch (attr) { + case GX_VA_PNMTXIDX: + cpType = GET_REG_FIELD(__gx->vcdLo, 1, 0); + break; + case GX_VA_TEX0MTXIDX: + cpType = GET_REG_FIELD(__gx->vcdLo, 1, 1); + break; + case GX_VA_TEX1MTXIDX: + cpType = GET_REG_FIELD(__gx->vcdLo, 1, 2); + break; + case GX_VA_TEX2MTXIDX: + cpType = GET_REG_FIELD(__gx->vcdLo, 1, 3); + break; + case GX_VA_TEX3MTXIDX: + cpType = GET_REG_FIELD(__gx->vcdLo, 1, 4); + break; + case GX_VA_TEX4MTXIDX: + cpType = GET_REG_FIELD(__gx->vcdLo, 1, 5); + break; + case GX_VA_TEX5MTXIDX: + cpType = GET_REG_FIELD(__gx->vcdLo, 1, 6); + break; + case GX_VA_TEX6MTXIDX: + cpType = GET_REG_FIELD(__gx->vcdLo, 1, 7); + break; + case GX_VA_TEX7MTXIDX: + cpType = GET_REG_FIELD(__gx->vcdLo, 1, 8); + break; + case GX_VA_POS: + cpType = GET_REG_FIELD(__gx->vcdLo, 2, 9); + break; + case GX_VA_NRM: + cpType = __gx->hasNrms ? GET_REG_FIELD(__gx->vcdLo, 2, 11) : GX_NONE; + break; + case GX_VA_NBT: + cpType = __gx->hasBiNrms ? GET_REG_FIELD(__gx->vcdLo, 2, 11) : GX_NONE; + break; + case GX_VA_CLR0: + cpType = GET_REG_FIELD(__gx->vcdLo, 2, 13); + break; + case GX_VA_CLR1: + cpType = GET_REG_FIELD(__gx->vcdLo, 2, 15); + break; + case GX_VA_TEX0: + cpType = GET_REG_FIELD(__gx->vcdHi, 2, 0); + break; + case GX_VA_TEX1: + cpType = GET_REG_FIELD(__gx->vcdHi, 2, 2); + break; + case GX_VA_TEX2: + cpType = GET_REG_FIELD(__gx->vcdHi, 2, 4); + break; + case GX_VA_TEX3: + cpType = GET_REG_FIELD(__gx->vcdHi, 2, 6); + break; + case GX_VA_TEX4: + cpType = GET_REG_FIELD(__gx->vcdHi, 2, 8); + break; + case GX_VA_TEX5: + cpType = GET_REG_FIELD(__gx->vcdHi, 2, 10); + break; + case GX_VA_TEX6: + cpType = GET_REG_FIELD(__gx->vcdHi, 2, 12); + break; + case GX_VA_TEX7: + cpType = GET_REG_FIELD(__gx->vcdHi, 2, 14); + break; + default: + break; + } + + *type = static_cast(cpType); +} + +void GXGetVtxDescv(GXVtxDescList* vcd) { + CHECK(vcd != nullptr, "null vtx desc list"); + + GXVtxDescList* out = vcd; + for (GXAttr attr = GX_VA_PNMTXIDX; attr <= GX_VA_TEX7; attr = static_cast(attr + 1), ++out) { + out->attr = attr; + GXGetVtxDesc(attr, &out->type); + } + out->attr = GX_VA_NBT; + GXGetVtxDesc(GX_VA_NBT, &out->type); + ++out; + out->attr = GX_VA_NULL; +} + +void GXGetVtxAttrFmt(GXVtxFmt idx, GXAttr attr, GXCompCnt* compCnt, GXCompType* compType, u8* shift) { + CHECK(compCnt != nullptr && compType != nullptr && shift != nullptr, "null vtx attr fmt output"); + + const u32 va = __gx->vatA[idx]; + const u32 vb = __gx->vatB[idx]; + const u32 vc = __gx->vatC[idx]; + + switch (attr) { + case GX_VA_POS: + *compCnt = static_cast(GET_REG_FIELD(va, 1, 0)); + *compType = static_cast(GET_REG_FIELD(va, 3, 1)); + *shift = static_cast(GET_REG_FIELD(va, 5, 4)); + return; + case GX_VA_NRM: + case GX_VA_NBT: + *compCnt = static_cast(GET_REG_FIELD(va, 1, 9)); + if (*compCnt == GX_TEX_ST && GET_REG_FIELD(va, 1, 31) != 0) { + *compCnt = GX_NRM_NBT3; + } + *compType = static_cast(GET_REG_FIELD(va, 3, 10)); + *shift = GetFracForNrm(*compType); + return; + case GX_VA_CLR0: + *compCnt = static_cast(GET_REG_FIELD(va, 1, 13)); + *compType = static_cast(GET_REG_FIELD(va, 3, 14)); + *shift = 0; + return; + case GX_VA_CLR1: + *compCnt = static_cast(GET_REG_FIELD(va, 1, 17)); + *compType = static_cast(GET_REG_FIELD(va, 3, 18)); + *shift = 0; + return; + case GX_VA_TEX0: + *compCnt = static_cast(GET_REG_FIELD(va, 1, 21)); + *compType = static_cast(GET_REG_FIELD(va, 3, 22)); + *shift = static_cast(GET_REG_FIELD(va, 5, 25)); + return; + case GX_VA_TEX1: + *compCnt = static_cast(GET_REG_FIELD(vb, 1, 0)); + *compType = static_cast(GET_REG_FIELD(vb, 3, 1)); + *shift = static_cast(GET_REG_FIELD(vb, 5, 4)); + return; + case GX_VA_TEX2: + *compCnt = static_cast(GET_REG_FIELD(vb, 1, 9)); + *compType = static_cast(GET_REG_FIELD(vb, 3, 10)); + *shift = static_cast(GET_REG_FIELD(vb, 5, 13)); + return; + case GX_VA_TEX3: + *compCnt = static_cast(GET_REG_FIELD(vb, 1, 18)); + *compType = static_cast(GET_REG_FIELD(vb, 3, 19)); + *shift = static_cast(GET_REG_FIELD(vb, 5, 22)); + return; + case GX_VA_TEX4: + *compCnt = static_cast(GET_REG_FIELD(vb, 1, 27)); + *compType = static_cast(GET_REG_FIELD(vb, 3, 28)); + *shift = static_cast(GET_REG_FIELD(vc, 5, 0)); + return; + case GX_VA_TEX5: + *compCnt = static_cast(GET_REG_FIELD(vc, 1, 5)); + *compType = static_cast(GET_REG_FIELD(vc, 3, 6)); + *shift = static_cast(GET_REG_FIELD(vc, 5, 9)); + return; + case GX_VA_TEX6: + *compCnt = static_cast(GET_REG_FIELD(vc, 1, 14)); + *compType = static_cast(GET_REG_FIELD(vc, 3, 15)); + *shift = static_cast(GET_REG_FIELD(vc, 5, 18)); + return; + case GX_VA_TEX7: + *compCnt = static_cast(GET_REG_FIELD(vc, 1, 23)); + *compType = static_cast(GET_REG_FIELD(vc, 3, 24)); + *shift = static_cast(GET_REG_FIELD(vc, 5, 27)); + return; + default: + *compCnt = GX_TEX_ST; + *compType = GX_RGB565; + *shift = 0; + return; + } +} + +void GXGetVtxAttrFmtv(GXVtxFmt fmt, GXVtxAttrFmtList* vat) { + CHECK(vat != nullptr, "null vtx attr fmt list"); + + GXVtxAttrFmtList* out = vat; + for (GXAttr attr = GX_VA_POS; attr <= GX_VA_TEX7; attr = static_cast(attr + 1), ++out) { + out->attr = attr; + GXGetVtxAttrFmt(fmt, attr, &out->cnt, &out->type, &out->frac); + } + out->attr = GX_VA_NULL; +} + +void GXGetLineWidth(u8* width, GXTexOffset* texOffsets) { + CHECK(width != nullptr && texOffsets != nullptr, "null line width output"); + + *width = static_cast(GET_REG_FIELD(__gx->lpSize, 8, 0)); + *texOffsets = static_cast(GET_REG_FIELD(__gx->lpSize, 3, 16)); +} + +void GXGetPointSize(u8* pointSize, GXTexOffset* texOffsets) { + CHECK(pointSize != nullptr && texOffsets != nullptr, "null point size output"); + + *pointSize = static_cast(GET_REG_FIELD(__gx->lpSize, 8, 8)); + *texOffsets = static_cast(GET_REG_FIELD(__gx->lpSize, 3, 19)); +} + +void GXGetViewportv(f32 *vp) { + CHECK(vp != nullptr, "null viewport output"); + + vp[0] = __gx->vpLeft; + vp[1] = __gx->vpTop; + vp[2] = __gx->vpWd; + vp[3] = __gx->vpHt; + vp[4] = __gx->vpNearz; + vp[5] = __gx->vpFarz; +} + +void GXGetProjectionv(f32* p) { + p[0] = __gx->projType == GX_PERSPECTIVE ? 0.0f : 1.0f; + p[1] = __gx->projMtx[0]; + p[2] = __gx->projMtx[1]; + p[3] = __gx->projMtx[2]; + p[4] = __gx->projMtx[3]; + p[5] = __gx->projMtx[4]; + p[6] = __gx->projMtx[5]; +} + +void GXGetScissor(u32* left, u32* top, u32* wd, u32* ht) { + CHECK(left != nullptr && top != nullptr && wd != nullptr && ht != nullptr, "null scissor output"); + + const u32 tp = GET_REG_FIELD(__gx->suScis0, 11, 0); + const u32 lf = GET_REG_FIELD(__gx->suScis0, 11, 12); + const u32 bm = GET_REG_FIELD(__gx->suScis1, 11, 0); + const u32 rt = GET_REG_FIELD(__gx->suScis1, 11, 12); + + *left = lf - 342; + *top = tp - 342; + *wd = rt - lf + 1; + *ht = bm - tp + 1; +} + +void GXGetCullMode(GXCullMode* mode) { + CHECK(mode != nullptr, "null cull mode output"); + + const auto hwMode = static_cast(GET_REG_FIELD(__gx->genMode, 2, 14)); + switch (hwMode) { + case GX_CULL_FRONT: + *mode = GX_CULL_BACK; + break; + case GX_CULL_BACK: + *mode = GX_CULL_FRONT; + break; + default: + *mode = hwMode; + break; + } +} + +void GXGetLightAttnA(GXLightObj* light_, float* a0, float* a1, float* a2) { + auto* light = reinterpret_cast(light_); + *a0 = light->a0; + *a1 = light->a1; + *a2 = light->a2; +} + +void GXGetLightAttnK(GXLightObj* light_, float* k0, float* k1, float* k2) { + auto* light = reinterpret_cast(light_); + *k0 = light->k0; + *k1 = light->k1; + *k2 = light->k2; +} + +void GXGetLightPos(GXLightObj* light_, float* x, float* y, float* z) { + auto* light = reinterpret_cast(light_); + *x = light->px; + *y = light->py; + *z = light->pz; +} + +void GXGetLightDir(GXLightObj* light_, float* nx, float* ny, float* nz) { + auto* light = reinterpret_cast(light_); + *nx = -light->nx; + *ny = -light->ny; + *nz = -light->nz; +} + +void GXGetLightColor(GXLightObj* light_, GXColor* col) { + auto* light = reinterpret_cast(light_); + *col = light->color; +} + +void* GXGetTexObjData(GXTexObj* tex_obj) { + return const_cast(reinterpret_cast(tex_obj)->data); +} + +u16 GXGetTexObjWidth(GXTexObj* tex_obj) { return static_cast(reinterpret_cast(tex_obj)->width()); } + +u16 GXGetTexObjHeight(GXTexObj* tex_obj) { return static_cast(reinterpret_cast(tex_obj)->height()); } + +GXTexFmt GXGetTexObjFmt(GXTexObj* tex_obj) { + return static_cast(reinterpret_cast(tex_obj)->format()); +} + +GXTexWrapMode GXGetTexObjWrapS(GXTexObj* tex_obj) { return reinterpret_cast(tex_obj)->wrap_s(); } + +GXTexWrapMode GXGetTexObjWrapT(GXTexObj* tex_obj) { return reinterpret_cast(tex_obj)->wrap_t(); } + +GXBool GXGetTexObjMipMap(GXTexObj* tex_obj) { return reinterpret_cast(tex_obj)->has_mips(); } + +// TODO GXGetTexObjAll +// TODO GXGetTexObjMinFilt +// TODO GXGetTexObjMagFilt +// TODO GXGetTexObjMinLOD +// TODO GXGetTexObjMaxLOD +// TODO GXGetTexObjLODBias +// TODO GXGetTexObjBiasClamp +// TODO GXGetTexObjEdgeLOD +// TODO GXGetTexObjMaxAniso +// TODO GXGetTexObjLODAll +u32 GXGetTexObjTlut(const GXTexObj* tex_obj) { return reinterpret_cast(tex_obj)->tlut; } +// TODO GXGetTlutObjData +// TODO GXGetTlutObjFmt +// TODO GXGetTlutObjNumEntries +// TODO GXGetTlutObjAll +// TODO GXGetTexRegionAll +// TODO GXGetTlutRegionAll +} diff --git a/aurora-main/lib/dolphin/gx/GXLighting.cpp b/aurora-main/lib/dolphin/gx/GXLighting.cpp new file mode 100644 index 0000000..f7331c6 --- /dev/null +++ b/aurora-main/lib/dolphin/gx/GXLighting.cpp @@ -0,0 +1,291 @@ +#include "gx.hpp" +#include "__gx.h" + +extern "C" { +void GXInitLightAttn(GXLightObj* light_, float a0, float a1, float a2, float k0, float k1, float k2) { + auto* light = reinterpret_cast(light_); + light->a0 = a0; + light->a1 = a1; + light->a2 = a2; + light->k0 = k0; + light->k1 = k1; + light->k2 = k2; +} + +void GXInitLightAttnA(GXLightObj* light_, float a0, float a1, float a2) { + auto* light = reinterpret_cast(light_); + light->a0 = a0; + light->a1 = a1; + light->a2 = a2; +} + +void GXInitLightAttnK(GXLightObj* light_, float k0, float k1, float k2) { + auto* light = reinterpret_cast(light_); + light->k0 = k0; + light->k1 = k1; + light->k2 = k2; +} + +void GXInitLightSpot(GXLightObj* light_, float cutoff, GXSpotFn spotFn) { + if (cutoff <= 0.f || cutoff > 90.f) { + spotFn = GX_SP_OFF; + } + + float cr = std::cos((cutoff * M_PIF) / 180.f); + float a0 = 1.f; + float a1 = 0.f; + float a2 = 0.f; + switch (spotFn) { + default: + break; + case GX_SP_FLAT: + a0 = -1000.f * cr; + a1 = 1000.f; + a2 = 0.f; + break; + case GX_SP_COS: + a0 = -cr / (1.f - cr); + a1 = 1.f / (1.f - cr); + a2 = 0.f; + break; + case GX_SP_COS2: + a0 = 0.f; + a1 = -cr / (1.f - cr); + a2 = 1.f / (1.f - cr); + break; + case GX_SP_SHARP: { + const float d = (1.f - cr) * (1.f - cr); + a0 = cr * (cr - 2.f) / d; + a1 = 2.f / d; + a2 = -1.f / d; + break; + } + case GX_SP_RING1: { + const float d = (1.f - cr) * (1.f - cr); + a0 = -4.f * cr / d; + a1 = 4.f * (1.f + cr) / d; + a2 = -4.f / d; + break; + } + case GX_SP_RING2: { + const float d = (1.f - cr) * (1.f - cr); + a0 = 1.f - 2.f * cr * cr / d; + a1 = 4.f * cr / d; + a2 = -2.f / d; + break; + } + } + + auto* light = reinterpret_cast(light_); + light->a0 = a0; + light->a1 = a1; + light->a2 = a2; +} + +void GXInitLightDistAttn(GXLightObj* light_, float refDistance, float refBrightness, GXDistAttnFn distFunc) { + if (refDistance < 0.f || refBrightness < 0.f || refBrightness >= 1.f) { + distFunc = GX_DA_OFF; + } + float k0 = 1.f; + float k1 = 0.f; + float k2 = 0.f; + switch (distFunc) { + case GX_DA_GENTLE: + k0 = 1.0f; + k1 = (1.0f - refBrightness) / (refBrightness * refDistance); + k2 = 0.0f; + break; + case GX_DA_MEDIUM: + k0 = 1.0f; + k1 = 0.5f * (1.0f - refBrightness) / (refBrightness * refDistance); + k2 = 0.5f * (1.0f - refBrightness) / (refBrightness * refDistance * refDistance); + break; + case GX_DA_STEEP: + k0 = 1.0f; + k1 = 0.0f; + k2 = (1.0f - refBrightness) / (refBrightness * refDistance * refDistance); + break; + case GX_DA_OFF: + k0 = 1.0f; + k1 = 0.0f; + k2 = 0.0f; + break; + } + + auto* light = reinterpret_cast(light_); + light->k0 = k0; + light->k1 = k1; + light->k2 = k2; +} + +void GXInitLightPos(GXLightObj* light_, float x, float y, float z) { + auto* light = reinterpret_cast(light_); + light->px = x; + light->py = y; + light->pz = z; +} + +void GXInitLightColor(GXLightObj* light_, GXColor col) { + auto* light = reinterpret_cast(light_); + light->color = col; +} + +void GXLoadLightObjImm(GXLightObj* light_, GXLightID id) { + u32 idx = std::log2(id); + auto* light = reinterpret_cast(light_); + + // XF bulk write: 16 values at light base address + // Light addresses: 0x600 + idx * 0x10 + u32 addr = 0x600 + idx * 0x10; + u32 reg = addr | (0xF << 16); // 16-1=15 values + + // Convert color to packed u32 for XF + u32 colorPacked = (static_cast(light->color.r) << 24) | (static_cast(light->color.g) << 16) | + (static_cast(light->color.b) << 8) | static_cast(light->color.a); + + GX_WRITE_U8(0x10); + GX_WRITE_U32(reg); + // Padding (3 u32s) + GX_WRITE_U32(0); + GX_WRITE_U32(0); + GX_WRITE_U32(0); + // Color + GX_WRITE_U32(colorPacked); + // Cosine attenuation (a0, a1, a2) + GX_WRITE_F32(light->a0); + GX_WRITE_F32(light->a1); + GX_WRITE_F32(light->a2); + // Distance attenuation (k0, k1, k2) + GX_WRITE_F32(light->k0); + GX_WRITE_F32(light->k1); + GX_WRITE_F32(light->k2); + // Position (px, py, pz) + GX_WRITE_F32(light->px); + GX_WRITE_F32(light->py); + GX_WRITE_F32(light->pz); + // Direction (nx, ny, nz) + GX_WRITE_F32(light->nx); + GX_WRITE_F32(light->ny); + GX_WRITE_F32(light->nz); +} + +void GXSetChanAmbColor(GXChannelID id, GXColor color) { + if (id == GX_COLOR0A0) { + GXSetChanAmbColor(GX_COLOR0, color); + GXSetChanAmbColor(GX_ALPHA0, color); + return; + } else if (id == GX_COLOR1A1) { + GXSetChanAmbColor(GX_COLOR1, color); + GXSetChanAmbColor(GX_ALPHA1, color); + return; + } + CHECK(id >= GX_COLOR0 && id <= GX_ALPHA1, "bad channel {}", static_cast(id)); + + // XF ambient color registers: 0x100A (chan 0), 0x100B (chan 1) + u32 packed = (static_cast(color.r) << 24) | (static_cast(color.g) << 16) | + (static_cast(color.b) << 8) | static_cast(color.a); + if (id == GX_COLOR0 || id == GX_ALPHA0) { + __gx->ambColor[0] = packed; + GX_WRITE_XF_REG(0xA, packed); + } else { + __gx->ambColor[1] = packed; + GX_WRITE_XF_REG(0xB, packed); + } + __gx->bpSent = 0; +} + +void GXSetChanMatColor(GXChannelID id, GXColor color) { + if (id == GX_COLOR0A0) { + GXSetChanMatColor(GX_COLOR0, color); + GXSetChanMatColor(GX_ALPHA0, color); + return; + } else if (id == GX_COLOR1A1) { + GXSetChanMatColor(GX_COLOR1, color); + GXSetChanMatColor(GX_ALPHA1, color); + return; + } + CHECK(id >= GX_COLOR0 && id <= GX_ALPHA1, "bad channel {}", static_cast(id)); + + // XF material color registers: 0x100C (chan 0), 0x100D (chan 1) + u32 packed = (static_cast(color.r) << 24) | (static_cast(color.g) << 16) | + (static_cast(color.b) << 8) | static_cast(color.a); + if (id == GX_COLOR0 || id == GX_ALPHA0) { + __gx->matColor[0] = packed; + GX_WRITE_XF_REG(0xC, packed); + } else { + __gx->matColor[1] = packed; + GX_WRITE_XF_REG(0xD, packed); + } + __gx->bpSent = 0; +} + +void GXSetNumChans(u8 num) { + SET_REG_FIELD(0, __gx->genMode, 3, 4, num); + GX_WRITE_XF_REG(9, num); + __gx->dirtyState |= 4; +} + +void GXInitLightDir(GXLightObj* light_, float nx, float ny, float nz) { + auto* light = reinterpret_cast(light_); + light->nx = -nx; + light->ny = -ny; + light->nz = -nz; +} + +void GXInitSpecularDir(GXLightObj* light_, float nx, float ny, float nz) { + float hx = -nx; + float hy = -ny; + float hz = (-nz + 1.0f); + float mag = ((hx * hx) + (hy * hy) + (hz * hz)); + if (mag != 0.0f) { + mag = 1.0f / sqrtf(mag); + } + + auto* light = reinterpret_cast(light_); + light->px = (nx * GX_LARGE_NUMBER); + light->py = (ny * GX_LARGE_NUMBER); + light->pz = (nz * GX_LARGE_NUMBER); + light->nx = hx * mag; + light->ny = hy * mag; + light->nz = hz * mag; +} + +void GXInitSpecularDirHA(GXLightObj* light_, float nx, float ny, float nz, float hx, float hy, float hz) { + auto* light = reinterpret_cast(light_); + light->px = (nx * GX_LARGE_NUMBER); + light->py = (ny * GX_LARGE_NUMBER); + light->pz = (nz * GX_LARGE_NUMBER); + light->nx = hx; + light->ny = hy; + light->nz = hz; +} + +void GXSetChanCtrl(GXChannelID id, bool lightingEnabled, GXColorSrc ambSrc, GXColorSrc matSrc, u32 lightState, + GXDiffuseFn diffFn, GXAttnFn attnFn) { + if (id == GX_COLOR0A0) { + GXSetChanCtrl(GX_COLOR0, lightingEnabled, ambSrc, matSrc, lightState, diffFn, attnFn); + GXSetChanCtrl(GX_ALPHA0, lightingEnabled, ambSrc, matSrc, lightState, diffFn, attnFn); + return; + } else if (id == GX_COLOR1A1) { + GXSetChanCtrl(GX_COLOR1, lightingEnabled, ambSrc, matSrc, lightState, diffFn, attnFn); + GXSetChanCtrl(GX_ALPHA1, lightingEnabled, ambSrc, matSrc, lightState, diffFn, attnFn); + return; + } + CHECK(id >= GX_COLOR0 && id <= GX_ALPHA1, "bad channel {}", static_cast(id)); + + // Build XF channel control register + u32 reg = 0; + SET_REG_FIELD(0, reg, 1, 0, matSrc); + SET_REG_FIELD(0, reg, 1, 1, lightingEnabled); + SET_REG_FIELD(0, reg, 4, 2, lightState & 0xF); // lights 0-3 + SET_REG_FIELD(0, reg, 1, 6, ambSrc); + SET_REG_FIELD(0, reg, 2, 7, (attnFn == GX_AF_SPEC) ? GX_DF_NONE : diffFn); + SET_REG_FIELD(0, reg, 1, 9, (attnFn != GX_AF_NONE)); // raw XF attn bit 0 + SET_REG_FIELD(0, reg, 1, 10, (attnFn != GX_AF_SPEC)); // raw XF attn bit 1 + SET_REG_FIELD(0, reg, 4, 11, (lightState >> 4) & 0xF); // lights 4-7 + + // XF channel control registers: 0x100E-0x1011 + GX_WRITE_XF_REG(0xE + id, reg); + __gx->bpSent = 0; +} +} diff --git a/aurora-main/lib/dolphin/gx/GXManage.cpp b/aurora-main/lib/dolphin/gx/GXManage.cpp new file mode 100644 index 0000000..80b3b92 --- /dev/null +++ b/aurora-main/lib/dolphin/gx/GXManage.cpp @@ -0,0 +1,502 @@ +#include "gx.hpp" +#include "__gx.h" + +#include "../../gx/fifo.hpp" + +#include + +// Global shadow register instance +static __GXData_struct sGXData; +__GXData_struct* __gx = &sGXData; +static GXFifoObj sFifoObj; + +extern "C" { +static GXDrawDoneCallback DrawDoneCB = nullptr; + +GXFifoObj* GXInit(void* base, u32 size) { + GXRenderModeObj* rmode; + f32 identity_mtx[3][4]; + GXColor clear = {64, 64, 64, 255}; + GXColor black = {0, 0, 0, 0}; + GXColor white = {255, 255, 255, 255}; + u32 i; + + std::memset(&sGXData, 0, sizeof(sGXData)); + __gx = &sGXData; + __gx->inDispList = 0; + __gx->dlSaveContext = 1; + __gx->tcsManEnab = 0; + __gx->vNum = 0; + + // Initialize FIFO subsystem + aurora::gx::fifo::init(); + GXInitFifoBase(&sFifoObj, base, size); + GXSetCPUFifo(&sFifoObj); + GXSetGPFifo(&sFifoObj); + + // Initialize shadow registers: genMode, bpMask, lpSize + __gx->genMode = 0; + SET_REG_FIELD(0, __gx->genMode, 8, 24, 0x00); + __gx->bpMask = 0xFF; + SET_REG_FIELD(0, __gx->bpMask, 8, 24, 0x0F); + __gx->lpSize = 0; + SET_REG_FIELD(0, __gx->lpSize, 8, 24, 0x22); + + // TEV / tref / ksel shadow registers + for (i = 0; i < 16; i++) { + __gx->tevc[i] = 0; + __gx->teva[i] = 0; + __gx->tref[i / 2] = 0; + __gx->texmapId[i] = GX_TEXMAP_NULL; + SET_REG_FIELD(0, __gx->tevc[i], 8, 24, 0xC0 + i * 2); + SET_REG_FIELD(0, __gx->teva[i], 8, 24, 0xC1 + i * 2); + SET_REG_FIELD(0, __gx->tevKsel[i / 2], 8, 24, 0xF6 + i / 2); + SET_REG_FIELD(0, __gx->tref[i / 2], 8, 24, 0x28 + i / 2); + } + + // iref and SU texture scale registers + __gx->iref = 0; + SET_REG_FIELD(0, __gx->iref, 8, 24, 0x27); + for (i = 0; i < 8; i++) { + __gx->suTs0[i] = 0; + __gx->suTs1[i] = 0; + SET_REG_FIELD(0, __gx->suTs0[i], 8, 24, 0x30 + i * 2); + SET_REG_FIELD(0, __gx->suTs1[i], 8, 24, 0x31 + i * 2); + } + + // Other BP command byte init + SET_REG_FIELD(0, __gx->suScis0, 8, 24, 0x20); + SET_REG_FIELD(0, __gx->suScis1, 8, 24, 0x21); + SET_REG_FIELD(0, __gx->cmode0, 8, 24, 0x41); + SET_REG_FIELD(0, __gx->cmode1, 8, 24, 0x42); + SET_REG_FIELD(0, __gx->zmode, 8, 24, 0x40); + SET_REG_FIELD(0, __gx->peCtrl, 8, 24, 0x43); + SET_REG_FIELD(0, __gx->IndTexScale0, 8, 24, 0x25); + SET_REG_FIELD(0, __gx->IndTexScale1, 8, 24, 0x26); + + __gx->dirtyState = 0; + __gx->dirtyVAT = 0; + + // VAT initialization: set default bits and write vatB to CP + for (i = 0; i < GX_MAX_VTXFMT; i++) { + SET_REG_FIELD(0, __gx->vatA[i], 1, 30, 1); + SET_REG_FIELD(0, __gx->vatB[i], 1, 31, 1); + GX_WRITE_U8(8); + GX_WRITE_U8(i | 0x80); + GX_WRITE_U32(__gx->vatB[i]); + } + + // XF register init: error/mode control + dual-tex transform + { + u32 reg1 = 0; + u32 reg2 = 0; + SET_REG_FIELD(0, reg1, 1, 0, 1); + SET_REG_FIELD(0, reg1, 1, 1, 1); + SET_REG_FIELD(0, reg1, 1, 2, 1); + SET_REG_FIELD(0, reg1, 1, 3, 1); + SET_REG_FIELD(0, reg1, 1, 4, 1); + SET_REG_FIELD(0, reg1, 1, 5, 1); + GX_WRITE_XF_REG(0, reg1); + SET_REG_FIELD(0, reg2, 1, 0, 1); + GX_WRITE_XF_REG(0x12, reg2); + } + + // BP 0x58 register init + { + u32 reg = 0; + SET_REG_FIELD(0, reg, 1, 0, 1); + SET_REG_FIELD(0, reg, 1, 1, 1); + SET_REG_FIELD(0, reg, 1, 2, 1); + SET_REG_FIELD(0, reg, 1, 3, 1); + SET_REG_FIELD(0, reg, 8, 24, 0x58); + GX_WRITE_RAS_REG(reg); + } + + // Default render mode (NTSC) + rmode = &GXNtsc480IntDf; + + // Default state initialization via API calls + GXSetCopyClear(clear, 0xFFFFFF); + GXSetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, 0x3C); + GXSetTexCoordGen(GX_TEXCOORD1, GX_TG_MTX2x4, GX_TG_TEX1, 0x3C); + GXSetTexCoordGen(GX_TEXCOORD2, GX_TG_MTX2x4, GX_TG_TEX2, 0x3C); + GXSetTexCoordGen(GX_TEXCOORD3, GX_TG_MTX2x4, GX_TG_TEX3, 0x3C); + GXSetTexCoordGen(GX_TEXCOORD4, GX_TG_MTX2x4, GX_TG_TEX4, 0x3C); + GXSetTexCoordGen(GX_TEXCOORD5, GX_TG_MTX2x4, GX_TG_TEX5, 0x3C); + GXSetTexCoordGen(GX_TEXCOORD6, GX_TG_MTX2x4, GX_TG_TEX6, 0x3C); + GXSetTexCoordGen(GX_TEXCOORD7, GX_TG_MTX2x4, GX_TG_TEX7, 0x3C); + GXSetNumTexGens(1); + GXClearVtxDesc(); + GXInvalidateVtxCache(); + GXSetLineWidth(6, GX_TO_ZERO); + GXSetPointSize(6, GX_TO_ZERO); + GXEnableTexOffsets(GX_TEXCOORD0, GX_DISABLE, GX_DISABLE); + GXEnableTexOffsets(GX_TEXCOORD1, GX_DISABLE, GX_DISABLE); + GXEnableTexOffsets(GX_TEXCOORD2, GX_DISABLE, GX_DISABLE); + GXEnableTexOffsets(GX_TEXCOORD3, GX_DISABLE, GX_DISABLE); + GXEnableTexOffsets(GX_TEXCOORD4, GX_DISABLE, GX_DISABLE); + GXEnableTexOffsets(GX_TEXCOORD5, GX_DISABLE, GX_DISABLE); + GXEnableTexOffsets(GX_TEXCOORD6, GX_DISABLE, GX_DISABLE); + GXEnableTexOffsets(GX_TEXCOORD7, GX_DISABLE, GX_DISABLE); + + // Identity matrix + identity_mtx[0][0] = 1.0f; + identity_mtx[0][1] = 0.0f; + identity_mtx[0][2] = 0.0f; + identity_mtx[0][3] = 0.0f; + identity_mtx[1][0] = 0.0f; + identity_mtx[1][1] = 1.0f; + identity_mtx[1][2] = 0.0f; + identity_mtx[1][3] = 0.0f; + identity_mtx[2][0] = 0.0f; + identity_mtx[2][1] = 0.0f; + identity_mtx[2][2] = 1.0f; + identity_mtx[2][3] = 0.0f; + GXLoadPosMtxImm(identity_mtx, GX_PNMTX0); + GXLoadNrmMtxImm(identity_mtx, GX_PNMTX0); + GXSetCurrentMtx(GX_PNMTX0); + GXLoadTexMtxImm(identity_mtx, GX_IDENTITY, GX_MTX3x4); + GXLoadTexMtxImm(identity_mtx, GX_PTIDENTITY, GX_MTX3x4); + + GXSetViewport(0.0f, 0.0f, rmode->fbWidth, rmode->xfbHeight, 0.0f, 1.0f); + GXSetCoPlanar(GX_DISABLE); + GXSetCullMode(GX_CULL_BACK); + GXSetClipMode(GX_CLIP_ENABLE); + GXSetScissor(0, 0, rmode->fbWidth, rmode->efbHeight); + // GXSetScissorBoxOffset(0, 0); + + GXSetNumChans(0); + GXSetChanCtrl(GX_COLOR0A0, GX_DISABLE, GX_SRC_REG, GX_SRC_VTX, GX_LIGHT_NULL, GX_DF_NONE, GX_AF_NONE); + GXSetChanAmbColor(GX_COLOR0A0, black); + GXSetChanMatColor(GX_COLOR0A0, white); + GXSetChanCtrl(GX_COLOR1A1, GX_DISABLE, GX_SRC_REG, GX_SRC_VTX, GX_LIGHT_NULL, GX_DF_NONE, GX_AF_NONE); + GXSetChanAmbColor(GX_COLOR1A1, black); + GXSetChanMatColor(GX_COLOR1A1, white); + + GXInvalidateTexAll(); + // __gx->nextTexRgn = 0; + // for (i = 0; i < 8; i++) + // GXInitTexCacheRegion(&__gx->TexRegions[i], 0, i * 0x8000, 0, 0x80000 + i * 0x8000, 0); + // __gx->nextTexRgnCI = 0; + // for (i = 0; i < 4; i++) + // GXInitTexCacheRegion(&__gx->TexRegionsCI[i], 0, (i * 2 + 8) * 0x8000, 0, (i * 2 + 9) * 0x8000, 0); + // for (i = 0; i < 16; i++) + // GXInitTlutRegion(&__gx->TlutRegions[i], 0xC0000 + i * 0x2000, 16); + // for (i = 0; i < 4; i++) + // GXInitTlutRegion(&__gx->TlutRegions[i + 16], 0xE0000 + i * 0x8000, 64); + // GXSetTexRegionCallback(__GXDefaultTexRegionCallback); + // GXSetTlutRegionCallback(__GXDefaultTlutRegionCallback); + + GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR0A0); + GXSetTevOrder(GX_TEVSTAGE1, GX_TEXCOORD1, GX_TEXMAP1, GX_COLOR0A0); + GXSetTevOrder(GX_TEVSTAGE2, GX_TEXCOORD2, GX_TEXMAP2, GX_COLOR0A0); + GXSetTevOrder(GX_TEVSTAGE3, GX_TEXCOORD3, GX_TEXMAP3, GX_COLOR0A0); + GXSetTevOrder(GX_TEVSTAGE4, GX_TEXCOORD4, GX_TEXMAP4, GX_COLOR0A0); + GXSetTevOrder(GX_TEVSTAGE5, GX_TEXCOORD5, GX_TEXMAP5, GX_COLOR0A0); + GXSetTevOrder(GX_TEVSTAGE6, GX_TEXCOORD6, GX_TEXMAP6, GX_COLOR0A0); + GXSetTevOrder(GX_TEVSTAGE7, GX_TEXCOORD7, GX_TEXMAP7, GX_COLOR0A0); + GXSetTevOrder(GX_TEVSTAGE8, GX_TEXCOORD_NULL, GX_TEXMAP_NULL, GX_COLOR_NULL); + GXSetTevOrder(GX_TEVSTAGE9, GX_TEXCOORD_NULL, GX_TEXMAP_NULL, GX_COLOR_NULL); + GXSetTevOrder(GX_TEVSTAGE10, GX_TEXCOORD_NULL, GX_TEXMAP_NULL, GX_COLOR_NULL); + GXSetTevOrder(GX_TEVSTAGE11, GX_TEXCOORD_NULL, GX_TEXMAP_NULL, GX_COLOR_NULL); + GXSetTevOrder(GX_TEVSTAGE12, GX_TEXCOORD_NULL, GX_TEXMAP_NULL, GX_COLOR_NULL); + GXSetTevOrder(GX_TEVSTAGE13, GX_TEXCOORD_NULL, GX_TEXMAP_NULL, GX_COLOR_NULL); + GXSetTevOrder(GX_TEVSTAGE14, GX_TEXCOORD_NULL, GX_TEXMAP_NULL, GX_COLOR_NULL); + GXSetTevOrder(GX_TEVSTAGE15, GX_TEXCOORD_NULL, GX_TEXMAP_NULL, GX_COLOR_NULL); + + GXSetNumTevStages(1); + GXSetTevOp(GX_TEVSTAGE0, GX_REPLACE); + GXSetAlphaCompare(GX_ALWAYS, 0, GX_AOP_AND, GX_ALWAYS, 0); + GXSetZTexture(GX_ZT_DISABLE, GX_TF_Z8, 0); + + for (i = GX_TEVSTAGE0; i < GX_MAX_TEVSTAGE; i++) { + GXSetTevKColorSel(static_cast(i), GX_TEV_KCSEL_1_4); + GXSetTevKAlphaSel(static_cast(i), GX_TEV_KASEL_1); + GXSetTevSwapMode(static_cast(i), GX_TEV_SWAP0, GX_TEV_SWAP0); + } + GXSetTevSwapModeTable(GX_TEV_SWAP0, GX_CH_RED, GX_CH_GREEN, GX_CH_BLUE, GX_CH_ALPHA); + GXSetTevSwapModeTable(GX_TEV_SWAP1, GX_CH_RED, GX_CH_RED, GX_CH_RED, GX_CH_ALPHA); + GXSetTevSwapModeTable(GX_TEV_SWAP2, GX_CH_GREEN, GX_CH_GREEN, GX_CH_GREEN, GX_CH_ALPHA); + GXSetTevSwapModeTable(GX_TEV_SWAP3, GX_CH_BLUE, GX_CH_BLUE, GX_CH_BLUE, GX_CH_ALPHA); + + for (i = GX_TEVSTAGE0; i < GX_MAX_TEVSTAGE; i++) + GXSetTevDirect(static_cast(i)); + GXSetNumIndStages(0); + GXSetIndTexCoordScale(GX_INDTEXSTAGE0, GX_ITS_1, GX_ITS_1); + GXSetIndTexCoordScale(GX_INDTEXSTAGE1, GX_ITS_1, GX_ITS_1); + GXSetIndTexCoordScale(GX_INDTEXSTAGE2, GX_ITS_1, GX_ITS_1); + GXSetIndTexCoordScale(GX_INDTEXSTAGE3, GX_ITS_1, GX_ITS_1); + + GXSetFog(GX_FOG_NONE, 0.0f, 1.0f, 0.1f, 1.0f, black); + GXSetFogRangeAdj(GX_DISABLE, 0, nullptr); + GXSetBlendMode(GX_BM_NONE, GX_BL_SRCALPHA, GX_BL_INVSRCALPHA, GX_LO_CLEAR); + GXSetColorUpdate(GX_ENABLE); + GXSetAlphaUpdate(GX_ENABLE); + GXSetZMode(GX_TRUE, GX_LEQUAL, GX_TRUE); + GXSetZCompLoc(GX_TRUE); + GXSetDither(GX_ENABLE); + GXSetDstAlpha(GX_DISABLE, 0); + GXSetPixelFmt(GX_PF_RGB8_Z24, GX_ZC_LINEAR); + GXSetFieldMask(GX_ENABLE, GX_ENABLE); + GXSetFieldMode(rmode->field_rendering, ((rmode->viHeight == 2 * rmode->xfbHeight) ? GX_ENABLE : GX_DISABLE)); + + GXSetDispCopySrc(0, 0, rmode->fbWidth, rmode->efbHeight); + GXSetDispCopyDst(rmode->fbWidth, rmode->efbHeight); + GXSetDispCopyYScale(static_cast(rmode->xfbHeight) / static_cast(rmode->efbHeight)); + // GXSetCopyClamp(static_cast(GX_CLAMP_TOP | GX_CLAMP_BOTTOM)); + GXSetCopyFilter(rmode->aa, rmode->sample_pattern, GX_TRUE, rmode->vfilter); + GXSetDispCopyGamma(GX_GM_1_0); + // GXSetDispCopyFrame2Field(GX_COPY_PROGRESSIVE); + // GXClearBoundingBox(); + + // GXPokeColorUpdate(GX_TRUE); + // GXPokeAlphaUpdate(GX_TRUE); + // GXPokeDither(GX_FALSE); + // GXPokeBlendMode(GX_BM_NONE, GX_BL_ZERO, GX_BL_ONE, GX_LO_SET); + // GXPokeAlphaMode(GX_ALWAYS, 0); + // GXPokeAlphaRead(GX_READ_FF); + // GXPokeDstAlpha(GX_DISABLE, 0); + // GXPokeZMode(GX_TRUE, GX_ALWAYS, GX_TRUE); + + return &sFifoObj; +} + +void GXDrawDone() { + aurora::gx::fifo::drain(); + if (DrawDoneCB != nullptr) + DrawDoneCB(); +} + +void GXSetDrawDone() { + aurora::gx::fifo::drain(); + if (DrawDoneCB != nullptr) + DrawDoneCB(); +} + +GXDrawDoneCallback GXSetDrawDoneCallback(GXDrawDoneCallback cb) { + GXDrawDoneCallback old = DrawDoneCB; + DrawDoneCB = cb; + return old; +} + +void GXFlush() { + if (__gx->dirtyState) { + __GXSetDirtyState(); + } +} + +void GXPixModeSync() { + GX_WRITE_RAS_REG(__gx->peCtrl); + __gx->bpSent = 1; +} + +void GXTexModeSync() { + GX_WRITE_RAS_REG(0x63000000); + __gx->bpSent = 1; +} + +// Dirty state flush functions + +void __GXSetDirtyState() { + if (__gx->dirtyState & 1) { + __GXSetSUTexRegs(); + } + if (__gx->dirtyState & 2) { + __GXUpdateBPMask(); + } + if (__gx->dirtyState & 4) { + __GXSetGenMode(); + } + if (__gx->dirtyState & 8) { + __GXSetVCD(); + } + if (__gx->dirtyState & 0x10) { + __GXSetVAT(); + } + __gx->dirtyState = 0; +} + +void __GXSetGenMode() { + GX_WRITE_RAS_REG(__gx->genMode); + __gx->bpSent = 1; +} + +void __GXSendFlushPrim() { + // Originally a dummy triangle strip to push the GP through the FIFO and flush pending BP + // register changes. We can skip the FIFO writes and just clear the bpSent flag. + + // GX_WRITE_U8(0x98); + // GX_WRITE_U16(__gx->vNum); + // for (u32 i = 0; i < __gx->vNum * __gx->vLim; i += 4) { + // GX_WRITE_U32(0); + // } + + __gx->bpSent = 0; +} + +void __GXSetVCD() { + // Write VCD lo and hi to CP registers + GX_WRITE_SOME_REG4(8, 0x50, __gx->vcdLo, -12); + GX_WRITE_SOME_REG4(8, 0x60, __gx->vcdHi, -12); + + // Write XF vertex specs + u32 nCols = 0; + nCols = GET_REG_FIELD(__gx->vcdLo, 2, 13) ? 1 : 0; + nCols += GET_REG_FIELD(__gx->vcdLo, 2, 15) ? 1 : 0; + u32 nNrm = __gx->hasBiNrms ? 2 : __gx->hasNrms ? 1 : 0; + u32 nTex = 0; + nTex += GET_REG_FIELD(__gx->vcdHi, 2, 0) ? 1 : 0; + nTex += GET_REG_FIELD(__gx->vcdHi, 2, 2) ? 1 : 0; + nTex += GET_REG_FIELD(__gx->vcdHi, 2, 4) ? 1 : 0; + nTex += GET_REG_FIELD(__gx->vcdHi, 2, 6) ? 1 : 0; + nTex += GET_REG_FIELD(__gx->vcdHi, 2, 8) ? 1 : 0; + nTex += GET_REG_FIELD(__gx->vcdHi, 2, 10) ? 1 : 0; + nTex += GET_REG_FIELD(__gx->vcdHi, 2, 12) ? 1 : 0; + nTex += GET_REG_FIELD(__gx->vcdHi, 2, 14) ? 1 : 0; + u32 reg = (nCols) | (nNrm << 2) | (nTex << 4); + GX_WRITE_XF_REG(8, reg); + __gx->bpSent = 0; + + // Calculate vertex data limit for flush prim + if (__gx->vNum != 0) { + static u8 tbl1[] = {0, 4, 1, 2}; + static u8 tbl2[] = {0, 8, 1, 2}; + static u8 tbl3[] = {0, 12, 1, 2}; + + u32 vl = __gx->vcdLo; + u32 vh = __gx->vcdHi; + u32 vlm = 0; + vlm = GET_REG_FIELD(vl, 1, 0); + vlm += static_cast(GET_REG_FIELD(vl, 1, 1)); + vlm += static_cast(GET_REG_FIELD(vl, 1, 2)); + vlm += static_cast(GET_REG_FIELD(vl, 1, 3)); + vlm += static_cast(GET_REG_FIELD(vl, 1, 4)); + vlm += static_cast(GET_REG_FIELD(vl, 1, 5)); + vlm += static_cast(GET_REG_FIELD(vl, 1, 6)); + vlm += static_cast(GET_REG_FIELD(vl, 1, 7)); + vlm += static_cast(GET_REG_FIELD(vl, 1, 8)); + vlm += tbl3[static_cast(GET_REG_FIELD(vl, 2, 9))]; + u32 b = (__gx->hasBiNrms << 1) + 1; + vlm += tbl3[static_cast(GET_REG_FIELD(vl, 2, 11))] * b; + vlm += tbl1[static_cast(GET_REG_FIELD(vl, 2, 13))]; + vlm += tbl1[static_cast(GET_REG_FIELD(vl, 2, 15))]; + vlm += tbl2[static_cast(GET_REG_FIELD(vh, 2, 0))]; + vlm += tbl2[static_cast(GET_REG_FIELD(vh, 2, 2))]; + vlm += tbl2[static_cast(GET_REG_FIELD(vh, 2, 4))]; + vlm += tbl2[static_cast(GET_REG_FIELD(vh, 2, 6))]; + vlm += tbl2[static_cast(GET_REG_FIELD(vh, 2, 8))]; + vlm += tbl2[static_cast(GET_REG_FIELD(vh, 2, 10))]; + vlm += tbl2[static_cast(GET_REG_FIELD(vh, 2, 12))]; + vlm += tbl2[static_cast(GET_REG_FIELD(vh, 2, 14))]; + __gx->vLim = vlm; + } +} + +void __GXSetVAT() { + for (u8 i = 0; i < 8; i++) { + if (__gx->dirtyVAT & (1 << i)) { + GX_WRITE_SOME_REG4(8, i | 0x70, __gx->vatA[i], i - 12); + GX_WRITE_SOME_REG4(8, i | 0x80, __gx->vatB[i], i - 12); + GX_WRITE_SOME_REG4(8, i | 0x90, __gx->vatC[i], i - 12); + } + } + __gx->dirtyVAT = 0; +} + +static void __SetSURegs(u32 tmap, u32 tcoord) { + // Copy texture dimensions from tImage0 to SU registers + u32 w = GET_REG_FIELD(__gx->tImage0[tmap], 10, 0); + u32 h = GET_REG_FIELD(__gx->tImage0[tmap], 10, 10); + SET_REG_FIELD(0, __gx->suTs0[tcoord], 16, 0, w); + SET_REG_FIELD(0, __gx->suTs1[tcoord], 16, 0, h); + // Bias from wrap mode + u8 s_bias = GET_REG_FIELD(__gx->tMode0[tmap], 2, 0) == 1; + u8 t_bias = GET_REG_FIELD(__gx->tMode0[tmap], 2, 2) == 1; + SET_REG_FIELD(0, __gx->suTs0[tcoord], 1, 16, s_bias); + SET_REG_FIELD(0, __gx->suTs1[tcoord], 1, 16, t_bias); + GX_WRITE_RAS_REG(__gx->suTs0[tcoord]); + GX_WRITE_RAS_REG(__gx->suTs1[tcoord]); + __gx->bpSent = 1; +} + +void __GXSetSUTexRegs() { + // SU texture size/bias registers for each active TEV and indirect stage, skipping coords with + // manual scale (tcsManEnab). If all coords are manual (0xFF), skip entirely. + if (__gx->tcsManEnab == 0xFF) { + return; + } + + u32 nStages = GET_REG_FIELD(__gx->genMode, 4, 10) + 1; + u32 nIndStages = GET_REG_FIELD(__gx->genMode, 3, 16); + + // Indirect texture stages + for (u32 i = 0; i < nIndStages; i++) { + u32 tmap, coord; + switch (i) { + case 0: + tmap = GET_REG_FIELD(__gx->iref, 3, 0); + coord = GET_REG_FIELD(__gx->iref, 3, 3); + break; + case 1: + tmap = GET_REG_FIELD(__gx->iref, 3, 6); + coord = GET_REG_FIELD(__gx->iref, 3, 9); + break; + case 2: + tmap = GET_REG_FIELD(__gx->iref, 3, 12); + coord = GET_REG_FIELD(__gx->iref, 3, 15); + break; + default: + tmap = GET_REG_FIELD(__gx->iref, 3, 18); + coord = GET_REG_FIELD(__gx->iref, 3, 21); + break; + } + if (!(__gx->tcsManEnab & (1 << coord))) { + __SetSURegs(tmap, coord); + } + } + + // Direct TEV stages + for (u32 i = 0; i < nStages; i++) { + u32* ptref = &__gx->tref[i / 2]; + u32 map = __gx->texmapId[i]; + u32 tmap = map & ~0x100u; + u32 coord; + if (i & 1) { + coord = GET_REG_FIELD(*ptref, 3, 15); + } else { + coord = GET_REG_FIELD(*ptref, 3, 3); + } + if (tmap != 0xFF && !(__gx->tcsManEnab & (1 << coord)) && (__gx->texmapValid & (1u << i))) { + __SetSURegs(tmap, coord); + } + } +} + +void __GXUpdateBPMask() { + // Mark texture maps used by indirect texture stages in the BP mask + u32 nIndStages = GET_REG_FIELD(__gx->genMode, 3, 16); + u32 newImask = 0; + + for (u32 i = 0; i < nIndStages; i++) { + u32 tmap = GET_REG_FIELD(__gx->iref, 3, i * 6); + newImask |= 1u << tmap; + } + + if (static_cast(__gx->bpMask) != static_cast(newImask)) { + SET_REG_FIELD(0, __gx->bpMask, 8, 0, newImask); + GX_WRITE_RAS_REG(__gx->bpMask); + __gx->bpSent = 1; + } +} + +void __GXSetMatrixIndex(GXAttr matIdxAttr) { + if (matIdxAttr < GX_VA_TEX4MTXIDX) { + GX_WRITE_SOME_REG4(8, 0x30, __gx->matIdxA, -12); + GX_WRITE_XF_REG(24, __gx->matIdxA); + } else { + GX_WRITE_SOME_REG4(8, 0x40, __gx->matIdxB, -12); + GX_WRITE_XF_REG(25, __gx->matIdxB); + } + __gx->bpSent = 0; +} +}; diff --git a/aurora-main/lib/dolphin/gx/GXPixel.cpp b/aurora-main/lib/dolphin/gx/GXPixel.cpp new file mode 100644 index 0000000..d4f3fb5 --- /dev/null +++ b/aurora-main/lib/dolphin/gx/GXPixel.cpp @@ -0,0 +1,247 @@ +#include "gx.hpp" +#include "__gx.h" + +#include + +extern "C" { +void GXSetFog(GXFogType type, float startZ, float endZ, float nearZ, float farZ, GXColor color) { + // Compute fog coefficients matching SDK. + const bool orthographic = (static_cast(type) & 0x8u) != 0; + f32 a = 0.0f; + f32 c = 0.0f; + u32 b_m = 0; + u32 b_s = 0; + + if (!orthographic) { + f32 A; + f32 B; + f32 C; + if (farZ == nearZ || endZ == startZ) { + A = 0.0f; + B = 0.5f; + C = 0.0f; + } else { + A = (farZ * nearZ) / ((farZ - nearZ) * (endZ - startZ)); + B = farZ / (farZ - nearZ); + C = startZ / (endZ - startZ); + } + + // Normalize B mantissa. + f32 B_mant = B; + int B_expn = 0; + while (B_mant > 1.0f) { + B_mant *= 0.5f; + B_expn++; + } + while (B_mant > 0.0f && B_mant < 0.5f) { + B_mant *= 2.0f; + B_expn--; + } + + a = std::ldexp(A, -(B_expn + 1)); + b_m = static_cast(8.388638e6f * B_mant); + b_s = static_cast(B_expn + 1) & 0x1Fu; + c = C; + } else if (farZ != nearZ && endZ != startZ) { + const f32 invDepthRange = 1.0f / (endZ - startZ); + a = invDepthRange * (farZ - nearZ); + c = invDepthRange * (startZ - nearZ); + } + + u32 a_hex, c_hex; + std::memcpy(&a_hex, &a, sizeof(a_hex)); + std::memcpy(&c_hex, &c, sizeof(c_hex)); + + // BP FOG0 (0xEE) - A parameter + u32 fog0 = 0; + SET_REG_FIELD(0, fog0, 11, 0, (a_hex >> 12) & 0x7FF); + SET_REG_FIELD(0, fog0, 8, 11, (a_hex >> 23) & 0xFF); + SET_REG_FIELD(0, fog0, 1, 19, (a_hex >> 31)); + SET_REG_FIELD(0, fog0, 8, 24, 0xEE); + + // BP FOG1 (0xEF) - B mantissa + u32 fog1 = 0; + SET_REG_FIELD(0, fog1, 24, 0, b_m); + SET_REG_FIELD(0, fog1, 8, 24, 0xEF); + + // BP FOG2 (0xF0) - B scale + u32 fog2 = 0; + SET_REG_FIELD(0, fog2, 5, 0, b_s); + SET_REG_FIELD(0, fog2, 8, 24, 0xF0); + + // BP FOG3 (0xF1) - C parameter + type + u32 fog3 = 0; + SET_REG_FIELD(0, fog3, 11, 0, (c_hex >> 12) & 0x7FF); + SET_REG_FIELD(0, fog3, 8, 11, (c_hex >> 23) & 0xFF); + SET_REG_FIELD(0, fog3, 1, 19, (c_hex >> 31)); + SET_REG_FIELD(0, fog3, 1, 20, orthographic ? 1 : 0); + SET_REG_FIELD(0, fog3, 3, 21, static_cast(type) & 0x7u); + SET_REG_FIELD(0, fog3, 8, 24, 0xF1); + + // BP FOGCLR (0xF2) - color + u32 fogclr = 0; + SET_REG_FIELD(0, fogclr, 8, 0, color.b); + SET_REG_FIELD(0, fogclr, 8, 8, color.g); + SET_REG_FIELD(0, fogclr, 8, 16, color.r); + SET_REG_FIELD(0, fogclr, 8, 24, 0xF2); + + GX_WRITE_RAS_REG(fog0); + GX_WRITE_RAS_REG(fog1); + GX_WRITE_RAS_REG(fog2); + GX_WRITE_RAS_REG(fog3); + GX_WRITE_RAS_REG(fogclr); + __gx->bpSent = 1; +} + +void GXSetFogColor(GXColor color) { + // BP FOGCLR (0xF2) + u32 fogclr = 0; + SET_REG_FIELD(0, fogclr, 8, 0, color.b); + SET_REG_FIELD(0, fogclr, 8, 8, color.g); + SET_REG_FIELD(0, fogclr, 8, 16, color.r); + SET_REG_FIELD(0, fogclr, 8, 24, 0xF2); + GX_WRITE_RAS_REG(fogclr); + __gx->bpSent = 1; +} + +void GXSetFogRangeAdj(GXBool enable, u16 center, GXFogAdjTable* table) { + u32 i; + u32 range_adj; + u32 range_c; + + if (enable) { + for (i = 0; i < 10; i += 2) { + range_adj = 0; + SET_REG_FIELD(0x10D, range_adj, 12, 0, table->r[i]); + SET_REG_FIELD(0x10E, range_adj, 12, 12, table->r[i + 1]); + SET_REG_FIELD(0x10F, range_adj, 8, 24, (i >> 1) + 0xE9); + GX_WRITE_RAS_REG(range_adj); + } + } + range_c = 0; + SET_REG_FIELD(0x115, range_c, 10, 0, center + 342); + SET_REG_FIELD(0x116, range_c, 1, 10, enable); + SET_REG_FIELD(0x117, range_c, 8, 24, 0xE8); + GX_WRITE_RAS_REG(range_c); + __gx->bpSent = 1; +} + +void GXSetBlendMode(GXBlendMode mode, GXBlendFactor src, GXBlendFactor dst, GXLogicOp op) { + SET_REG_FIELD(0, __gx->cmode0, 1, 0, (mode == GX_BM_BLEND || mode == GX_BM_SUBTRACT)); + SET_REG_FIELD(0, __gx->cmode0, 1, 11, (mode == GX_BM_SUBTRACT)); + SET_REG_FIELD(0, __gx->cmode0, 1, 1, (mode == GX_BM_LOGIC)); + SET_REG_FIELD(0, __gx->cmode0, 4, 12, op); + SET_REG_FIELD(0, __gx->cmode0, 3, 8, src); + SET_REG_FIELD(0, __gx->cmode0, 3, 5, dst); + GX_WRITE_RAS_REG(__gx->cmode0); + __gx->bpSent = 1; +} + +void GXSetColorUpdate(GXBool enabled) { + SET_REG_FIELD(0, __gx->cmode0, 1, 3, enabled); + GX_WRITE_RAS_REG(__gx->cmode0); + __gx->bpSent = 1; +} + +void GXSetAlphaUpdate(bool enabled) { + SET_REG_FIELD(0, __gx->cmode0, 1, 4, enabled); + GX_WRITE_RAS_REG(__gx->cmode0); + __gx->bpSent = 1; +} + +void GXSetZMode(bool compare_enable, GXCompare func, bool update_enable) { + SET_REG_FIELD(0, __gx->zmode, 1, 0, compare_enable); + SET_REG_FIELD(0, __gx->zmode, 3, 1, func); + SET_REG_FIELD(0, __gx->zmode, 1, 4, update_enable); + GX_WRITE_RAS_REG(__gx->zmode); + __gx->bpSent = 1; +} + +void GXSetZCompLoc(GXBool before_tex) { + SET_REG_FIELD(0, __gx->peCtrl, 1, 6, before_tex); + GX_WRITE_RAS_REG(__gx->peCtrl); + __gx->bpSent = 1; +} + +void GXSetPixelFmt(GXPixelFmt pix_fmt, GXZFmt16 z_fmt) { + u32 oldPeCtrl = __gx->peCtrl; + u8 hwPixelFmt = 0; + + switch (pix_fmt) { + case GX_PF_RGB8_Z24: + hwPixelFmt = 0; + break; + case GX_PF_RGBA6_Z24: + hwPixelFmt = 1; + break; + case GX_PF_RGB565_Z16: + hwPixelFmt = 2; + break; + case GX_PF_Z24: + hwPixelFmt = 3; + break; + case GX_PF_Y8: + case GX_PF_U8: + case GX_PF_V8: + hwPixelFmt = 4; + break; + case GX_PF_YUV420: + hwPixelFmt = 5; + break; + default: + UNLIKELY FATAL("GXSetPixelFmt: unsupported pixel format {}", static_cast(pix_fmt)); + } + + SET_REG_FIELD(0, __gx->peCtrl, 3, 0, hwPixelFmt); + SET_REG_FIELD(0, __gx->peCtrl, 3, 3, z_fmt); + if (oldPeCtrl != __gx->peCtrl) { + GX_WRITE_RAS_REG(__gx->peCtrl); + SET_REG_FIELD(0, __gx->genMode, 1, 9, pix_fmt == GX_PF_RGB565_Z16); + __gx->dirtyState |= 4; + } + + if (hwPixelFmt == 4) { + SET_REG_FIELD(0, __gx->cmode1, 2, 9, (static_cast(pix_fmt) - GX_PF_Y8) & 0x3); + GX_WRITE_RAS_REG(__gx->cmode1); + } + + __gx->bpSent = 1; +} + +void GXSetDither(GXBool dither) { + SET_REG_FIELD(0, __gx->cmode0, 1, 2, dither); + GX_WRITE_RAS_REG(__gx->cmode0); + __gx->bpSent = 1; +} + +void GXSetDstAlpha(bool enabled, u8 value) { + SET_REG_FIELD(0, __gx->cmode1, 8, 0, value); + SET_REG_FIELD(0, __gx->cmode1, 1, 8, enabled); + GX_WRITE_RAS_REG(__gx->cmode1); + __gx->bpSent = 1; +} + +void GXSetFieldMask(GXBool odd_mask, GXBool even_mask) { + u32 reg; + + reg = 0; + SET_REG_FIELD(0x1FB, reg, 1, 0, even_mask); + SET_REG_FIELD(0x1FC, reg, 1, 1, odd_mask); + SET_REG_FIELD(0x1FD, reg, 8, 24, 0x44); + GX_WRITE_RAS_REG(reg); + __gx->bpSent = 1; +} + +void GXSetFieldMode(GXBool field_mode, GXBool half_aspect_ratio) { + u32 reg; + + SET_REG_FIELD(0x21A, __gx->lpSize, 1, 22, half_aspect_ratio); + GX_WRITE_RAS_REG(__gx->lpSize); + __GXFlushTextureState(); + + SET_REG_FIELD(0, reg, 8, 24, 0x68); + SET_REG_FIELD(0, reg, 1, 0, field_mode); + GX_WRITE_RAS_REG(reg); + __GXFlushTextureState(); +} +} diff --git a/aurora-main/lib/dolphin/gx/GXTev.cpp b/aurora-main/lib/dolphin/gx/GXTev.cpp new file mode 100644 index 0000000..8b021fe --- /dev/null +++ b/aurora-main/lib/dolphin/gx/GXTev.cpp @@ -0,0 +1,267 @@ +#include "gx.hpp" +#include "__gx.h" + +extern "C" { +void GXSetTevOp(GXTevStageID id, GXTevMode mode) { + GXTevColorArg inputColor = GX_CC_RASC; + GXTevAlphaArg inputAlpha = GX_CA_RASA; + if (id != GX_TEVSTAGE0) { + inputColor = GX_CC_CPREV; + inputAlpha = GX_CA_APREV; + } + switch (mode) { + case GX_MODULATE: + GXSetTevColorIn(id, GX_CC_ZERO, GX_CC_TEXC, inputColor, GX_CC_ZERO); + GXSetTevAlphaIn(id, GX_CA_ZERO, GX_CA_TEXA, inputAlpha, GX_CA_ZERO); + break; + case GX_DECAL: + GXSetTevColorIn(id, inputColor, GX_CC_TEXC, GX_CC_TEXA, GX_CC_ZERO); + GXSetTevAlphaIn(id, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, inputAlpha); + break; + case GX_BLEND: + GXSetTevColorIn(id, inputColor, GX_CC_ONE, GX_CC_TEXC, GX_CC_ZERO); + GXSetTevAlphaIn(id, GX_CA_ZERO, GX_CA_TEXA, inputAlpha, GX_CA_ZERO); + break; + case GX_REPLACE: + GXSetTevColorIn(id, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, GX_CC_TEXC); + GXSetTevAlphaIn(id, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_TEXA); + break; + case GX_PASSCLR: + GXSetTevColorIn(id, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, inputColor); + GXSetTevAlphaIn(id, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, inputAlpha); + break; + } + GXSetTevColorOp(id, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + GXSetTevAlphaOp(id, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); +} + +void GXSetTevColorIn(GXTevStageID stageId, GXTevColorArg a, GXTevColorArg b, GXTevColorArg c, GXTevColorArg d) { + u32* reg = &__gx->tevc[stageId]; + SET_REG_FIELD(0, *reg, 4, 12, a); + SET_REG_FIELD(0, *reg, 4, 8, b); + SET_REG_FIELD(0, *reg, 4, 4, c); + SET_REG_FIELD(0, *reg, 4, 0, d); + GX_WRITE_RAS_REG(*reg); + __gx->bpSent = 1; +} + +void GXSetTevAlphaIn(GXTevStageID stageId, GXTevAlphaArg a, GXTevAlphaArg b, GXTevAlphaArg c, GXTevAlphaArg d) { + u32* reg = &__gx->teva[stageId]; + SET_REG_FIELD(0, *reg, 3, 13, a); + SET_REG_FIELD(0, *reg, 3, 10, b); + SET_REG_FIELD(0, *reg, 3, 7, c); + SET_REG_FIELD(0, *reg, 3, 4, d); + GX_WRITE_RAS_REG(*reg); + __gx->bpSent = 1; +} + +void GXSetTevColorOp(GXTevStageID stageId, GXTevOp op, GXTevBias bias, GXTevScale scale, bool clamp, + GXTevRegID outReg) { + u32* reg = &__gx->tevc[stageId]; + SET_REG_FIELD(0, *reg, 1, 18, op & 1); + if (op <= 1) { + SET_REG_FIELD(0, *reg, 2, 20, scale); + SET_REG_FIELD(0, *reg, 2, 16, bias); + } else { + SET_REG_FIELD(0, *reg, 2, 20, (op >> 1) & 3); + SET_REG_FIELD(0, *reg, 2, 16, 3); + } + SET_REG_FIELD(0, *reg, 1, 19, clamp); + SET_REG_FIELD(0, *reg, 2, 22, outReg); + GX_WRITE_RAS_REG(*reg); + __gx->bpSent = 1; +} + +void GXSetTevAlphaOp(GXTevStageID stageId, GXTevOp op, GXTevBias bias, GXTevScale scale, bool clamp, + GXTevRegID outReg) { + u32* reg = &__gx->teva[stageId]; + SET_REG_FIELD(0, *reg, 1, 18, op & 1); + if (op <= 1) { + SET_REG_FIELD(0, *reg, 2, 20, scale); + SET_REG_FIELD(0, *reg, 2, 16, bias); + } else { + SET_REG_FIELD(0, *reg, 2, 20, (op >> 1) & 3); + SET_REG_FIELD(0, *reg, 2, 16, 3); + } + SET_REG_FIELD(0, *reg, 1, 19, clamp); + SET_REG_FIELD(0, *reg, 2, 22, outReg); + GX_WRITE_RAS_REG(*reg); + __gx->bpSent = 1; +} + +void GXSetTevColor(GXTevRegID id, GXColor color) { + CHECK(id >= GX_TEVPREV && id < GX_MAX_TEVREG, "bad tevreg {}", static_cast(id)); + + // Write BP registers (RA + BG pairs) - needed for display list capture + u32 regRA = 0; + SET_REG_FIELD(0, regRA, 11, 0, color.r); + SET_REG_FIELD(0, regRA, 11, 12, color.a); + SET_REG_FIELD(0, regRA, 8, 24, 0xE0 + id * 2); + u32 regBG = 0; + SET_REG_FIELD(0, regBG, 11, 0, color.b); + SET_REG_FIELD(0, regBG, 11, 12, color.g); + SET_REG_FIELD(0, regBG, 8, 24, 0xE1 + id * 2); + GX_WRITE_RAS_REG(regRA); + GX_WRITE_RAS_REG(regBG); + // NOTE: the SDK writes regBG three more times here for hardware timing. The value does not + // change and our software command processor needs no sync delay, so skip them. + __gx->bpSent = 1; +} + +void GXSetTevColorS10(GXTevRegID id, GXColorS10 color) { + // Write BP registers (RA + BG pairs) - needed for display list capture + u32 regRA = 0; + SET_REG_FIELD(0, regRA, 11, 0, color.r & 0x7FF); + SET_REG_FIELD(0, regRA, 11, 12, color.a & 0x7FF); + SET_REG_FIELD(0, regRA, 8, 24, 0xE0 + id * 2); + u32 regBG = 0; + SET_REG_FIELD(0, regBG, 11, 0, color.b & 0x7FF); + SET_REG_FIELD(0, regBG, 11, 12, color.g & 0x7FF); + SET_REG_FIELD(0, regBG, 8, 24, 0xE1 + id * 2); + GX_WRITE_RAS_REG(regRA); + GX_WRITE_RAS_REG(regBG); + // NOTE: the SDK writes regBG three more times here for hardware timing. The value does not + // change and our software command processor needs no sync delay, so skip them. + __gx->bpSent = 1; +} + +void GXSetAlphaCompare(GXCompare comp0, u8 ref0, GXAlphaOp op, GXCompare comp1, u8 ref1) { + // BP register 0xF3 + u32 reg = 0; + SET_REG_FIELD(0, reg, 8, 0, ref0); + SET_REG_FIELD(0, reg, 8, 8, ref1); + SET_REG_FIELD(0, reg, 3, 16, comp0); + SET_REG_FIELD(0, reg, 3, 19, comp1); + SET_REG_FIELD(0, reg, 2, 22, op); + SET_REG_FIELD(0, reg, 8, 24, 0xF3); + GX_WRITE_RAS_REG(reg); + __gx->bpSent = 1; +} + +void GXSetTevOrder(GXTevStageID id, GXTexCoordID tcid, GXTexMapID tmid, GXChannelID cid) { + // Channel ID mapping to hardware register values + static const u8 c2r[] = {0, 1, 0, 1, 0, 1, 7, 5, 6}; + u32* ptref = &__gx->tref[id / 2]; + __gx->texmapId[id] = tmid; + + u32 tmap = tmid & ~0x100u; + tmap = (tmap >= GX_MAX_TEXMAP) ? GX_TEXMAP0 : tmap; + const bool texCoordValid = tcid < GX_MAX_TEXCOORD; + if (texCoordValid) { + __gx->texmapValid |= 1u << id; + } else { + __gx->texmapValid &= ~(1u << id); + } + u32 tcoord = texCoordValid ? tcid : GX_TEXCOORD0; + u32 chanHw = (cid == GX_COLOR_NULL) ? 7 : c2r[cid]; + + if (id & 1) { + SET_REG_FIELD(0, *ptref, 3, 12, tmap); + SET_REG_FIELD(0, *ptref, 3, 15, tcoord); + SET_REG_FIELD(0, *ptref, 3, 19, chanHw); + SET_REG_FIELD(0, *ptref, 1, 18, (tmid != GX_TEXMAP_NULL && !(tmid & 0x100))); + } else { + SET_REG_FIELD(0, *ptref, 3, 0, tmap); + SET_REG_FIELD(0, *ptref, 3, 3, tcoord); + SET_REG_FIELD(0, *ptref, 3, 7, chanHw); + SET_REG_FIELD(0, *ptref, 1, 6, (tmid != GX_TEXMAP_NULL && !(tmid & 0x100))); + } + + GX_WRITE_RAS_REG(*ptref); + __gx->bpSent = 1; + __gx->dirtyState |= 1; // SU tex regs dirty +} + +void GXSetZTexture(GXZTexOp op, GXTexFmt fmt, u32 bias) { + u32 zfmt; + switch (fmt) { + case GX_TF_Z8: + zfmt = 0; + break; + case GX_TF_Z16: + zfmt = 1; + break; + case GX_TF_Z24X8: + zfmt = 2; + break; + default: + zfmt = 2; + break; + } + + GX_WRITE_RAS_REG(0xF4000000u | (bias & 0x00FFFFFFu)); + GX_WRITE_RAS_REG(0xF5000000u | ((static_cast(op) & 0x3u) << 2) | (zfmt & 0x3u)); + __gx->bpSent = 1; +} + +void GXSetNumTevStages(u8 num) { + SET_REG_FIELD(0, __gx->genMode, 4, 10, num - 1); + __gx->dirtyState |= 4; // gen mode dirty +} + +void GXSetTevKColor(GXTevKColorID id, GXColor color) { + CHECK(id >= GX_KCOLOR0 && id < GX_MAX_KCOLOR, "bad kcolor {}", static_cast(id)); + + // Write BP registers (RA + BG pairs with bit 23 set for K color) + u32 regRA = 0; + SET_REG_FIELD(0, regRA, 8, 0, color.r); + SET_REG_FIELD(0, regRA, 8, 12, color.a); + SET_REG_FIELD(0, regRA, 4, 20, 8); // K color flag + SET_REG_FIELD(0, regRA, 8, 24, 0xE0 + id * 2); + u32 regBG = 0; + SET_REG_FIELD(0, regBG, 8, 0, color.b); + SET_REG_FIELD(0, regBG, 8, 12, color.g); + SET_REG_FIELD(0, regBG, 4, 20, 8); // K color flag + SET_REG_FIELD(0, regBG, 8, 24, 0xE1 + id * 2); + GX_WRITE_RAS_REG(regRA); + GX_WRITE_RAS_REG(regBG); + __gx->bpSent = 1; +} + +void GXSetTevKColorSel(GXTevStageID id, GXTevKColorSel sel) { + // tevKsel registers: 2 stages per register + u32 kselIdx = id / 2; + if (id & 1) { + SET_REG_FIELD(0, __gx->tevKsel[kselIdx], 5, 14, sel); + } else { + SET_REG_FIELD(0, __gx->tevKsel[kselIdx], 5, 4, sel); + } + GX_WRITE_RAS_REG(__gx->tevKsel[kselIdx]); + __gx->bpSent = 1; +} + +void GXSetTevKAlphaSel(GXTevStageID id, GXTevKAlphaSel sel) { + u32 kselIdx = id / 2; + if (id & 1) { + SET_REG_FIELD(0, __gx->tevKsel[kselIdx], 5, 19, sel); + } else { + SET_REG_FIELD(0, __gx->tevKsel[kselIdx], 5, 9, sel); + } + GX_WRITE_RAS_REG(__gx->tevKsel[kselIdx]); + __gx->bpSent = 1; +} + +void GXSetTevSwapMode(GXTevStageID stageId, GXTevSwapSel rasSel, GXTevSwapSel texSel) { + // Swap mode is stored in teva register + u32* reg = &__gx->teva[stageId]; + SET_REG_FIELD(0, *reg, 2, 0, rasSel); + SET_REG_FIELD(0, *reg, 2, 2, texSel); + GX_WRITE_RAS_REG(*reg); + __gx->bpSent = 1; +} + +void GXSetTevSwapModeTable(GXTevSwapSel id, GXTevColorChan red, GXTevColorChan green, GXTevColorChan blue, + GXTevColorChan alpha) { + CHECK(id >= GX_TEV_SWAP0 && id < GX_MAX_TEVSWAP, "bad tev swap sel {}", static_cast(id)); + // Swap table is stored in tevKsel registers + u32 kselIdx = id * 2; + SET_REG_FIELD(0, __gx->tevKsel[kselIdx], 2, 0, red); + SET_REG_FIELD(0, __gx->tevKsel[kselIdx], 2, 2, green); + GX_WRITE_RAS_REG(__gx->tevKsel[kselIdx]); + + SET_REG_FIELD(0, __gx->tevKsel[kselIdx + 1], 2, 0, blue); + SET_REG_FIELD(0, __gx->tevKsel[kselIdx + 1], 2, 2, alpha); + GX_WRITE_RAS_REG(__gx->tevKsel[kselIdx + 1]); + __gx->bpSent = 1; +} +} diff --git a/aurora-main/lib/dolphin/gx/GXTexture.cpp b/aurora-main/lib/dolphin/gx/GXTexture.cpp new file mode 100644 index 0000000..684ba79 --- /dev/null +++ b/aurora-main/lib/dolphin/gx/GXTexture.cpp @@ -0,0 +1,484 @@ +#include "gx.hpp" +#include "__gx.h" + +#include "../../gfx/texture.hpp" +#include "../../gfx/texture_replacement.hpp" +#include "dolphin/gx/GXAurora.h" + +#include + +#include +#include +#include + +#include "tracy/Tracy.hpp" + +namespace { +constexpr u8 GXTexMode0Ids[8] = {0x80, 0x81, 0x82, 0x83, 0xA0, 0xA1, 0xA2, 0xA3}; +constexpr u8 GXTexMode1Ids[8] = {0x84, 0x85, 0x86, 0x87, 0xA4, 0xA5, 0xA6, 0xA7}; +constexpr u8 GXTexImage0Ids[8] = {0x88, 0x89, 0x8A, 0x8B, 0xA8, 0xA9, 0xAA, 0xAB}; +constexpr u8 GXTexImage1Ids[8] = {0x8C, 0x8D, 0x8E, 0x8F, 0xAC, 0xAD, 0xAE, 0xAF}; +constexpr u8 GXTexImage2Ids[8] = {0x90, 0x91, 0x92, 0x93, 0xB0, 0xB1, 0xB2, 0xB3}; +constexpr u8 GXTexImage3Ids[8] = {0x94, 0x95, 0x96, 0x97, 0xB4, 0xB5, 0xB6, 0xB7}; +constexpr u8 GXTexTlutIds[8] = {0x98, 0x99, 0x9A, 0x9B, 0xB8, 0xB9, 0xBA, 0xBB}; +constexpr u8 GX2HWFiltConv[6] = {0x00, 0x04, 0x01, 0x05, 0x02, 0x06}; + +u32 sNextTexObjId = 1; +u32 sNextTlutObjId = 1; + +u32 next_tex_obj_id() { + if (sNextTexObjId == 0) { + FATAL("texObj ID overflow"); + } + return sNextTexObjId++; +} + +u32 next_tlut_obj_id() { + if (sNextTlutObjId == 0) { + FATAL("tlutObj ID overflow"); + } + return sNextTlutObjId++; +} + + // Give each palette buffer a stable ID so its GPU cache can be reused. +struct TlutIdentity { + const void* data = nullptr; + u32 format = 0; + u32 entries = 0; + + bool operator==(const TlutIdentity& rhs) const noexcept { + return data == rhs.data && format == rhs.format && entries == rhs.entries; + } + + template + friend H AbslHashValue(H h, const TlutIdentity& key) { + return H::combine(std::move(h), key.data, key.format, key.entries); + } +}; + +struct TlutIdentityRecord { + u32 objId = 0; + u32 dataVersion = 0; + u64 digest = 0; +}; + + // Limit cached palette IDs; evicted palettes are safely uploaded again. +constexpr size_t MaxTlutIdentities = 4096; +absl::flat_hash_map sTlutIdentities; + + // Use separate high-range versions when a palette points to a new buffer. +u32 sNextRepointedTlutDataVersion = 0; + +u32 next_repointed_tlut_data_version() { + if (sNextRepointedTlutDataVersion == 0x7FFFFFFFu) { + sNextRepointedTlutDataVersion = 0; + } + return 0x80000000u | ++sNextRepointedTlutDataVersion; +} + +std::pair resolve_tlut_identity(const void* data, GXTlutFmt format, u16 entries) { + if (data == nullptr || entries == 0) { + return {next_tlut_obj_id(), 1}; + } + + const auto digest = + static_cast(aurora::xxh3_hash_s(data, static_cast(entries) * sizeof(u16))); + if (sTlutIdentities.size() >= MaxTlutIdentities) { + sTlutIdentities.clear(); + } + + const TlutIdentity key{ + .data = data, + .format = static_cast(format), + .entries = entries, + }; + auto [it, inserted] = sTlutIdentities.try_emplace(key); + auto& record = it->second; + if (inserted) { + record.objId = next_tlut_obj_id(); + record.dataVersion = 1; + record.digest = digest; + } else if (record.digest != digest) { + // Same buffer, different palette: invalidate everything keyed on this ID. + record.digest = digest; + if (record.dataVersion >= 0x7FFFFFFFu) { + record.dataVersion = 1; + } else { + ++record.dataVersion; + } + } + return {record.objId, record.dataVersion}; +} + +int __cntlzw(unsigned int val) { + if (val == 0) + return 32; // PowerPC returns 32 if the input is 0 +#ifdef _MSC_VER + unsigned long idx; + _BitScanReverse(&idx, val); + return 31 - (int)idx; +#else + return __builtin_clz(val); +#endif +} + +void init_texobj_common(GXTexObj_& obj, const void* data, u16 width, u16 height, u32 format, GXTexWrapMode wrapS, + GXTexWrapMode wrapT, GXBool mipmap) { + obj = {}; + obj.mWidth = width; + obj.mHeight = height; + obj.mFormat = format; + obj.tlut = GX_TLUT0; + obj.flags = 2; + obj.texObjId = next_tex_obj_id(); + obj.texDataVersion = 1; + + SET_REG_FIELD(0, obj.mode0, 2, 0, wrapS); + SET_REG_FIELD(0, obj.mode0, 2, 2, wrapT); + SET_REG_FIELD(0, obj.mode0, 1, 4, 1); + if (mipmap) { + obj.flags |= 1; + obj.mode0 = (obj.mode0 & 0xFFFFFF1F) | 0xC0; + const u32 maxDim = std::max(width, height); + const u8 lmax = static_cast(16.0f * static_cast(31 - __cntlzw(maxDim))); + SET_REG_FIELD(0, obj.mode1, 8, 8, lmax); + } else { + obj.mode0 = (obj.mode0 & 0xFFFFFF1F) | 0x80; + } + SET_REG_FIELD(0, obj.image0, 10, 0, static_cast(width - 1)); + SET_REG_FIELD(0, obj.image0, 10, 10, static_cast(height - 1)); + SET_REG_FIELD(0, obj.image0, 4, 20, format & 0xF); + + obj.data = data; +} + +void emit_loaded_texobj_metadata(const GXTexObj_& obj, GXTexMapID id) { + GX_WRITE_AURORA(GX_LOAD_AURORA_TEXOBJ); + GX_WRITE_U8(static_cast(id)); + GX_WRITE_U64(reinterpret_cast(obj.data)); + GX_WRITE_U32(obj.width()); + GX_WRITE_U32(obj.height()); + GX_WRITE_U32(obj.format()); + GX_WRITE_U32(static_cast(obj.tlut)); + GX_WRITE_U8(static_cast(obj.has_mips())); + GX_WRITE_U32(obj.texObjId); + GX_WRITE_U32(obj.texDataVersion); +} + +void emit_loaded_tlut_metadata(const GXTlutObj_& obj, u32 idx) { + GX_WRITE_AURORA(GX_LOAD_AURORA_TLUT); + GX_WRITE_U8(static_cast(idx)); + GX_WRITE_U64(reinterpret_cast(obj.data)); + GX_WRITE_U32(static_cast(obj.format)); + GX_WRITE_U16(obj.numEntries); + GX_WRITE_U32(obj.tlutObjId); + GX_WRITE_U32(obj.tlutDataVersion); +} +} // namespace + +extern "C" { +void GXInitTexObj(GXTexObj* obj_, const void* data, u16 width, u16 height, GXTexFmt format, GXTexWrapMode wrapS, + GXTexWrapMode wrapT, GXBool mipmap) { + auto* obj = reinterpret_cast(obj_); + init_texobj_common(*obj, data, width, height, format, wrapS, wrapT, mipmap); +} + +void GXInitTexObjCI(GXTexObj* obj_, const void* data, u16 width, u16 height, GXCITexFmt format, GXTexWrapMode wrapS, + GXTexWrapMode wrapT, GXBool mipmap, u32 tlut) { + auto* obj = reinterpret_cast(obj_); + init_texobj_common(*obj, data, width, height, format, wrapS, wrapT, mipmap); + obj->tlut = static_cast(tlut); + obj->flags &= ~2u; +} + +void GXInitTexObjLOD(GXTexObj* obj_, GXTexFilter minFilt, GXTexFilter magFilt, float minLod, float maxLod, + float lodBias, GXBool biasClamp, GXBool doEdgeLod, GXAnisotropy maxAniso) { + auto* obj = reinterpret_cast(obj_); + const float clampedBias = std::clamp(lodBias, -4.0f, 3.99f); + const auto lbias = static_cast(32.0f * clampedBias); + SET_REG_FIELD(0, obj->mode0, 8, 9, lbias); + SET_REG_FIELD(0, obj->mode0, 1, 4, magFilt == GX_LINEAR ? 1 : 0); + SET_REG_FIELD(0, obj->mode0, 3, 5, GX2HWFiltConv[minFilt]); + SET_REG_FIELD(0, obj->mode0, 1, 8, doEdgeLod ? 0 : 1); + obj->mode0 &= 0xFFFDFFFF; + obj->mode0 &= 0xFFFBFFFF; + SET_REG_FIELD(0, obj->mode0, 2, 19, maxAniso); + SET_REG_FIELD(0, obj->mode0, 1, 21, biasClamp); + + const auto clampedMin = std::clamp(minLod, 0.0f, 10.0f); + const auto clampedMax = std::clamp(maxLod, 0.0f, 10.0f); + SET_REG_FIELD(0, obj->mode1, 8, 0, static_cast(16.0f * clampedMin)); + SET_REG_FIELD(0, obj->mode1, 8, 8, static_cast(16.0f * clampedMax)); +} + +void GXInitTexObjData(GXTexObj* obj_, const void* data) { + auto* obj = reinterpret_cast(obj_); + obj->data = data; + ++obj->texDataVersion; +} + +void GXInitTexObjWrapMode(GXTexObj* obj_, GXTexWrapMode wrapS, GXTexWrapMode wrapT) { + auto* obj = reinterpret_cast(obj_); + SET_REG_FIELD(0, obj->mode0, 2, 0, wrapS); + SET_REG_FIELD(0, obj->mode0, 2, 2, wrapT); +} + +void GXInitTexObjTlut(GXTexObj* obj_, u32 tlut) { + auto* obj = reinterpret_cast(obj_); + obj->tlut = static_cast(tlut); +} + +void GXInitTexObjFilter(GXTexObj* obj_, GXTexFilter minFilt, GXTexFilter magFilt) { + auto* obj = reinterpret_cast(obj_); + SET_REG_FIELD(0, obj->mode0, 3, 5, GX2HWFiltConv[minFilt]); + SET_REG_FIELD(0, obj->mode0, 1, 4, magFilt == GX_LINEAR ? 1 : 0); +} + +void GXInitTexObjMaxLOD(GXTexObj* obj_, float maxLod) { + auto* obj = reinterpret_cast(obj_); + const auto clampedMax = std::clamp(maxLod, 0.0f, 10.0f); + SET_REG_FIELD(0, obj->mode1, 8, 8, static_cast(16.0f * clampedMax)); +} + +void GXInitTexObjMinLOD(GXTexObj* obj_, float minLod) { + auto* obj = reinterpret_cast(obj_); + const auto clampedMin = std::clamp(minLod, 0.0f, 10.0f); + SET_REG_FIELD(0, obj->mode1, 8, 0, static_cast(16.0f * clampedMin)); +} + +void GXInitTexObjLODBias(GXTexObj* obj_, float lodBias) { + auto* obj = reinterpret_cast(obj_); + const float clampedBias = std::clamp(lodBias, -4.0f, 3.99f); + const auto lbias = static_cast(32.0f * clampedBias); + SET_REG_FIELD(0, obj->mode0, 8, 9, lbias); +} + +void GXInitTexObjBiasClamp(GXTexObj* obj_, GXBool biasClamp) { + auto* obj = reinterpret_cast(obj_); + SET_REG_FIELD(0, obj->mode0, 1, 21, biasClamp); +} + +void GXInitTexObjEdgeLOD(GXTexObj* obj_, GXBool doEdgeLod) { + auto* obj = reinterpret_cast(obj_); + SET_REG_FIELD(0, obj->mode0, 1, 8, doEdgeLod ? 0 : 1); +} + +void GXInitTexObjMaxAniso(GXTexObj* obj_, GXAnisotropy maxAniso) { + auto* obj = reinterpret_cast(obj_); + SET_REG_FIELD(0, obj->mode0, 2, 19, maxAniso); +} + +void GXInitTexObjUserData(GXTexObj* obj_, void* userData) { + auto* obj = reinterpret_cast(obj_); + obj->userData = userData; +} + +void* GXGetTexObjUserData(const GXTexObj* obj_) { + const auto* obj = reinterpret_cast(obj_); + return const_cast(obj->userData); +} + +void GXLoadTexObj(GXTexObj* obj_, GXTexMapID id) { + auto* obj = reinterpret_cast(obj_); + SET_REG_FIELD(0, obj->mode0, 8, 24, GXTexMode0Ids[id]); + SET_REG_FIELD(0, obj->mode1, 8, 24, GXTexMode1Ids[id]); + SET_REG_FIELD(0, obj->image0, 8, 24, GXTexImage0Ids[id]); + + u32 image1 = 0; + u32 image2 = 0; + SET_REG_FIELD(0, image1, 8, 24, GXTexImage1Ids[id]); + SET_REG_FIELD(0, image2, 8, 24, GXTexImage2Ids[id]); + SET_REG_FIELD(0, obj->image3, 8, 24, GXTexImage3Ids[id]); + + GX_WRITE_RAS_REG(obj->mode0); + GX_WRITE_RAS_REG(obj->mode1); + GX_WRITE_RAS_REG(obj->image0); + GX_WRITE_RAS_REG(image1); + GX_WRITE_RAS_REG(image2); + GX_WRITE_RAS_REG(obj->image3); + + if ((obj->flags & 2) == 0) { + u32 tlut = 0; + SET_REG_FIELD(0, tlut, 10, 0, static_cast(obj->tlut)); + SET_REG_FIELD(0, tlut, 8, 24, GXTexTlutIds[id]); + GX_WRITE_RAS_REG(tlut); + } + + __gx->tImage0[id] = obj->image0; + __gx->tMode0[id] = obj->mode0; + __gx->dirtyState |= 1; + __gx->bpSent = 1; + + emit_loaded_texobj_metadata(*obj, id); +} + +u32 GXGetTexBufferSize(u16 width, u16 height, u32 fmt, GXBool mips, u8 maxLod) { + if (fmt == GX_TF_R8_PC || fmt == GX_TF_RGBA8_PC) { + const u32 bytesPerPixel = fmt == GX_TF_R8_PC ? 1u : 4u; + const u32 mipCount = mips ? static_cast(maxLod) + 1u : 1u; + u64 byteCount = 0; + for (u32 mip = 0; mip < mipCount; ++mip) { + byteCount += static_cast(width) * height * bytesPerPixel; + if (width == 1 && height == 1) { + break; + } + width = std::max(width / 2, 1); + height = std::max(height / 2, 1); + } + CHECK(byteCount <= UINT32_MAX, "GXGetTexBufferSize: linear PC texture is too large ({} bytes)", byteCount); + return static_cast(byteCount); + } + + s32 shiftX = 0; + s32 shiftY = 0; + switch (fmt) { + case GX_TF_I4: + case GX_TF_C4: + case GX_TF_CMPR: + case GX_CTF_R4: + case GX_CTF_Z4: + shiftX = 3; + shiftY = 3; + break; + case GX_TF_I8: + case GX_TF_IA4: + case GX_TF_C8: + case GX_TF_Z8: + case GX_CTF_RA4: + case GX_CTF_A8: + case GX_CTF_R8: + case GX_CTF_G8: + case GX_CTF_B8: + case GX_CTF_Z8M: + case GX_CTF_Z8L: + shiftX = 3; + shiftY = 2; + break; + case GX_TF_IA8: + case GX_TF_RGB565: + case GX_TF_RGB5A3: + case GX_TF_RGBA8: + case GX_TF_C14X2: + case GX_TF_Z16: + case GX_TF_Z24X8: + case GX_CTF_RA8: + case GX_CTF_RG8: + case GX_CTF_GB8: + case GX_CTF_Z16L: + shiftX = 2; + shiftY = 2; + break; + default: + break; + } + u32 bitSize = fmt == GX_TF_RGBA8 || fmt == GX_TF_Z24X8 ? 64 : 32; + u32 bufLen = 0; + if (mips) { + const u32 mipCount = static_cast(maxLod) + 1; + for (u32 mip = 0; mip < mipCount; ++mip) { + const u32 tileX = ((width + (1 << shiftX) - 1) >> shiftX); + const u32 tileY = ((height + (1 << shiftY) - 1) >> shiftY); + bufLen += bitSize * tileX * tileY; + + if (width == 1 && height == 1) { + return bufLen; + } + + width = (width < 2) ? 1 : width / 2; + height = (height < 2) ? 1 : height / 2; + } + } else { + const u32 tileX = ((width + (1 << shiftX) - 1) >> shiftX); + const u32 tileY = ((height + (1 << shiftY) - 1) >> shiftY); + bufLen = bitSize * tileX * tileY; + } + + return bufLen; +} + +void GXInitTlutObj(GXTlutObj* obj_, const void* data, GXTlutFmt format, u16 entries) { + memset(obj_, 0, sizeof(GXTlutObj)); + auto* obj = reinterpret_cast(obj_); + obj->data = data; + obj->format = format; + obj->numEntries = entries; + const auto [tlutObjId, tlutDataVersion] = resolve_tlut_identity(data, format, entries); + obj->tlutObjId = tlutObjId; + obj->tlutDataVersion = tlutDataVersion; + + SET_REG_FIELD(0, obj->tlut, 2, 10, format); + SET_REG_FIELD(0, obj->loadTlut0, 8, 24, 0x64); + aurora::gfx::texture_replacement::register_tlut(obj_, data, format, entries); +} + +void GXInitTlutObjData(GXTlutObj* obj_, const void* data) { + auto* obj = reinterpret_cast(obj_); + obj->data = data; + obj->tlutDataVersion = next_repointed_tlut_data_version(); +} + +void GXLoadTlut(const GXTlutObj* obj_, u32 idx) { + auto* obj = reinterpret_cast(obj_); + __GXFlushTextureState(); + GX_WRITE_RAS_REG(obj->loadTlut0); + + u32 loadTlut1 = 0; + SET_REG_FIELD(0, loadTlut1, 10, 0, idx); + SET_REG_FIELD(0, loadTlut1, 10, 10, obj->numEntries - 1); + SET_REG_FIELD(0, loadTlut1, 8, 24, 0x65); + GX_WRITE_RAS_REG(loadTlut1); + __GXFlushTextureState(); + + aurora::gfx::texture_replacement::load_tlut(obj_, idx); + emit_loaded_tlut_metadata(*obj, idx); +} + +// TODO GXInitTexCacheRegion +// TODO GXInitTexPreLoadRegion +// TODO GXInitTlutRegion +// TODO GXInvalidateTexRegion + +void GXInvalidateTexAll() { + GX_WRITE_AURORA(GX_LOAD_AURORA_INVALIDATE_TEX_ALL); +} + +// TODO GXPreLoadEntireTexture +// TODO GXSetTexRegionCallback +// TODO GXSetTlutRegionCallback +// TODO GXLoadTexObjPreLoaded +void GXSetTexCoordScaleManually(GXTexCoordID coord, GXBool enable, u16 ss, u16 ts) { + __gx->tcsManEnab = (__gx->tcsManEnab & ~(1 << coord)) | (enable << coord); + if (enable) { + SET_REG_FIELD(0, __gx->suTs0[coord], 16, 0, static_cast(ss - 1)); + SET_REG_FIELD(0, __gx->suTs1[coord], 16, 0, static_cast(ts - 1)); + GX_WRITE_RAS_REG(__gx->suTs0[coord]); + GX_WRITE_RAS_REG(__gx->suTs1[coord]); + __gx->bpSent = 1; + } +} + +void GXSetTexCoordCylWrap(GXTexCoordID coord, GXBool s_enable, GXBool t_enable) { + SET_REG_FIELD(0, __gx->suTs0[coord], 1, 17, s_enable); + SET_REG_FIELD(0, __gx->suTs1[coord], 1, 17, t_enable); + if (__gx->tcsManEnab & (1 << coord)) { + GX_WRITE_RAS_REG(__gx->suTs0[coord]); + GX_WRITE_RAS_REG(__gx->suTs1[coord]); + __gx->bpSent = 1; + } +} + +void GXSetTexCoordBias(GXTexCoordID coord, GXBool s_enable, GXBool t_enable) { + SET_REG_FIELD(0, __gx->suTs0[coord], 1, 16, s_enable); + SET_REG_FIELD(0, __gx->suTs1[coord], 1, 16, t_enable); + if (__gx->tcsManEnab & (1 << coord)) { + GX_WRITE_RAS_REG(__gx->suTs0[coord]); + GX_WRITE_RAS_REG(__gx->suTs1[coord]); + __gx->bpSent = 1; + } +} + +void __GXFlushTextureState() { + GX_WRITE_RAS_REG(__gx->bpMask); + __gx->bpSent = 1; +} +} diff --git a/aurora-main/lib/dolphin/gx/GXTransform.cpp b/aurora-main/lib/dolphin/gx/GXTransform.cpp new file mode 100644 index 0000000..b881d38 --- /dev/null +++ b/aurora-main/lib/dolphin/gx/GXTransform.cpp @@ -0,0 +1,216 @@ +#include "gx.hpp" +#include "__gx.h" +#include "dolphin/mtx/GeoTypes.h" + +static inline void CacheProjectionVector(const f32* ptr, GXProjectionType type) { + __gx->projType = type; + for (int i = 0; i < 6; ++i) { + __gx->projMtx[i] = ptr[i + 1]; + } +} + +extern "C" { + +void GXSetProjection(const void* mtx_, GXProjectionType type) { + const auto& mtx = *reinterpret_cast*>(mtx_); + const f32 projVec[] = { + static_cast(type == GX_ORTHOGRAPHIC), + mtx[0][0], + type == GX_ORTHOGRAPHIC ? mtx[0][3] : mtx[0][2], + mtx[1][1], + type == GX_ORTHOGRAPHIC ? mtx[1][3] : mtx[1][2], + mtx[2][2], + mtx[2][3], + }; + CacheProjectionVector(projVec, type); + + // XF bulk write: 6 params + projection type at 0x1020-0x1026 + GX_WRITE_U8(0x10); + GX_WRITE_U32(0x00061020); + GX_WRITE_XF_REG_F(32, __gx->projMtx[0]); + GX_WRITE_XF_REG_F(33, __gx->projMtx[1]); + GX_WRITE_XF_REG_F(34, __gx->projMtx[2]); + GX_WRITE_XF_REG_F(35, __gx->projMtx[3]); + GX_WRITE_XF_REG_F(36, __gx->projMtx[4]); + GX_WRITE_XF_REG_F(37, __gx->projMtx[5]); + GX_WRITE_XF_REG_2(38, __gx->projType); + __gx->bpSent = 0; +} + +void GXSetProjectionv(const f32* ptr) { + CHECK(ptr != nullptr, "null projection vector"); + + const GXProjectionType type = ptr[0] == 0.0f ? GX_PERSPECTIVE : GX_ORTHOGRAPHIC; + CacheProjectionVector(ptr, type); + + // XF bulk write: 6 params + projection type at 0x1020-0x1026 + GX_WRITE_U8(0x10); + GX_WRITE_U32(0x00061020); + for (int i = 0; i < 6; ++i) { + GX_WRITE_F32(__gx->projMtx[i]); + } + GX_WRITE_U32(__gx->projType); + __gx->bpSent = 0; +} + +void GXLoadPosMtxImm(const void* mtx_, u32 id) { + CHECK(id >= GX_PNMTX0 && id <= GX_PNMTX9, "invalid pn mtx {}", static_cast(id)); + const auto* mtx = reinterpret_cast(mtx_); + + GX_WRITE_U8(0x10); + GX_WRITE_U32((id * 4) | 0xB0000); + for (int i = 0; i < 12; i++) { + GX_WRITE_F32(mtx[i]); + } +} + +void GXLoadNrmMtxImm(const void* mtx_, u32 id) { + CHECK(id >= GX_PNMTX0 && id <= GX_PNMTX9, "invalid pn mtx {}", static_cast(id)); + const auto* mtx = reinterpret_cast(mtx_); + + GX_WRITE_U8(0x10); + GX_WRITE_U32((id * 3 + 0x400) | 0x80000); + // Write 3x3 from 3x4 matrix (skip translation column) + GX_WRITE_F32(mtx[0]); + GX_WRITE_F32(mtx[1]); + GX_WRITE_F32(mtx[2]); + GX_WRITE_F32(mtx[4]); + GX_WRITE_F32(mtx[5]); + GX_WRITE_F32(mtx[6]); + GX_WRITE_F32(mtx[8]); + GX_WRITE_F32(mtx[9]); + GX_WRITE_F32(mtx[10]); +} + +void GXSetCurrentMtx(u32 id) { + CHECK(id >= GX_PNMTX0 && id <= GX_PNMTX9, "invalid pn mtx {}", id); + SET_REG_FIELD(0, __gx->matIdxA, 6, 0, id); + __GXSetMatrixIndex(GX_VA_PNMTXIDX); +} + +void GXLoadTexMtxImm(const void* mtx_, u32 id, GXTexMtxType type) { + CHECK((id >= GX_TEXMTX0 && id <= GX_IDENTITY) || (id >= GX_PTTEXMTX0 && id <= GX_PTIDENTITY), "invalid tex mtx {}", + id); + + u32 addr; + if (id >= GX_PTTEXMTX0) { + addr = (id - GX_PTTEXMTX0) * 4 + 0x500; + CHECK(type == GX_MTX3x4, "invalid pt mtx type {}", underlying(type)); + } else { + addr = id * 4; + } + + u32 count = (type == GX_MTX2x4) ? 8 : 12; + u32 reg = addr | ((count - 1) << 16); + + GX_WRITE_U8(0x10); + GX_WRITE_U32(reg); + + const auto* mtx = reinterpret_cast(mtx_); + for (u32 i = 0; i < count; i++) { + GX_WRITE_F32(mtx[i]); + } +} + +void GXSetViewport(float left, float top, float width, float height, float nearZ, float farZ) { + GXSetViewportJitter(left, top, width, height, nearZ, farZ, 1); +} + +void GXSetViewportJitter(float left, float top, float width, float height, float nearZ, float farZ, u32 field) { + float sx; + float sy; + float sz; + float ox; + float oy; + float oz; + float zmin; + float zmax; + + if (field == 0) { + top -= 0.5f; + } + + sx = width / 2.0f; + sy = -height / 2.0f; + ox = 340.0f + (left + width / 2.0f); + oy = 340.0f + (top + height / 2.0f); + zmin = 1.6777215e7f * nearZ; + zmax = 1.6777215e7f * farZ; + sz = zmax - zmin; + oz = zmax; + + __gx->vpLeft = left; + __gx->vpTop = top; + __gx->vpWd = width; + __gx->vpHt = height; + __gx->vpNearz = nearZ; + __gx->vpFarz = farZ; + + GX_WRITE_U8(0x10); + GX_WRITE_U32(0x0005101A); + GX_WRITE_XF_REG_F(26, sx); + GX_WRITE_XF_REG_F(27, sy); + GX_WRITE_XF_REG_F(28, sz); + GX_WRITE_XF_REG_F(29, ox); + GX_WRITE_XF_REG_F(30, oy); + GX_WRITE_XF_REG_F(31, oz); + __gx->bpSent = 0; +} + +void GXProject(f32 x, f32 y, f32 z, const f32 mtx[3][4], const f32* pm, const f32* vp, f32* sx, + f32* sy, f32* sz) { + Vec peye; + f32 xc; + f32 yc; + f32 zc; + f32 wc; + + peye.x = mtx[0][3] + ((mtx[0][2] * z) + ((mtx[0][0] * x) + (mtx[0][1] * y))); + peye.y = mtx[1][3] + ((mtx[1][2] * z) + ((mtx[1][0] * x) + (mtx[1][1] * y))); + peye.z = mtx[2][3] + ((mtx[2][2] * z) + ((mtx[2][0] * x) + (mtx[2][1] * y))); + if (pm[0] == 0.0f) { + xc = (peye.x * pm[1]) + (peye.z * pm[2]); + yc = (peye.y * pm[3]) + (peye.z * pm[4]); + zc = pm[6] + (peye.z * pm[5]); + wc = 1.0f / -peye.z; + } else { + xc = pm[2] + (peye.x * pm[1]); + yc = pm[4] + (peye.y * pm[3]); + zc = pm[6] + (peye.z * pm[5]); + wc = 1.0f; + } + *sx = (vp[2] / 2.0f) + (vp[0] + (wc * (xc * vp[2] / 2.0f))); + *sy = (vp[3] / 2.0f) + (vp[1] + (wc * (-yc * vp[3] / 2.0f))); + *sz = vp[5] + (wc * (zc * (vp[5] - vp[4]))); +} + +// TODO GXLoadPosMtxIndx +// TODO GXLoadNrmMtxImm3x3 +// TODO GXLoadNrmMtxIndx3x3 +// TODO GXLoadTexMtxIndx +void GXSetZScaleOffset(f32 scale, f32 offset) { + g_gxState.zScale = scale; + g_gxState.zOffset = offset; + constexpr f32 z24Scale = 16777215.0f; + const f32 sz = z24Scale * offset; + const f32 oz = 1.0f + z24Scale * scale; + + GX_WRITE_U8(0x10); + GX_WRITE_U32(0x101c); + GX_WRITE_F32(sz); + GX_WRITE_U8(0x10); + GX_WRITE_U32(0x101f); + GX_WRITE_F32(oz); + __gx->bpSent = 0; +} + +void GXSetScissorBoxOffset(s32 x_off, s32 y_off) { + g_gxState.scissorOffsetX = x_off; + g_gxState.scissorOffsetY = y_off; + + const u32 reg = 0x59000000u | (((static_cast(y_off + 0x156) * 0x200u) & 0x000ffc00u)) | + ((static_cast(x_off + 0x156) >> 1) & 0x000003ffu); + GX_WRITE_RAS_REG(reg); + __gx->bpSent = 0; +} +} diff --git a/aurora-main/lib/dolphin/gx/GXVert.cpp b/aurora-main/lib/dolphin/gx/GXVert.cpp new file mode 100644 index 0000000..6f71945 --- /dev/null +++ b/aurora-main/lib/dolphin/gx/GXVert.cpp @@ -0,0 +1,733 @@ +#include "gx.hpp" +#include "__gx.h" +#include "../../gx/fifo.hpp" + +#include +#include +#include +#include +#include + +// Track vertex count between GXBegin/GXEnd for mismatch detection +static u16 sBeginNVerts = 0; +static u32 sBeginFifoSize = 0; +static GXPrimitive sBeginPrimitive = GX_TRIANGLES; +static GXVtxFmt sBeginVtxFmt = GX_VTXFMT0; +static bool sInBegin = false; +static GXAttr sWriteAttr = GX_VA_NULL; +static u32 sWriteAttrComps = 0; + +namespace { + +static u8 normal_frac_bits(GXCompType type) { + switch (type) { + case GX_U8: + return 7; + case GX_S8: + return 6; + case GX_U16: + return 15; + case GX_S16: + return 14; + default: + return 0; + } +} + +struct ActiveAttrFmt { + GXCompCnt cnt = GX_POS_XYZ; + GXCompType type = GX_F32; + u8 frac = 0; +}; + +enum class ScalarKind { + Float, + Signed, + Unsigned, +}; + +struct Scalar { + ScalarKind kind = ScalarKind::Float; + float f = 0.0f; + int32_t s = 0; + uint32_t u = 0; +}; + +static Scalar scalar_f32(float value) { + return Scalar{.kind = ScalarKind::Float, .f = value, .s = static_cast(value), .u = static_cast(value)}; +} + +static Scalar scalar_u32(uint32_t value) { + return Scalar{.kind = ScalarKind::Unsigned, .f = static_cast(value), .s = static_cast(value), .u = value}; +} + +static Scalar scalar_s32(int32_t value) { + return Scalar{.kind = ScalarKind::Signed, .f = static_cast(value), .s = value, .u = static_cast(value)}; +} + +static bool is_matrix_index_attr(GXAttr attr) { + return attr >= GX_VA_PNMTXIDX && attr <= GX_VA_TEX7MTXIDX; +} + +static bool is_color_attr(GXAttr attr) { + return attr == GX_VA_CLR0 || attr == GX_VA_CLR1; +} + +static bool is_tex_attr(GXAttr attr) { + return attr >= GX_VA_TEX0 && attr <= GX_VA_TEX7; +} + +static GXAttrType active_attr_type(GXAttr attr) { + switch (attr) { + case GX_VA_PNMTXIDX: + case GX_VA_TEX0MTXIDX: + case GX_VA_TEX1MTXIDX: + case GX_VA_TEX2MTXIDX: + case GX_VA_TEX3MTXIDX: + case GX_VA_TEX4MTXIDX: + case GX_VA_TEX5MTXIDX: + case GX_VA_TEX6MTXIDX: + case GX_VA_TEX7MTXIDX: + return GET_REG_FIELD(__gx->vcdLo, 1, underlying(attr)) ? GX_DIRECT : GX_NONE; + case GX_VA_POS: + return static_cast(GET_REG_FIELD(__gx->vcdLo, 2, 9)); + case GX_VA_NRM: + return (__gx->hasNrms || __gx->hasBiNrms) ? static_cast(GET_REG_FIELD(__gx->vcdLo, 2, 11)) : GX_NONE; + case GX_VA_CLR0: + return static_cast(GET_REG_FIELD(__gx->vcdLo, 2, 13)); + case GX_VA_CLR1: + return static_cast(GET_REG_FIELD(__gx->vcdLo, 2, 15)); + case GX_VA_TEX0: + return static_cast(GET_REG_FIELD(__gx->vcdHi, 2, 0)); + case GX_VA_TEX1: + return static_cast(GET_REG_FIELD(__gx->vcdHi, 2, 2)); + case GX_VA_TEX2: + return static_cast(GET_REG_FIELD(__gx->vcdHi, 2, 4)); + case GX_VA_TEX3: + return static_cast(GET_REG_FIELD(__gx->vcdHi, 2, 6)); + case GX_VA_TEX4: + return static_cast(GET_REG_FIELD(__gx->vcdHi, 2, 8)); + case GX_VA_TEX5: + return static_cast(GET_REG_FIELD(__gx->vcdHi, 2, 10)); + case GX_VA_TEX6: + return static_cast(GET_REG_FIELD(__gx->vcdHi, 2, 12)); + case GX_VA_TEX7: + return static_cast(GET_REG_FIELD(__gx->vcdHi, 2, 14)); + default: + return GX_NONE; + } +} + +static ActiveAttrFmt active_attr_fmt(GXVtxFmt fmt, GXAttr attr) { + ActiveAttrFmt out{}; + const u32 va = __gx->vatA[fmt]; + const u32 vb = __gx->vatB[fmt]; + const u32 vc = __gx->vatC[fmt]; + + switch (attr) { + case GX_VA_POS: + out.cnt = GET_REG_FIELD(va, 1, 0) ? GX_POS_XYZ : GX_POS_XY; + out.type = static_cast(GET_REG_FIELD(va, 3, 1)); + out.frac = static_cast(GET_REG_FIELD(va, 5, 4)); + break; + case GX_VA_NRM: + out.type = static_cast(GET_REG_FIELD(va, 3, 10)); + if (GET_REG_FIELD(va, 1, 31)) { + out.cnt = GX_NRM_NBT3; + } else { + out.cnt = GET_REG_FIELD(va, 1, 9) ? GX_NRM_NBT : GX_NRM_XYZ; + } + out.frac = normal_frac_bits(out.type); + break; + case GX_VA_CLR0: + out.cnt = GET_REG_FIELD(va, 1, 13) ? GX_CLR_RGBA : GX_CLR_RGB; + out.type = static_cast(GET_REG_FIELD(va, 3, 14)); + out.frac = 0; + break; + case GX_VA_CLR1: + out.cnt = GET_REG_FIELD(va, 1, 17) ? GX_CLR_RGBA : GX_CLR_RGB; + out.type = static_cast(GET_REG_FIELD(va, 3, 18)); + out.frac = 0; + break; + case GX_VA_TEX0: + out.cnt = GET_REG_FIELD(va, 1, 21) ? GX_TEX_ST : GX_TEX_S; + out.type = static_cast(GET_REG_FIELD(va, 3, 22)); + out.frac = static_cast(GET_REG_FIELD(va, 5, 25)); + break; + case GX_VA_TEX1: + out.cnt = GET_REG_FIELD(vb, 1, 0) ? GX_TEX_ST : GX_TEX_S; + out.type = static_cast(GET_REG_FIELD(vb, 3, 1)); + out.frac = static_cast(GET_REG_FIELD(vb, 5, 4)); + break; + case GX_VA_TEX2: + out.cnt = GET_REG_FIELD(vb, 1, 9) ? GX_TEX_ST : GX_TEX_S; + out.type = static_cast(GET_REG_FIELD(vb, 3, 10)); + out.frac = static_cast(GET_REG_FIELD(vb, 5, 13)); + break; + case GX_VA_TEX3: + out.cnt = GET_REG_FIELD(vb, 1, 18) ? GX_TEX_ST : GX_TEX_S; + out.type = static_cast(GET_REG_FIELD(vb, 3, 19)); + out.frac = static_cast(GET_REG_FIELD(vb, 5, 22)); + break; + case GX_VA_TEX4: + out.cnt = GET_REG_FIELD(vb, 1, 27) ? GX_TEX_ST : GX_TEX_S; + out.type = static_cast(GET_REG_FIELD(vb, 3, 28)); + out.frac = static_cast(GET_REG_FIELD(vc, 5, 0)); + break; + case GX_VA_TEX5: + out.cnt = GET_REG_FIELD(vc, 1, 5) ? GX_TEX_ST : GX_TEX_S; + out.type = static_cast(GET_REG_FIELD(vc, 3, 6)); + out.frac = static_cast(GET_REG_FIELD(vc, 5, 9)); + break; + case GX_VA_TEX6: + out.cnt = GET_REG_FIELD(vc, 1, 14) ? GX_TEX_ST : GX_TEX_S; + out.type = static_cast(GET_REG_FIELD(vc, 3, 15)); + out.frac = static_cast(GET_REG_FIELD(vc, 5, 18)); + break; + case GX_VA_TEX7: + out.cnt = GET_REG_FIELD(vc, 1, 23) ? GX_TEX_ST : GX_TEX_S; + out.type = static_cast(GET_REG_FIELD(vc, 3, 24)); + out.frac = static_cast(GET_REG_FIELD(vc, 5, 27)); + break; + default: + break; + } + return out; +} + +static u32 expected_component_count(GXAttr attr, const ActiveAttrFmt& fmt, GXAttrType type) { + if (is_matrix_index_attr(attr) || is_color_attr(attr)) { + return 1; + } + if (type == GX_INDEX8 || type == GX_INDEX16) { + return (attr == GX_VA_NRM && fmt.cnt == GX_NRM_NBT3) ? 3u : 1u; + } + if (attr == GX_VA_POS) { + return fmt.cnt == GX_POS_XY ? 2u : 3u; + } + if (attr == GX_VA_NRM) { + return (fmt.cnt == GX_NRM_NBT || fmt.cnt == GX_NRM_NBT3) ? 9u : 3u; + } + if (is_tex_attr(attr)) { + return fmt.cnt == GX_TEX_S ? 1u : 2u; + } + return 1; +} + + // Cache decoded vertex settings and refresh them when their GX registers change. +struct AttrCacheEntry { + ActiveAttrFmt fmt{}; + GXAttrType type = GX_NONE; + u32 expected = 1; + GXAttr next = GX_VA_NULL; +}; + +struct VtxStateCache { + bool valid = false; + GXVtxFmt vtxFmt = GX_VTXFMT0; + u32 vcdLo = 0; + u32 vcdHi = 0; + u32 vatA = 0; + u32 vatB = 0; + u32 vatC = 0; + u8 hasNrms = 0; + u8 hasBiNrms = 0; + GXAttr first = GX_VA_NULL; + std::array attrs{}; +}; + +static VtxStateCache sVtxCache; + +static void rebuild_vtx_cache(GXVtxFmt vtxFmt) { + sVtxCache.valid = true; + sVtxCache.vtxFmt = vtxFmt; + sVtxCache.vcdLo = __gx->vcdLo; + sVtxCache.vcdHi = __gx->vcdHi; + sVtxCache.vatA = __gx->vatA[vtxFmt]; + sVtxCache.vatB = __gx->vatB[vtxFmt]; + sVtxCache.vatC = __gx->vatC[vtxFmt]; + sVtxCache.hasNrms = __gx->hasNrms; + sVtxCache.hasBiNrms = __gx->hasBiNrms; + + for (int i = 0; i < GX_VA_MAX_ATTR; ++i) { + const auto attr = static_cast(i); + AttrCacheEntry& entry = sVtxCache.attrs[i]; + entry.type = active_attr_type(attr); + entry.fmt = active_attr_fmt(vtxFmt, attr); + entry.expected = expected_component_count(attr, entry.fmt, entry.type); + entry.next = GX_VA_NULL; + } + + GXAttr running = GX_VA_NULL; + for (int i = GX_VA_TEX7; i >= GX_VA_PNMTXIDX; --i) { + sVtxCache.attrs[i].next = running; + if (sVtxCache.attrs[i].type != GX_NONE) { + running = static_cast(i); + } + } + sVtxCache.first = running; +} + +static inline void ensure_vtx_cache(GXVtxFmt vtxFmt) { + if (sVtxCache.valid && sVtxCache.vtxFmt == vtxFmt && sVtxCache.vcdLo == __gx->vcdLo && + sVtxCache.vcdHi == __gx->vcdHi && sVtxCache.vatA == __gx->vatA[vtxFmt] && + sVtxCache.vatB == __gx->vatB[vtxFmt] && sVtxCache.vatC == __gx->vatC[vtxFmt] && + sVtxCache.hasNrms == __gx->hasNrms && sVtxCache.hasBiNrms == __gx->hasBiNrms) { + return; + } + rebuild_vtx_cache(vtxFmt); +} + +static inline const AttrCacheEntry& attr_cache(GXAttr attr) { + static const AttrCacheEntry kDisabled{}; + const auto idx = static_cast(attr); + return idx < GX_VA_MAX_ATTR ? sVtxCache.attrs[idx] : kDisabled; +} + +static void reset_attr_writer() { + sWriteAttr = sVtxCache.first; + sWriteAttrComps = 0; +} + +static GXAttr select_implicit_attr(GXAttr first, GXAttr last) { + if (sInBegin && sWriteAttr >= first && sWriteAttr <= last && attr_cache(sWriteAttr).type != GX_NONE) { + return sWriteAttr; + } + if (sInBegin) { + for (int i = static_cast(sWriteAttr); i <= static_cast(last); ++i) { + const auto attr = static_cast(i); + if (attr >= first && attr_cache(attr).type != GX_NONE) { + sWriteAttr = attr; + sWriteAttrComps = 0; + return attr; + } + } + } + for (int i = static_cast(first); i <= static_cast(last); ++i) { + const auto attr = static_cast(i); + if (attr_cache(attr).type != GX_NONE) { + sWriteAttr = attr; + sWriteAttrComps = 0; + return attr; + } + } + return first; +} + +static void align_explicit_attr(GXAttr attr) { + if (!sInBegin || sWriteAttr == attr) { + return; + } + sWriteAttr = attr; + sWriteAttrComps = 0; +} + +static void advance_attr(GXAttr attr, u32 compsWritten) { + if (!sInBegin) { + return; + } + const AttrCacheEntry& entry = attr_cache(attr); + sWriteAttrComps += compsWritten; + if (sWriteAttrComps < entry.expected) { + return; + } + sWriteAttr = entry.next; + sWriteAttrComps = 0; + if (sWriteAttr == GX_VA_NULL) { + reset_attr_writer(); + } +} + +static int64_t encode_float_component(float value, bool isSigned, u8 bits, u8 frac) { + if (!std::isfinite(value)) { + value = 0.0f; + } + const double scaled = static_cast(value) * static_cast(uint64_t{1} << frac); + const double rounded = std::round(scaled); + if (isSigned) { + const int64_t minVal = -(int64_t{1} << (bits - 1)); + const int64_t maxVal = (int64_t{1} << (bits - 1)) - 1; + return std::clamp(static_cast(rounded), minVal, maxVal); + } + const int64_t maxVal = (int64_t{1} << bits) - 1; + return std::clamp(static_cast(rounded), int64_t{0}, maxVal); +} + +static int64_t scalar_to_signed_raw(const Scalar& value, u8 bits, u8 frac) { + if (value.kind == ScalarKind::Float) { + return encode_float_component(value.f, true, bits, frac); + } + const int64_t raw = value.kind == ScalarKind::Signed ? value.s : static_cast(value.u); + const int64_t minVal = -(int64_t{1} << (bits - 1)); + const int64_t maxVal = (int64_t{1} << (bits - 1)) - 1; + return std::clamp(raw, minVal, maxVal); +} + +static uint64_t scalar_to_unsigned_raw(const Scalar& value, u8 bits, u8 frac) { + if (value.kind == ScalarKind::Float) { + return static_cast(encode_float_component(value.f, false, bits, frac)); + } + const uint64_t raw = value.kind == ScalarKind::Signed ? static_cast(std::max(value.s, int32_t{0})) : value.u; + const uint64_t maxVal = (uint64_t{1} << bits) - 1; + return std::min(raw, maxVal); +} + +static float scalar_to_float(const Scalar& value) { + if (value.kind == ScalarKind::Float) { + return value.f; + } + if (value.kind == ScalarKind::Signed) { + return static_cast(value.s); + } + return static_cast(value.u); +} + +static void write_numeric_component(const ActiveAttrFmt& fmt, const Scalar& value) { + switch (fmt.type) { + case GX_U8: + GX_WRITE_U8(static_cast(scalar_to_unsigned_raw(value, 8, fmt.frac))); + break; + case GX_S8: + GX_WRITE_U8(static_cast(static_cast(scalar_to_signed_raw(value, 8, fmt.frac)))); + break; + case GX_U16: + GX_WRITE_U16(static_cast(scalar_to_unsigned_raw(value, 16, fmt.frac))); + break; + case GX_S16: + GX_WRITE_U16(static_cast(static_cast(scalar_to_signed_raw(value, 16, fmt.frac)))); + break; + case GX_F32: + default: + GX_WRITE_F32(scalar_to_float(value)); + break; + } +} + +template +static void write_numeric_attr(GXAttr attr, const std::array& values) { + ensure_vtx_cache(sBeginVtxFmt); + align_explicit_attr(attr); + const AttrCacheEntry& entry = attr_cache(attr); + if (entry.type == GX_NONE) { + return; + } + if (entry.type == GX_INDEX8 || entry.type == GX_INDEX16) { + return; + } + const u32 expected = entry.expected; + const u32 remaining = expected > sWriteAttrComps ? expected - sWriteAttrComps : expected; + const u32 count = std::min(static_cast(values.size()), remaining); + for (u32 i = 0; i < count; ++i) { + write_numeric_component(entry.fmt, values[i]); + } + advance_attr(attr, count); +} + +static u8 clamp_color_float(float value) { + if (!std::isfinite(value)) { + return 0; + } + return static_cast(std::clamp(std::round(value * 255.0f), 0.0f, 255.0f)); +} + +static u8 quantize_unorm(u8 value, u8 bits) { + const u32 maxValue = (1u << bits) - 1u; + return static_cast((static_cast(value) * maxValue + 127u) / 255u); +} + +static void write_color_attr(u8 r, u8 g, u8 b, u8 a) { + ensure_vtx_cache(sBeginVtxFmt); + const GXAttr attr = select_implicit_attr(GX_VA_CLR0, GX_VA_CLR1); + const AttrCacheEntry& entry = attr_cache(attr); + if (entry.type == GX_NONE) { + return; + } + if (entry.type == GX_INDEX8 || entry.type == GX_INDEX16) { + return; + } + + const ActiveAttrFmt& fmt = entry.fmt; + switch (fmt.type) { + case GX_RGB565: { + const u16 packed = static_cast((quantize_unorm(r, 5) << 11) | (quantize_unorm(g, 6) << 5) | + quantize_unorm(b, 5)); + GX_WRITE_U16(packed); + break; + } + case GX_RGB8: + GX_WRITE_U8(r); + GX_WRITE_U8(g); + GX_WRITE_U8(b); + break; + case GX_RGBX8: + GX_WRITE_U8(r); + GX_WRITE_U8(g); + GX_WRITE_U8(b); + GX_WRITE_U8(0xFF); + break; + case GX_RGBA4: { + const u16 packed = static_cast((quantize_unorm(r, 4) << 12) | (quantize_unorm(g, 4) << 8) | + (quantize_unorm(b, 4) << 4) | quantize_unorm(a, 4)); + GX_WRITE_U16(packed); + break; + } + case GX_RGBA6: { + const u32 packed = (quantize_unorm(r, 6) << 18) | (quantize_unorm(g, 6) << 12) | + (quantize_unorm(b, 6) << 6) | quantize_unorm(a, 6); + GX_WRITE_U8(static_cast((packed >> 16) & 0xFF)); + GX_WRITE_U8(static_cast((packed >> 8) & 0xFF)); + GX_WRITE_U8(static_cast(packed & 0xFF)); + break; + } + case GX_RGBA8: + default: + GX_WRITE_U8(r); + GX_WRITE_U8(g); + GX_WRITE_U8(b); + GX_WRITE_U8(a); + break; + } + advance_attr(attr, 1); +} + +template +static void write_tex_attr(const std::array& values) { + ensure_vtx_cache(sBeginVtxFmt); + const GXAttr attr = select_implicit_attr(GX_VA_TEX0, GX_VA_TEX7); + write_numeric_attr(attr, values); +} + +static void write_index_attr(GXAttr attr, u16 index, bool index16) { + ensure_vtx_cache(sBeginVtxFmt); + if (attr == GX_VA_TEX0) { + attr = select_implicit_attr(GX_VA_TEX0, GX_VA_TEX7); + } else if (attr == GX_VA_CLR0) { + attr = select_implicit_attr(GX_VA_CLR0, GX_VA_CLR1); + } else { + align_explicit_attr(attr); + } + + const GXAttrType type = attr_cache(attr).type; + if (type == GX_INDEX16 || (type != GX_INDEX8 && index16)) { + GX_WRITE_U16(index); + } else { + GX_WRITE_U8(static_cast(index)); + } + advance_attr(attr, 1); +} + +} // namespace + +extern "C" { + +void GXBegin(GXPrimitive primitive, GXVtxFmt vtxFmt, u16 nVerts) { + CHECK(!sInBegin, "GXBegin: called without matching GXEnd"); + + // Flush dirty state before starting a draw + if (__gx->dirtyState != 0) { + __GXSetDirtyState(); + } + + // Flush pending primitives if needed + if (*reinterpret_cast(&__gx->vNum) != 0) { + __GXSendFlushPrim(); + } + + GX_WRITE_U8(vtxFmt | primitive); + GX_WRITE_U16(nVerts); + + // Record state for vertex count validation in GXEnd + sBeginNVerts = nVerts; + sBeginFifoSize = aurora::gx::fifo::get_buffer_size(); + sBeginPrimitive = primitive; + sBeginVtxFmt = vtxFmt; + sInBegin = true; + ensure_vtx_cache(vtxFmt); + reset_attr_writer(); +} + +void GXEnd() { + if (sInBegin) { + u32 bytesWritten = aurora::gx::fifo::get_buffer_size() - sBeginFifoSize; + if (sBeginNVerts > 0 && bytesWritten > 0) { + u32 vtxSize = bytesWritten / sBeginNVerts; + u32 remainder = bytesWritten % sBeginNVerts; + if (remainder != 0) { + Log.warn("GXEnd: vertex data not evenly divisible: {} bytes for {} vertices prim={} fmt={} " + "vcdLo=0x{:08x} vcdHi=0x{:08x} vatA=0x{:08x} vatB=0x{:08x} vatC=0x{:08x}", + bytesWritten, sBeginNVerts, underlying(sBeginPrimitive), underlying(sBeginVtxFmt), + __gx->vcdLo, __gx->vcdHi, __gx->vatA[sBeginVtxFmt], __gx->vatB[sBeginVtxFmt], + __gx->vatC[sBeginVtxFmt]); + } + u32 actualVerts = (vtxSize > 0) ? bytesWritten / vtxSize : 0; + CHECK(actualVerts == sBeginNVerts, + "GXEnd: vertex count mismatch: GXBegin declared {} vertices ({}B each, {}B total) " + "but {} bytes were written ({} vertices)", + sBeginNVerts, vtxSize, sBeginNVerts * vtxSize, bytesWritten, actualVerts); + } + sInBegin = false; + sWriteAttr = GX_VA_NULL; + sWriteAttrComps = 0; + } + if (!aurora::gx::fifo::in_display_list()) { + aurora::gx::fifo::drain(); + } +} + +void GXPosition3f32(f32 x, f32 y, f32 z) { + write_numeric_attr(GX_VA_POS, std::array{scalar_f32(x), scalar_f32(y), scalar_f32(z)}); +} + +void GXPosition3u16(u16 x, u16 y, u16 z) { + write_numeric_attr(GX_VA_POS, std::array{scalar_u32(x), scalar_u32(y), scalar_u32(z)}); +} + +void GXPosition3s16(s16 x, s16 y, s16 z) { + write_numeric_attr(GX_VA_POS, std::array{scalar_s32(x), scalar_s32(y), scalar_s32(z)}); +} + +void GXPosition3u8(u8 x, u8 y, u8 z) { + write_numeric_attr(GX_VA_POS, std::array{scalar_u32(x), scalar_u32(y), scalar_u32(z)}); +} + +void GXPosition3s8(s8 x, s8 y, s8 z) { + write_numeric_attr(GX_VA_POS, std::array{scalar_s32(x), scalar_s32(y), scalar_s32(z)}); +} + +void GXPosition2f32(f32 x, f32 y) { + write_numeric_attr(GX_VA_POS, std::array{scalar_f32(x), scalar_f32(y)}); +} + +void GXPosition2u16(u16 x, u16 y) { + write_numeric_attr(GX_VA_POS, std::array{scalar_u32(x), scalar_u32(y)}); +} + +void GXPosition2s16(s16 x, s16 y) { + write_numeric_attr(GX_VA_POS, std::array{scalar_s32(x), scalar_s32(y)}); +} + +void GXPosition2u8(u8 x, u8 y) { + write_numeric_attr(GX_VA_POS, std::array{scalar_u32(x), scalar_u32(y)}); +} + +void GXPosition2s8(s8 x, s8 y) { + write_numeric_attr(GX_VA_POS, std::array{scalar_s32(x), scalar_s32(y)}); +} + +void GXPosition1x16(u16 idx) { write_index_attr(GX_VA_POS, idx, true); } + +void GXPosition1x8(u8 idx) { write_index_attr(GX_VA_POS, idx, false); } + +void GXNormal3f32(f32 x, f32 y, f32 z) { + write_numeric_attr(GX_VA_NRM, std::array{scalar_f32(x), scalar_f32(y), scalar_f32(z)}); +} + +void GXNormal3u16(u16 x, u16 y, u16 z) { + write_numeric_attr(GX_VA_NRM, std::array{scalar_u32(x), scalar_u32(y), scalar_u32(z)}); +} + +void GXNormal3s16(s16 x, s16 y, s16 z) { + write_numeric_attr(GX_VA_NRM, std::array{scalar_s32(x), scalar_s32(y), scalar_s32(z)}); +} + +void GXNormal3u8(u8 x, u8 y, u8 z) { + write_numeric_attr(GX_VA_NRM, std::array{scalar_u32(x), scalar_u32(y), scalar_u32(z)}); +} + +void GXNormal3s8(s8 x, s8 y, s8 z) { + write_numeric_attr(GX_VA_NRM, std::array{scalar_s32(x), scalar_s32(y), scalar_s32(z)}); +} + +void GXNormal1x16(u16 index) { write_index_attr(GX_VA_NRM, index, true); } + +void GXNormal1x8(u8 index) { write_index_attr(GX_VA_NRM, index, false); } + +void GXColor4f32(f32 r, f32 g, f32 b, f32 a) { + write_color_attr(clamp_color_float(r), clamp_color_float(g), clamp_color_float(b), clamp_color_float(a)); +} + +void GXColor4u8(u8 r, u8 g, u8 b, u8 a) { + write_color_attr(r, g, b, a); +} + +void GXColor3u8(u8 r, u8 g, u8 b) { + write_color_attr(r, g, b, 0xFF); +} + +void GXColor1u32(u32 clr) { + write_color_attr(static_cast((clr >> 24) & 0xFF), static_cast((clr >> 16) & 0xFF), + static_cast((clr >> 8) & 0xFF), static_cast(clr & 0xFF)); +} + +void GXColor1u16(u16 clr) { + const u8 r = static_cast(((clr >> 11) & 0x1F) * 255 / 31); + const u8 g = static_cast(((clr >> 5) & 0x3F) * 255 / 63); + const u8 b = static_cast((clr & 0x1F) * 255 / 31); + write_color_attr(r, g, b, 0xFF); +} + +void GXColor1x16(u16 index) { write_index_attr(GX_VA_CLR0, index, true); } + +void GXColor1x8(u8 index) { write_index_attr(GX_VA_CLR0, index, false); } + +void GXTexCoord2f32(f32 s, f32 t) { + write_tex_attr(std::array{scalar_f32(s), scalar_f32(t)}); +} + +void GXTexCoord2u16(u16 s, u16 t) { + write_tex_attr(std::array{scalar_u32(s), scalar_u32(t)}); +} + +void GXTexCoord2s16(s16 s, s16 t) { + write_tex_attr(std::array{scalar_s32(s), scalar_s32(t)}); +} + +void GXTexCoord2u8(u8 s, u8 t) { + write_tex_attr(std::array{scalar_u32(s), scalar_u32(t)}); +} + +void GXTexCoord2s8(s8 s, s8 t) { + write_tex_attr(std::array{scalar_s32(s), scalar_s32(t)}); +} + +void GXTexCoord1f32(f32 s) { write_tex_attr(std::array{scalar_f32(s)}); } + +void GXTexCoord1u16(u16 s) { write_tex_attr(std::array{scalar_u32(s)}); } + +void GXTexCoord1s16(s16 s) { write_tex_attr(std::array{scalar_s32(s)}); } + +void GXTexCoord1u8(u8 s) { write_tex_attr(std::array{scalar_u32(s)}); } + +void GXTexCoord1s8(s8 s) { write_tex_attr(std::array{scalar_s32(s)}); } + +void GXTexCoord1x16(u16 index) { write_index_attr(GX_VA_TEX0, index, true); } + +void GXTexCoord1x8(u8 index) { write_index_attr(GX_VA_TEX0, index, false); } + +void GXMatrixIndex1u8(GXAttr attr, u8 index) { write_index_attr(attr, index, false); } + +void GXCmd1u8(const u8 x) { aurora::gx::fifo::write_u8(x); } +void GXCmd1u16(const u16 x) { aurora::gx::fifo::write_u16(x); } +void GXCmd1u32(const u32 x) { aurora::gx::fifo::write_u32(x); } +void GXCmd1u64(const u64 x) { aurora::gx::fifo::write_u64(x); } + +void GXParam1u8(const u8 x) { aurora::gx::fifo::write_u8(x); } +void GXParam1u16(const u16 x) { aurora::gx::fifo::write_u16(x); } +void GXParam1u32(const u32 x) { aurora::gx::fifo::write_u32(x); } +void GXParam1s8(const s8 x) { aurora::gx::fifo::write_u8(static_cast(x)); } +void GXParam1s16(const s16 x) { aurora::gx::fifo::write_u16(static_cast(x)); } +void GXParam1s32(const s32 x) { aurora::gx::fifo::write_u32(static_cast(x)); } +void GXParam1f32(const f32 x) { aurora::gx::fifo::write_f32(x); } +void GXParam3f32(const f32 x, const f32 y, const f32 z) { + aurora::gx::fifo::write_f32(x); + aurora::gx::fifo::write_f32(y); + aurora::gx::fifo::write_f32(z); +} +void GXParam4f32(const f32 x, const f32 y, const f32 z, const f32 w) { + aurora::gx::fifo::write_f32(x); + aurora::gx::fifo::write_f32(y); + aurora::gx::fifo::write_f32(z); + aurora::gx::fifo::write_f32(w); +} + +} // extern "C" diff --git a/aurora-main/lib/dolphin/gx/__gx.h b/aurora-main/lib/dolphin/gx/__gx.h new file mode 100644 index 0000000..460791e --- /dev/null +++ b/aurora-main/lib/dolphin/gx/__gx.h @@ -0,0 +1,179 @@ +#pragma once + +#include + +#include "../../gx/fifo.hpp" + +#define GX_WRITE_U8(ub) aurora::gx::fifo::write_u8(static_cast(ub)) +#define GX_WRITE_DATA(data, length) aurora::gx::fifo::write_data(data, length) +#define GX_WRITE_U16(us) aurora::gx::fifo::write_u16(static_cast(us)) +#define GX_WRITE_U32(ui) aurora::gx::fifo::write_u32(static_cast(ui)) +#define GX_WRITE_U64(ui) aurora::gx::fifo::write_u64(static_cast(ui)) +#define GX_WRITE_F32(f) aurora::gx::fifo::write_f32(static_cast(f)) + +// XF register write: opcode 0x10, address in XF space (0x1000+addr), then value +#define GX_WRITE_XF_REG(addr, value) \ + do { \ + GX_WRITE_U8(0x10); \ + GX_WRITE_U32(0x1000 + (addr)); \ + GX_WRITE_U32(value); \ + } while (0) + +// XF register write for u32 data (no address header - used in bulk writes) +#define GX_WRITE_XF_REG_2(addr, value) \ + do { \ + GX_WRITE_U32(value); \ + } while (0) + +// XF register write for f32 data (no address header - used in bulk writes) +#define GX_WRITE_XF_REG_F(addr, value) \ + do { \ + GX_WRITE_F32(value); \ + } while (0) + +// BP (RAS) register write: opcode 0x61, then 32-bit value (upper 8 bits = reg ID) +#define GX_WRITE_RAS_REG(value) \ + do { \ + GX_WRITE_U8(0x61); \ + GX_WRITE_U32(value); \ + } while (0) + +// CP register write with base/stride tracking for indexed arrays +// GX_WRITE_SOME_REG2: CP array base address +#define GX_WRITE_SOME_REG2(a, b, c, addr) \ + do { \ + GX_WRITE_U8(a); \ + GX_WRITE_U8(b); \ + GX_WRITE_U32(c); \ + } while (0) + +// GX_WRITE_SOME_REG3: CP array stride +#define GX_WRITE_SOME_REG3(a, b, c, addr) \ + do { \ + GX_WRITE_U8(a); \ + GX_WRITE_U8(b); \ + GX_WRITE_U32(c); \ + } while (0) + +// GX_WRITE_SOME_REG4: generic CP register write (VCD, VAT, matrix index) +#define GX_WRITE_SOME_REG4(a, b, c, addr) \ + do { \ + GX_WRITE_U8(a); \ + GX_WRITE_U8(b); \ + GX_WRITE_U32(c); \ + } while (0) + +#define GET_REG_FIELD(reg, size, shift) (static_cast(((reg) >> (shift)) & ((1 << (size)) - 1))) + +#define SET_REG_FIELD(line, reg, size, shift, val) \ + do { \ + (reg) = ((u32)(reg) & ~(((1u << (size)) - 1) << (shift))) | ((u32)(val) << (shift)); \ + } while (0) + +#define GX_WRITE_AURORA(cmd) \ + do { \ + GX_WRITE_U8(GX_LOAD_AURORA); \ + GX_WRITE_U16(cmd); \ + } while (0) + +#define GX_WRITE_CP_REG(addr, value) \ + do { \ + GX_WRITE_U8(GX_LOAD_CP_REG); \ + GX_WRITE_U8(addr); \ + GX_WRITE_U32(value); \ + } while (0) + +// This shadow register struct contains the GX state Aurora needs on PC. +struct __GXData_struct { + u16 vNum; // vertex count for flush prim + u16 bpSent; // BP register was sent (need flush prim before next draw) + u32 vLim; // vertex data size limit (bytes per vertex for flush prim) + + u32 vcdLo; // vertex component descriptor low + u32 vcdHi; // vertex component descriptor high + + u32 vatA[8]; // vertex attribute table A (per vtxfmt) + u32 vatB[8]; // vertex attribute table B + u32 vatC[8]; // vertex attribute table C + + u32 lpSize; // line/point size register + + u32 matIdxA; // matrix index A (pnmtx + texmtx 0-3) + u32 matIdxB; // matrix index B (texmtx 4-7) + + u32 ambColor[2]; // ambient colors (XF registers) + u32 matColor[2]; // material colors (XF registers) + + u32 suTs0[8]; // SU texture S0 registers + u32 suTs1[8]; // SU texture S1 registers + u8 tcsManEnab; // bitmask: manual tex coord scale enabled per coord + u32 suScis0; // scissor top-left + u32 suScis1; // scissor bottom-right + + u32 tref[8]; // TEV texture/color reference (2 stages per register) + u32 iref; // indirect texture reference + u32 bpMask; // BP mask register + + u32 IndTexScale0; // indirect texture scale 0 (stages 0-1) + u32 IndTexScale1; // indirect texture scale 1 (stages 2-3) + + u32 tevc[16]; // TEV color combiner registers + u32 teva[16]; // TEV alpha combiner registers + u32 tevKsel[8]; // TEV K color/alpha selection + + u32 cmode0; // blend mode register + u32 cmode1; // destination alpha register + u32 zmode; // Z-buffer mode register + u32 peCtrl; // pixel engine control + + u32 genMode; // general mode (numTexGens, numChans, numTevStages, cullMode, numIndStages) + + u32 tImage0[8]; // texture image 0 registers + u32 tMode0[8]; // texture mode 0 registers + u32 texmapId[16]; // texture map ID tracking + u32 texmapValid; // bitmask of TEV stages with a valid texture coordinate + + GXAttrType nrmType; // normal attribute type + u8 hasNrms; // has normal vectors + u8 hasBiNrms; // has binormal vectors + + u32 projType; // projection type (perspective/orthographic) + f32 projMtx[6]; // projection matrix (6 params) + + f32 vpLeft; // viewport left + f32 vpTop; // viewport top + f32 vpWd; // viewport width + f32 vpHt; // viewport height + f32 vpNearz; // viewport near Z + f32 vpFarz; // viewport far Z + + u8 fgRange; // fog range adjustment enabled + f32 fgSideX; // fog range side X + + u8 inDispList; // currently recording a display list + u8 dlSaveContext; // save/restore context around display lists + u8 dirtyVAT; // bitmask of dirty VAT entries + + u32 dirtyState; // dirty state flags: + // bit 0: SU tex regs + // bit 1: BP mask + // bit 2: gen mode + // bit 3: VCD + // bit 4: VAT +}; + +extern __GXData_struct* __gx; + +extern "C" { + +// Dirty state flush functions +void __GXSetVCD(); +void __GXSetVAT(); +void __GXUpdateBPMask(); +void __GXSetSUTexRegs(); +void __GXSetDirtyState(); +void __GXSetGenMode(); +void __GXSetMatrixIndex(GXAttr matIdxAttr); +void __GXSendFlushPrim(); +void __GXFlushTextureState(); +}; diff --git a/aurora-main/lib/dolphin/gx/gx.hpp b/aurora-main/lib/dolphin/gx/gx.hpp new file mode 100644 index 0000000..0319df0 --- /dev/null +++ b/aurora-main/lib/dolphin/gx/gx.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include "../../internal.hpp" +#include "../../gx/gx.hpp" + +// Defined once in GXAurora.cpp. The FATAL/CHECK/TRY macros resolve `Log` unqualified, so it stays +// at global scope like the rest of this header's using-declarations. +extern aurora::Module Log; + +using aurora::gx::g_gxState; diff --git a/aurora-main/lib/dolphin/ms/mouse.cpp b/aurora-main/lib/dolphin/ms/mouse.cpp new file mode 100644 index 0000000..a5b95f7 --- /dev/null +++ b/aurora-main/lib/dolphin/ms/mouse.cpp @@ -0,0 +1,33 @@ +#include "dolphin/ms.h" +#include + +#include "../../input.hpp" + +extern "C" { +static MSStatus gStatus{}; + +void MSPoll() { + const uint32_t buttons = SDL_GetGlobalMouseState(&gStatus.x, &gStatus.y); + SDL_GetRelativeMouseState(&gStatus.xrel, &gStatus.yrel); + aurora::input::get_mouse_scroll(&gStatus.scrollX, &gStatus.scrollY); + + gStatus.buttons = 0; + if (buttons & SDL_BUTTON_LEFT) { + gStatus.buttons |= MS_BUTTON_LEFT; + } + if (buttons & SDL_BUTTON_MIDDLE) { + gStatus.buttons |= MS_BUTTON_MIDDLE; + } + if (buttons & SDL_BUTTON_RIGHT) { + gStatus.buttons |= MS_BUTTON_RIGHT; + } + if (buttons & SDL_BUTTON_X1) { + gStatus.buttons |= MS_BUTTON_X1; + } + if (buttons & SDL_BUTTON_X2) { + gStatus.buttons |= MS_BUTTON_X2; + } +} + +void MSRead(MSStatus* status) { *status = gStatus; } +} \ No newline at end of file diff --git a/aurora-main/lib/dolphin/mtx/mtx.c b/aurora-main/lib/dolphin/mtx/mtx.c new file mode 100644 index 0000000..ba2796d --- /dev/null +++ b/aurora-main/lib/dolphin/mtx/mtx.c @@ -0,0 +1,618 @@ +#include + +#include +#include + +void C_MTXIdentity(Mtx m) { + assert(m && "MtxIdentity(): NULL Mtx 'm' "); + m[0][0] = 1; + m[0][1] = 0; + m[0][2] = 0; + m[0][3] = 0; + m[1][0] = 0; + m[1][1] = 1; + m[1][2] = 0; + m[1][3] = 0; + m[2][0] = 0; + m[2][1] = 0; + m[2][2] = 1; + m[2][3] = 0; +} + +void C_MTXCopy(const Mtx src, Mtx dst) { + assert(src && "MTXCopy(): NULL MtxPtr 'src' "); + assert(dst && "MTXCopy(): NULL MtxPtr 'dst' "); + if (src != dst) { + dst[0][0] = src[0][0]; + dst[0][1] = src[0][1]; + dst[0][2] = src[0][2]; + dst[0][3] = src[0][3]; + dst[1][0] = src[1][0]; + dst[1][1] = src[1][1]; + dst[1][2] = src[1][2]; + dst[1][3] = src[1][3]; + dst[2][0] = src[2][0]; + dst[2][1] = src[2][1]; + dst[2][2] = src[2][2]; + dst[2][3] = src[2][3]; + } +} + +void C_MTXConcat(const Mtx a, const Mtx b, Mtx ab) { + Mtx mTmp; + MtxPtr m; + + assert(a && "MTXConcat(): NULL MtxPtr 'a' "); + assert(b && "MTXConcat(): NULL MtxPtr 'b' "); + assert(ab && "MTXConcat(): NULL MtxPtr 'ab' "); + + if (ab == a || ab == b) { + m = mTmp; + } else { + m = ab; + } + + m[0][0] = 0 + a[0][2] * b[2][0] + ((a[0][0] * b[0][0]) + (a[0][1] * b[1][0])); + m[0][1] = 0 + a[0][2] * b[2][1] + ((a[0][0] * b[0][1]) + (a[0][1] * b[1][1])); + m[0][2] = 0 + a[0][2] * b[2][2] + ((a[0][0] * b[0][2]) + (a[0][1] * b[1][2])); + m[0][3] = a[0][3] + (a[0][2] * b[2][3] + (a[0][0] * b[0][3] + (a[0][1] * b[1][3]))); + + m[1][0] = 0 + a[1][2] * b[2][0] + ((a[1][0] * b[0][0]) + (a[1][1] * b[1][0])); + m[1][1] = 0 + a[1][2] * b[2][1] + ((a[1][0] * b[0][1]) + (a[1][1] * b[1][1])); + m[1][2] = 0 + a[1][2] * b[2][2] + ((a[1][0] * b[0][2]) + (a[1][1] * b[1][2])); + m[1][3] = a[1][3] + (a[1][2] * b[2][3] + (a[1][0] * b[0][3] + (a[1][1] * b[1][3]))); + + m[2][0] = 0 + a[2][2] * b[2][0] + ((a[2][0] * b[0][0]) + (a[2][1] * b[1][0])); + m[2][1] = 0 + a[2][2] * b[2][1] + ((a[2][0] * b[0][1]) + (a[2][1] * b[1][1])); + m[2][2] = 0 + a[2][2] * b[2][2] + ((a[2][0] * b[0][2]) + (a[2][1] * b[1][2])); + m[2][3] = a[2][3] + (a[2][2] * b[2][3] + (a[2][0] * b[0][3] + (a[2][1] * b[1][3]))); + + if (m == mTmp) { + C_MTXCopy(mTmp, ab); + } +} + +void C_MTXConcatArray(const Mtx a, const Mtx* srcBase, Mtx* dstBase, u32 count) { + u32 i; + + assert(a != 0 && "MTXConcatArray(): NULL MtxPtr 'a' "); + assert(srcBase != 0 && "MTXConcatArray(): NULL MtxPtr 'srcBase' "); + assert(dstBase != 0 && "MTXConcatArray(): NULL MtxPtr 'dstBase' "); + assert(count > 1 && "MTXConcatArray(): count must be greater than 1."); + + for (i = 0; i < count; i++) { + C_MTXConcat(a, *srcBase, *dstBase); + srcBase++; + dstBase++; + } +} + +void C_MTXTranspose(const Mtx src, Mtx xPose) { + Mtx mTmp; + MtxPtr m; + + assert(src && "MTXTranspose(): NULL MtxPtr 'src' "); + assert(xPose && "MTXTranspose(): NULL MtxPtr 'xPose' "); + + if (src == xPose) { + m = mTmp; + } else { + m = xPose; + } + + m[0][0] = src[0][0]; + m[0][1] = src[1][0]; + m[0][2] = src[2][0]; + m[0][3] = 0; + m[1][0] = src[0][1]; + m[1][1] = src[1][1]; + m[1][2] = src[2][1]; + m[1][3] = 0; + m[2][0] = src[0][2]; + m[2][1] = src[1][2]; + m[2][2] = src[2][2]; + m[2][3] = 0; + if (m == mTmp) { + C_MTXCopy(mTmp, xPose); + } +} + +u32 C_MTXInverse(const Mtx src, Mtx inv) { + Mtx mTmp; + MtxPtr m; + f32 det; + + assert(src && "MTXInverse(): NULL MtxPtr 'src' "); + assert(inv && "MTXInverse(): NULL MtxPtr 'inv' "); + + if (src == inv) { + m = mTmp; + } else { + m = inv; + } + det = ((((src[2][1] * (src[0][2] * src[1][0])) + + ((src[2][2] * (src[0][0] * src[1][1])) + + (src[2][0] * (src[0][1] * src[1][2])))) + - (src[0][2] * (src[2][0] * src[1][1]))) + - (src[2][2] * (src[1][0] * src[0][1]))) + - (src[1][2] * (src[0][0] * src[2][1])); + if (0 == det) { + return 0; + } + det = 1 / det; + m[0][0] = (det * +((src[1][1] * src[2][2]) - (src[2][1] * src[1][2]))); + m[0][1] = (det * -((src[0][1] * src[2][2]) - (src[2][1] * src[0][2]))); + m[0][2] = (det * +((src[0][1] * src[1][2]) - (src[1][1] * src[0][2]))); + + m[1][0] = (det * -((src[1][0] * src[2][2]) - (src[2][0] * src[1][2]))); + m[1][1] = (det * +((src[0][0] * src[2][2]) - (src[2][0] * src[0][2]))); + m[1][2] = (det * -((src[0][0] * src[1][2]) - (src[1][0] * src[0][2]))); + + m[2][0] = (det * +((src[1][0] * src[2][1]) - (src[2][0] * src[1][1]))); + m[2][1] = (det * -((src[0][0] * src[2][1]) - (src[2][0] * src[0][1]))); + m[2][2] = (det * +((src[0][0] * src[1][1]) - (src[1][0] * src[0][1]))); + + m[0][3] = ((-m[0][0] * src[0][3]) - (m[0][1] * src[1][3])) - (m[0][2] * src[2][3]); + m[1][3] = ((-m[1][0] * src[0][3]) - (m[1][1] * src[1][3])) - (m[1][2] * src[2][3]); + m[2][3] = ((-m[2][0] * src[0][3]) - (m[2][1] * src[1][3])) - (m[2][2] * src[2][3]); + + if (m == mTmp) { + C_MTXCopy(mTmp, inv); + } + return 1; +} + +u32 C_MTXInvXpose(const Mtx src, Mtx invX) { + Mtx mTmp; + MtxPtr m; + f32 det; + + assert(src && "MTXInvXpose(): NULL MtxPtr 'src' "); + assert(invX && "MTXInvXpose(): NULL MtxPtr 'invX' "); + + if (src == invX) { + m = mTmp; + } else { + m = invX; + } + det = ((((src[2][1] * (src[0][2] * src[1][0])) + + ((src[2][2] * (src[0][0] * src[1][1])) + + (src[2][0] * (src[0][1] * src[1][2])))) + - (src[0][2] * (src[2][0] * src[1][1]))) + - (src[2][2] * (src[1][0] * src[0][1]))) + - (src[1][2] * (src[0][0] * src[2][1])); + if (0 == det) { + return 0; + } + det = 1 / det; + m[0][0] = (det * +((src[1][1] * src[2][2]) - (src[2][1] * src[1][2]))); + m[0][1] = (det * -((src[1][0] * src[2][2]) - (src[2][0] * src[1][2]))); + m[0][2] = (det * +((src[1][0] * src[2][1]) - (src[2][0] * src[1][1]))); + + m[1][0] = (det * -((src[0][1] * src[2][2]) - (src[2][1] * src[0][2]))); + m[1][1] = (det * +((src[0][0] * src[2][2]) - (src[2][0] * src[0][2]))); + m[1][2] = (det * -((src[0][0] * src[2][1]) - (src[2][0] * src[0][1]))); + + m[2][0] = (det * +((src[0][1] * src[1][2]) - (src[1][1] * src[0][2]))); + m[2][1] = (det * -((src[0][0] * src[1][2]) - (src[1][0] * src[0][2]))); + m[2][2] = (det * +((src[0][0] * src[1][1]) - (src[1][0] * src[0][1]))); + + m[0][3] = 0; + m[1][3] = 0; + m[2][3] = 0; + + if (m == mTmp) { + C_MTXCopy(mTmp, invX); + } + return 1; +} + +void C_MTXTrans(Mtx m, f32 xT, f32 yT, f32 zT) { + assert(m && "MTXTrans(): NULL MtxPtr 'm' "); + m[0][0] = 1; + m[0][1] = 0; + m[0][2] = 0; + m[0][3] = xT; + m[1][0] = 0; + m[1][1] = 1; + m[1][2] = 0; + m[1][3] = yT; + m[2][0] = 0; + m[2][1] = 0; + m[2][2] = 1; + m[2][3] = zT; +} + +void C_MTXFrustum(Mtx44 m, f32 t, f32 b, f32 l, f32 r, f32 n, f32 f) { + f32 tmp; + + assert(m && "MTXFrustum(): NULL Mtx44Ptr 'm' "); + assert(t != b && "MTXFrustum(): 't' and 'b' clipping planes are equal "); + assert(l != r && "MTXFrustum(): 'l' and 'r' clipping planes are equal "); + assert(n != f && "MTXFrustum(): 'n' and 'f' clipping planes are equal "); + tmp = 1 / (r - l); + m[0][0] = (2 * n * tmp); + m[0][1] = 0; + m[0][2] = (tmp * (r + l)); + m[0][3] = 0; + tmp = 1 / (t - b); + m[1][0] = 0; + m[1][1] = (2 * n * tmp); + m[1][2] = (tmp * (t + b)); + m[1][3] = 0; + m[2][0] = 0; + m[2][1] = 0; + tmp = 1 / (f - n); + m[2][2] = (-n * tmp); + m[2][3] = (tmp * -(f * n)); + m[3][0] = 0; + m[3][1] = 0; + m[3][2] = -1; + m[3][3] = 0; +} + +void C_MTXOrtho(Mtx44 m, f32 t, f32 b, f32 l, f32 r, f32 n, f32 f) { + f32 tmp; + + assert(m && "MTXOrtho(): NULL Mtx44Ptr 'm' "); + assert(t != b && "MTXOrtho(): 't' and 'b' clipping planes are equal "); + assert(l != r && "MTXOrtho(): 'l' and 'r' clipping planes are equal "); + assert(n != f && "MTXOrtho(): 'n' and 'f' clipping planes are equal "); + tmp = 1 / (r - l); + m[0][0] = 2 * tmp; + m[0][1] = 0; + m[0][2] = 0; + m[0][3] = (tmp * -(r + l)); + tmp = 1 / (t - b); + m[1][0] = 0; + m[1][1] = 2 * tmp; + m[1][2] = 0; + m[1][3] = (tmp * -(t + b)); + m[2][0] = 0; + m[2][1] = 0; + tmp = 1 / (f - n); + m[2][2] = (-1 * tmp); + m[2][3] = (-f * tmp); + m[3][0] = 0; + m[3][1] = 0; + m[3][2] = 0; + m[3][3] = 1; +} + +void C_MTXQuat(Mtx m, const Quaternion* q) { + f32 s; + f32 xs; + f32 ys; + f32 zs; + f32 wx; + f32 wy; + f32 wz; + f32 xx; + f32 xy; + f32 xz; + f32 yy; + f32 yz; + f32 zz; + + assert(m && "MTXQuat(): NULL MtxPtr 'm' "); + assert(q && "MTXQuat(): NULL QuaternionPtr 'q' "); + assert(q->x || q->y || q->z || q->w && "MTXQuat(): zero-value quaternion "); + s = 2 / ((q->w * q->w) + ((q->z * q->z) + ((q->x * q->x) + (q->y * q->y)))); + xs = q->x * s; + ys = q->y * s; + zs = q->z * s; + wx = q->w * xs; + wy = q->w * ys; + wz = q->w * zs; + xx = q->x * xs; + xy = q->x * ys; + xz = q->x * zs; + yy = q->y * ys; + yz = q->y * zs; + zz = q->z * zs; + m[0][0] = (1 - (yy + zz)); + m[0][1] = (xy - wz); + m[0][2] = (xz + wy); + m[0][3] = 0; + m[1][0] = (xy + wz); + m[1][1] = (1 - (xx + zz)); + m[1][2] = (yz - wx); + m[1][3] = 0; + m[2][0] = (xz - wy); + m[2][1] = (yz + wx); + m[2][2] = (1 - (xx + yy)); + m[2][3] = 0; +} + +void C_MTXReflect(Mtx m, const Vec* p, const Vec* n) { + f32 vxy; + f32 vxz; + f32 vyz; + f32 pdotn; + + vxy = -2 * n->x * n->y; + vxz = -2 * n->x * n->z; + vyz = -2 * n->y * n->z; + pdotn = 2 * C_VECDotProduct(p, n); + m[0][0] = (1 - (2 * n->x * n->x)); + m[0][1] = vxy; + m[0][2] = vxz; + m[0][3] = (pdotn * n->x); + m[1][0] = vxy; + m[1][1] = (1 - (2 * n->y * n->y)); + m[1][2] = vyz; + m[1][3] = (pdotn * n->y); + m[2][0] = vxz; + m[2][1] = vyz; + m[2][2] = (1 - (2 * n->z * n->z)); + m[2][3] = (pdotn * n->z); +} + +void C_MTXTransApply(const Mtx src, Mtx dst, f32 xT, f32 yT, f32 zT) { + assert(src && "MTXTransApply(): NULL MtxPtr 'src' "); + assert(dst && "MTXTransApply(): NULL MtxPtr 'src' "); //! wrong assert string + + if (src != dst) { + dst[0][0] = src[0][0]; + dst[0][1] = src[0][1]; + dst[0][2] = src[0][2]; + dst[1][0] = src[1][0]; + dst[1][1] = src[1][1]; + dst[1][2] = src[1][2]; + dst[2][0] = src[2][0]; + dst[2][1] = src[2][1]; + dst[2][2] = src[2][2]; + } + + dst[0][3] = (src[0][3] + xT); + dst[1][3] = (src[1][3] + yT); + dst[2][3] = (src[2][3] + zT); +} + +void C_MTXScale(Mtx m, f32 xS, f32 yS, f32 zS) { + assert(m && "MTXScale(): NULL MtxPtr 'm' "); + m[0][0] = xS; + m[0][1] = 0; + m[0][2] = 0; + m[0][3] = 0; + m[1][0] = 0; + m[1][1] = yS; + m[1][2] = 0; + m[1][3] = 0; + m[2][0] = 0; + m[2][1] = 0; + m[2][2] = zS; + m[2][3] = 0; +} + +void C_MTXScaleApply(const Mtx src, Mtx dst, f32 xS, f32 yS, f32 zS) { + assert(src && "MTXScaleApply(): NULL MtxPtr 'src' "); + assert(dst && "MTXScaleApply(): NULL MtxPtr 'dst' "); + dst[0][0] = (src[0][0] * xS); + dst[0][1] = (src[0][1] * xS); + dst[0][2] = (src[0][2] * xS); + dst[0][3] = (src[0][3] * xS); + dst[1][0] = (src[1][0] * yS); + dst[1][1] = (src[1][1] * yS); + dst[1][2] = (src[1][2] * yS); + dst[1][3] = (src[1][3] * yS); + dst[2][0] = (src[2][0] * zS); + dst[2][1] = (src[2][1] * zS); + dst[2][2] = (src[2][2] * zS); + dst[2][3] = (src[2][3] * zS); +} + +void C_MTXRotRad(Mtx m, char axis, f32 rad) { + f32 sinA; + f32 cosA; + + assert(m && "MTXRotRad(): NULL MtxPtr 'm' "); + sinA = sinf(rad); + cosA = cosf(rad); + C_MTXRotTrig(m, axis, sinA, cosA); +} + +void C_MTXRotTrig(Mtx m, char axis, f32 sinA, f32 cosA) { + assert(m && "MTXRotTrig(): NULL MtxPtr 'm' "); + switch(axis) { + case 'x': + case 'X': + m[0][0] = 1; + m[0][1] = 0; + m[0][2] = 0; + m[0][3] = 0; + m[1][0] = 0; + m[1][1] = cosA; + m[1][2] = -sinA; + m[1][3] = 0; + m[2][0] = 0; + m[2][1] = sinA; + m[2][2] = cosA; + m[2][3] = 0; + break; + case 'y': + case 'Y': + m[0][0] = cosA; + m[0][1] = 0; + m[0][2] = sinA; + m[0][3] = 0; + m[1][0] = 0; + m[1][1] = 1; + m[1][2] = 0; + m[1][3] = 0; + m[2][0] = -sinA; + m[2][1] = 0; + m[2][2] = cosA; + m[2][3] = 0; + break; + case 'z': + case 'Z': + m[0][0] = cosA; + m[0][1] = -sinA; + m[0][2] = 0; + m[0][3] = 0; + m[1][0] = sinA; + m[1][1] = cosA; + m[1][2] = 0; + m[1][3] = 0; + m[2][0] = 0; + m[2][1] = 0; + m[2][2] = 1; + m[2][3] = 0; + break; + default: + assert(0 && "MTXRotTrig(): invalid 'axis' value "); + break; + } +} + +void C_MTXRotAxisRad(Mtx m, const Vec* axis, f32 rad) { + Vec vN; + f32 s; + f32 c; + f32 t; + f32 x; + f32 y; + f32 z; + f32 xSq; + f32 ySq; + f32 zSq; + + assert(m && "MTXRotAxisRad(): NULL MtxPtr 'm' "); + assert(axis && "MTXRotAxisRad(): NULL VecPtr 'axis' "); + + s = sinf(rad); + c = cosf(rad); + t = 1 - c; + C_VECNormalize(axis, &vN); + x = vN.x; + y = vN.y; + z = vN.z; + xSq = (x * x); + ySq = (y * y); + zSq = (z * z); + m[0][0] = (c + (t * xSq)); + m[0][1] = (y * (t * x)) - (s * z); + m[0][2] = (z * (t * x)) + (s * y); + m[0][3] = 0; + m[1][0] = ((y * (t * x)) + (s * z)); + m[1][1] = (c + (t * ySq)); + m[1][2] = ((z * (t * y)) - (s * x)); + m[1][3] = 0; + m[2][0] = ((z * (t * x)) - (s * y)); + m[2][1] = ((z * (t * y)) + (s * x)); + m[2][2] = (c + (t * zSq)); + m[2][3] = 0; +} + +void C_MTXReorder(const Mtx src, ROMtx dst) +{ + u32 i; + u32 j; + for (i = 0; i < 3; i++) { + for (j = 0; j < 4; j++) { + dst[j][i] = src[i][j]; + } + } +} + +void C_MTXLookAt(Mtx m, const Point3d* camPos, const Vec* camUp, const Point3d* target) { + Vec vLook; + Vec vRight; + Vec vUp; + + assert(m && "MTXLookAt(): NULL MtxPtr 'm' "); + assert(camPos && "MTXLookAt(): NULL VecPtr 'camPos' "); + assert(camUp && "MTXLookAt(): NULL VecPtr 'camUp' "); + assert(target && "MTXLookAt(): NULL Point3dPtr 'target' "); + + vLook.x = camPos->x - target->x; + vLook.y = camPos->y - target->y; + vLook.z = camPos->z - target->z; + VECNormalize(&vLook, &vLook); + VECCrossProduct(camUp, &vLook, &vRight); + VECNormalize(&vRight, &vRight); + VECCrossProduct(&vLook, &vRight, &vUp); + m[0][0] = vRight.x; + m[0][1] = vRight.y; + m[0][2] = vRight.z; + m[0][3] = -((camPos->z * vRight.z) + ((camPos->x * vRight.x) + (camPos->y * vRight.y))); + m[1][0] = vUp.x; + m[1][1] = vUp.y; + m[1][2] = vUp.z; + m[1][3] = -((camPos->z * vUp.z) + ((camPos->x * vUp.x) + (camPos->y * vUp.y))); + m[2][0] = vLook.x; + m[2][1] = vLook.y; + m[2][2] = vLook.z; + m[2][3] = -((camPos->z * vLook.z) + ((camPos->x * vLook.x) + (camPos->y * vLook.y))); +} + +void C_MTXLightFrustum(Mtx m, f32 t, f32 b, f32 l, f32 r, f32 n, f32 scaleS, f32 scaleT, f32 transS, f32 transT) { + f32 tmp; + + assert(m && "MTXLightFrustum(): NULL MtxPtr 'm' "); + assert((t != b) && "MTXLightFrustum(): 't' and 'b' clipping planes are equal "); + assert((l != r) && "MTXLightFrustum(): 'l' and 'r' clipping planes are equal "); + + tmp = 1 / (r - l); + m[0][0] = (scaleS * (2 * n * tmp)); + m[0][1] = 0; + m[0][2] = (scaleS * (tmp * (r + l))) - transS; + m[0][3] = 0; + tmp = 1 / (t - b); + m[1][0] = 0; + m[1][1] = (scaleT * (2 * n * tmp)); + m[1][2] = (scaleT * (tmp * (t + b))) - transT; + m[1][3] = 0; + m[2][0] = 0; + m[2][1] = 0; + m[2][2] = -1; + m[2][3] = 0; +} + +void C_MTXLightPerspective(Mtx m, f32 fovY, f32 aspect, f32 scaleS, f32 scaleT, f32 transS, f32 transT) { + f32 angle; + f32 cot; + + assert(m && "MTXLightPerspective(): NULL MtxPtr 'm' "); + assert((fovY > 0.0) && (fovY < 180.0) && "MTXLightPerspective(): 'fovY' out of range "); + assert(0 != aspect && "MTXLightPerspective(): 'aspect' is 0 "); + + angle = (0.5f * fovY); + angle = MTXDegToRad(angle); + cot = 1 / tanf(angle); + m[0][0] = (scaleS * (cot / aspect)); + m[0][1] = 0; + m[0][2] = -transS; + m[0][3] = 0; + m[1][0] = 0; + m[1][1] = (cot * scaleT); + m[1][2] = -transT; + m[1][3] = 0; + m[2][0] = 0; + m[2][1] = 0; + m[2][2] = -1; + m[2][3] = 0; +} + +void C_MTXLightOrtho(Mtx m, f32 t, f32 b, f32 l, f32 r, f32 scaleS, f32 scaleT, f32 transS, f32 transT) { + f32 tmp; + + assert(m && "MTXLightOrtho(): NULL MtxPtr 'm' "); + assert((t != b) && "MTXLightOrtho(): 't' and 'b' clipping planes are equal "); + assert((l != r) && "MTXLightOrtho(): 'l' and 'r' clipping planes are equal "); + tmp = 1 / (r - l); + m[0][0] = (2 * tmp * scaleS); + m[0][1] = 0; + m[0][2] = 0; + m[0][3] = (transS + (scaleS * (tmp * -(r + l)))); + tmp = 1/ (t - b); + m[1][0] = 0; + m[1][1] = (2 * tmp * scaleT); + m[1][2] = 0; + m[1][3] = (transT + (scaleT * (tmp * -(t + b)))); + m[2][0] = 0; + m[2][1] = 0; + m[2][2] = 0; + m[2][3] = 1; +} diff --git a/aurora-main/lib/dolphin/mtx/mtx44.c b/aurora-main/lib/dolphin/mtx/mtx44.c new file mode 100644 index 0000000..1ca47cf --- /dev/null +++ b/aurora-main/lib/dolphin/mtx/mtx44.c @@ -0,0 +1,34 @@ +#include +#include +#include + +void C_MTXPerspective(Mtx44 m, f32 fovY, f32 aspect, f32 n, f32 f) { + f32 angle; + f32 cot; + f32 tmp; + + assert(m && "MTXPerspective(): NULL Mtx44Ptr 'm' "); + assert((fovY > 0.0) && (fovY < 180.0) && "MTXPerspective(): 'fovY' out of range "); + assert(0.0f != aspect && "MTXPerspective(): 'aspect' is 0 "); + + angle = (0.5f * fovY); + angle = MTXDegToRad(angle); + cot = 1 / tanf(angle); + m[0][0] = (cot / aspect); + m[0][1] = 0; + m[0][2] = 0; + m[0][3] = 0; + m[1][0] = 0; + m[1][1] = (cot); + m[1][2] = 0; + m[1][3] = 0; + m[2][0] = 0; + m[2][1] = 0; + tmp = 1 / (f - n); + m[2][2] = (-n * tmp); + m[2][3] = (tmp * -(f * n)); + m[3][0] = 0; + m[3][1] = 0; + m[3][2] = -1; + m[3][3] = 0; +} \ No newline at end of file diff --git a/aurora-main/lib/dolphin/mtx/mtxstack.c b/aurora-main/lib/dolphin/mtx/mtxstack.c new file mode 100644 index 0000000..c026935 --- /dev/null +++ b/aurora-main/lib/dolphin/mtx/mtxstack.c @@ -0,0 +1,108 @@ +#include +#include +#include + +void MTXInitStack(MTXStack* sPtr, u32 numMtx) { + assert(sPtr && "MTXInitStack(): NULL MtxStackPtr 'sPtr' "); + assert(sPtr->stackBase && "MTXInitStack(): 'sPtr' contains a NULL ptr to stack memory "); + assert(numMtx && "MTXInitStack(): 'numMtx' is 0 "); + + sPtr->numMtx = numMtx; + sPtr->stackPtr = 0; +} + +MtxPtr MTXPush(MTXStack* sPtr, const Mtx m) { + assert(sPtr && "MTXPush(): NULL MtxStackPtr 'sPtr' "); + assert(sPtr->stackBase && "MTXPush(): 'sPtr' contains a NULL ptr to stack memory "); + assert(m && "MTXPush(): NULL MtxPtr 'm' "); + + if (sPtr->stackPtr == NULL) { + sPtr->stackPtr = sPtr->stackBase; + MTXCopy(m, sPtr->stackPtr); + } else { + assert(((((uintptr_t)sPtr->stackPtr - (uintptr_t)sPtr->stackBase) / 16) / 3) < (sPtr->numMtx - 1) && "MTXPush(): stack overflow "); + MTXCopy(m, sPtr->stackPtr + 3); + sPtr->stackPtr += 3; + } + + return sPtr->stackPtr; +} + +MtxPtr MTXPushFwd(MTXStack* sPtr, const Mtx m) { + assert(sPtr && "MTXPushFwd(): NULL MtxStackPtr 'sPtr' "); + assert(sPtr->stackBase && "MTXPushFwd(): 'sPtr' contains a NULL ptr to stack memory "); + assert(m && "MTXPushFwd(): NULL MtxPtr 'm' "); + + if (sPtr->stackPtr == NULL) { + sPtr->stackPtr = sPtr->stackBase; + MTXCopy(m, sPtr->stackPtr); + } else { + assert(((((uintptr_t)sPtr->stackPtr - (uintptr_t)sPtr->stackBase) / 16) / 3) < (sPtr->numMtx - 1) && "MTXPushFwd(): stack overflow"); + MTXConcat(sPtr->stackPtr, m, sPtr->stackPtr + 3); + sPtr->stackPtr += 3; + } + + return sPtr->stackPtr; +} + +MtxPtr MTXPushInv(MTXStack* sPtr, const Mtx m) { + Mtx mInv; + assert(sPtr && "MTXPushInv(): NULL MtxStackPtr 'sPtr' "); + assert(sPtr->stackBase && "MTXPushInv(): 'sPtr' contains a NULL ptr to stack memory "); + assert(m && "MTXPushInv(): NULL MtxPtr 'm' "); + + MTXInverse(m, mInv); + if (sPtr->stackPtr == NULL) { + sPtr->stackPtr = sPtr->stackBase; + MTXCopy(mInv, sPtr->stackPtr); + } else { + assert(((((uintptr_t)sPtr->stackPtr - (uintptr_t)sPtr->stackBase) / 16) / 3) < (sPtr->numMtx - 1) && "MTXPushInv(): stack overflow"); + MTXConcat(mInv, sPtr->stackPtr, sPtr->stackPtr + 3); + sPtr->stackPtr += 3; + } + + return sPtr->stackPtr; +} + +MtxPtr MTXPushInvXpose(MTXStack* sPtr, const Mtx m) { + Mtx mIT; + assert(sPtr && "MTXPushInvXpose(): NULL MtxStackPtr 'sPtr' "); + assert(sPtr->stackBase && "MTXPushInvXpose(): 'sPtr' contains a NULL ptr to stack memory "); + assert(m && "MTXPushInvXpose(): NULL MtxPtr 'm' "); + + MTXInverse(m, mIT); + MTXTranspose(mIT, mIT); + if (sPtr->stackPtr == NULL) { + sPtr->stackPtr = sPtr->stackBase; + MTXCopy(mIT, sPtr->stackPtr); + } else { + assert(((((uintptr_t)sPtr->stackPtr - (uintptr_t)sPtr->stackBase) / 16) / 3) < (sPtr->numMtx - 1) && "MTXPushInvXpose(): stack overflow "); + MTXConcat(sPtr->stackPtr, mIT, sPtr->stackPtr + 3); + sPtr->stackPtr += 3; + } + + return sPtr->stackPtr; +} + +MtxPtr MTXPop(MTXStack* sPtr) { + assert(sPtr && "MTXPop(): NULL MtxStackPtr 'sPtr' "); + assert(sPtr->stackBase && "MTXPop(): 'sPtr' contains a NULL ptr to stack memory "); + + if (sPtr->stackPtr == NULL) { + return NULL; + } + + if (sPtr->stackBase == sPtr->stackPtr) { + sPtr->stackPtr = NULL; + return NULL; + } + + sPtr->stackPtr -= 3; + return sPtr->stackPtr; +} + +MtxPtr MTXGetStackPtr(const MTXStack* sPtr) { + assert(sPtr && "MTXGetStackPtr(): NULL MtxStackPtr 'sPtr' "); + assert(sPtr->stackBase && "MTXGetStackPtr(): 'sPtr' contains a NULL ptr to stack memory "); + return sPtr->stackPtr; +} diff --git a/aurora-main/lib/dolphin/mtx/mtxvec.c b/aurora-main/lib/dolphin/mtx/mtxvec.c new file mode 100644 index 0000000..fcb32e0 --- /dev/null +++ b/aurora-main/lib/dolphin/mtx/mtxvec.c @@ -0,0 +1,95 @@ +#include +#include + +void C_MTXMultVec(const Mtx m, const Vec* src, Vec* dst) { + Vec vTmp; + + assert(m && "MTXMultVec(): NULL MtxPtr 'm' "); + assert(src && "MTXMultVec(): NULL VecPtr 'src' "); + assert(dst && "MTXMultVec(): NULL VecPtr 'dst' "); + + vTmp.x = m[0][3] + ((m[0][2] * src->z) + ((m[0][0] * src->x) + (m[0][1] * src->y))); + vTmp.y = m[1][3] + ((m[1][2] * src->z) + ((m[1][0] * src->x) + (m[1][1] * src->y))); + vTmp.z = m[2][3] + ((m[2][2] * src->z) + ((m[2][0] * src->x) + (m[2][1] * src->y))); + dst->x = vTmp.x; + dst->y = vTmp.y; + dst->z = vTmp.z; +} + +void C_MTXMultVecArray(const Mtx m, const Vec* srcBase, Vec* dstBase, u32 count) { + u32 i; + Vec vTmp; + + assert(m && "MTXMultVecArray(): NULL MtxPtr 'm' "); + assert(srcBase && "MTXMultVecArray(): NULL VecPtr 'srcBase' "); + assert(dstBase && "MTXMultVecArray(): NULL VecPtr 'dstBase' "); + assert(count > 1 && "MTXMultVecArray(): count must be greater than 1."); + + for(i = 0; i < count; i++) { + vTmp.x = m[0][3] + ((m[0][2] * srcBase->z) + ((m[0][0] * srcBase->x) + (m[0][1] * srcBase->y))); + vTmp.y = m[1][3] + ((m[1][2] * srcBase->z) + ((m[1][0] * srcBase->x) + (m[1][1] * srcBase->y))); + vTmp.z = m[2][3] + ((m[2][2] * srcBase->z) + ((m[2][0] * srcBase->x) + (m[2][1] * srcBase->y))); + dstBase->x = vTmp.x; + dstBase->y = vTmp.y; + dstBase->z = vTmp.z; + srcBase++; + dstBase++; + } +} + +void C_MTXMultVecSR(const Mtx m, const Vec* src, Vec* dst) { + Vec vTmp; + + assert(m && "MTXMultVecSR(): NULL MtxPtr 'm' "); + assert(src && "MTXMultVecSR(): NULL VecPtr 'src' "); + assert(dst && "MTXMultVecSR(): NULL VecPtr 'dst' "); + + vTmp.x = (m[0][2] * src->z) + ((m[0][0] * src->x) + (m[0][1] * src->y)); + vTmp.y = (m[1][2] * src->z) + ((m[1][0] * src->x) + (m[1][1] * src->y)); + vTmp.z = (m[2][2] * src->z) + ((m[2][0] * src->x) + (m[2][1] * src->y)); + dst->x = vTmp.x; + dst->y = vTmp.y; + dst->z = vTmp.z; +} + +void C_MTXMultVecArraySR(const Mtx m, const Vec* srcBase, Vec* dstBase, u32 count) { + u32 i; + Vec vTmp; + + assert(m && "MTXMultVecArraySR(): NULL MtxPtr 'm' "); + assert(srcBase && "MTXMultVecArraySR(): NULL VecPtr 'srcBase' "); + assert(dstBase && "MTXMultVecArraySR(): NULL VecPtr 'dstBase' "); + assert(count > 1 && "MTXMultVecArraySR(): count must be greater than 1."); + + for(i = 0; i < count; i++) { + vTmp.x = (m[0][2] * srcBase->z) + ((m[0][0] * srcBase->x) + (m[0][1] * srcBase->y)); + vTmp.y = (m[1][2] * srcBase->z) + ((m[1][0] * srcBase->x) + (m[1][1] * srcBase->y)); + vTmp.z = (m[2][2] * srcBase->z) + ((m[2][0] * srcBase->x) + (m[2][1] * srcBase->y)); + dstBase->x = vTmp.x; + dstBase->y = vTmp.y; + dstBase->z = vTmp.z; + srcBase++; + dstBase++; + } +} + +void C_MTXROMultVecArray(const ROMtx m, const Vec *srcBase, Vec *dstBase, u32 count) +{ + u32 i; + Vec vTmp; + + assert(m && "MTXROMultVecArray(): NULL MtxPtr 'm' "); + assert(srcBase && "MTXROMultVecArray(): NULL VecPtr 'srcBase' "); + assert(dstBase && "MTXROMultVecArray(): NULL VecPtr 'dstBase' "); + + for(i = 0; i < count; i++) { + vTmp.x = (m[0][0] * srcBase->x) + (m[1][0] * srcBase->y) + (m[2][0] * srcBase->z) + m[3][0]; + vTmp.y = (m[0][1] * srcBase->x) + (m[1][1] * srcBase->y) + (m[2][1] * srcBase->z) + m[3][1]; + vTmp.z = (m[0][2] * srcBase->x) + (m[1][2] * srcBase->y) + (m[2][2] * srcBase->z) + m[3][2]; + dstBase->x = vTmp.x; + dstBase->y = vTmp.y; + dstBase->z = vTmp.z; + srcBase++; + dstBase++; + } +} diff --git a/aurora-main/lib/dolphin/mtx/quat.c b/aurora-main/lib/dolphin/mtx/quat.c new file mode 100644 index 0000000..80966d6 --- /dev/null +++ b/aurora-main/lib/dolphin/mtx/quat.c @@ -0,0 +1,313 @@ +#include +#include +#include + +void C_QUATAdd(const Quaternion* p, const Quaternion* q, Quaternion* r) { + assert(p && "QUATAdd(): NULL QuaternionPtr 'p' "); + assert(q && "QUATAdd(): NULL QuaternionPtr 'q' "); + assert(r && "QUATAdd(): NULL QuaternionPtr 'r' "); + + r->x = p->x + q->x; + r->y = p->y + q->y; + r->z = p->z + q->z; + r->w = p->w + q->w; +} + +void C_QUATSubtract(const Quaternion* p, const Quaternion* q, Quaternion* r) { + assert(p && "QUATSubtract(): NULL QuaternionPtr 'p' "); + assert(q && "QUATSubtract(): NULL QuaternionPtr 'q' "); + assert(r && "QUATSubtract(): NULL QuaternionPtr 'r' "); + + r->x = p->x - q->x; + r->y = p->y - q->y; + r->z = p->z - q->z; + r->w = p->w - q->w; +} + +void C_QUATMultiply(const Quaternion* p, const Quaternion* q, Quaternion* pq) { + Quaternion* r; + Quaternion pqTmp; + + assert(p && "QUATMultiply(): NULL QuaternionPtr 'p' "); + assert(q && "QUATMultiply(): NULL QuaternionPtr 'q' "); + assert(pq && "QUATMultiply(): NULL QuaternionPtr 'pq' "); + + if (p == pq || q == pq){ + r = &pqTmp; + } else { + r = pq; + } + + r->w = (p->w * q->w) - (p->x * q->x) - (p->y * q->y) - (p->z * q->z); + r->x = (p->w * q->x) + (p->x * q->w) + (p->y * q->z) - (p->z * q->y); + r->y = (p->w * q->y) + (p->y * q->w) + (p->z * q->x) - (p->x * q->z); + r->z = (p->w * q->z) + (p->z * q->w) + (p->x * q->y) - (p->y * q->x); + + if (r == &pqTmp) { + *pq = pqTmp; + } +} + +void C_QUATDivide(const Quaternion* p, const Quaternion* q, Quaternion* r) { + Quaternion qtmp; + assert(p && "QUATDivide(): NULL QuaternionPtr 'p' "); + assert(q && "QUATDivide(): NULL QuaternionPtr 'q' "); + assert(r && "QUATDivide(): NULL QuaternionPtr 'r' "); + + C_QUATInverse(q, &qtmp); + C_QUATMultiply(&qtmp, p, r); +} + +void C_QUATScale(const Quaternion* q, Quaternion* r, f32 scale) { + assert(q && "QUATScale(): NULL QuaternionPtr 'q' "); + assert(r && "QUATScale(): NULL QuaternionPtr 'r' "); + + r->x = q->x * scale; + r->y = q->y * scale; + r->z = q->z * scale; + r->w = q->w * scale; +} + +f32 C_QUATDotProduct(const Quaternion* p, const Quaternion* q) { + assert(p && "QUATDotProduct(): NULL QuaternionPtr 'p' "); + assert(q && "QUATDotProduct(): NULL QuaternionPtr 'q' "); + + return (q->x * p->x) + (q->y * p->y) + (q->z * p->z) + (q->w * p->w); +} + +void C_QUATNormalize(const Quaternion* src, Quaternion* unit) { + f32 mag; + assert(src && "QUATNormalize(): NULL QuaternionPtr 'src' "); + assert(unit && "QUATNormalize(): NULL QuaternionPtr 'unit' "); + + mag = (src->x * src->x) + (src->y * src->y) + (src->z * src->z) + (src->w * src->w); + if (mag >= 0.00001f) { + mag = 1.0f / sqrtf(mag); + + unit->x = src->x * mag; + unit->y = src->y * mag; + unit->z = src->z * mag; + unit->w = src->w * mag; + } else { + unit->x = unit->y = unit->z = unit->w = 0.0f; + } +} + +void C_QUATInverse(const Quaternion* src, Quaternion* inv) { + f32 mag, norminv; + assert(src && "QUATInverse(): NULL QuaternionPtr 'src' "); + assert(inv && "QUATInverse(): NULL QuaternionPtr 'inv' "); + + mag = (src->x * src->x) + (src->y * src->y) + (src->z * src->z) + (src->w * src->w); + if (mag == 0.0f) { + mag = 1.0f; + } + + norminv = 1.0f / mag; + inv->x = -src->x * norminv; + inv->y = -src->y * norminv; + inv->z = -src->z * norminv; + inv->w = src->w * norminv; +} + +void C_QUATExp(const Quaternion* q, Quaternion* r) { + f32 theta, scale; + assert(q && "QUATExp(): NULL QuaternionPtr 'q' "); + assert(r && "QUATExp(): NULL QuaternionPtr 'r' "); + assert(q->w == 0.0f && "QUATExp(): 'q' is not a pure quaternion. "); + + theta = sqrtf((q->x * q->x) + (q->y * q->y) + (q->z * q->z)); + scale = 1.0f; + + if (theta > 0.00001f) { + scale = sinf(theta) / theta; + } + + r->x = scale * q->x; + r->y = scale * q->y; + r->z = scale * q->z; + r->w = cosf(theta); +} + +void C_QUATLogN(const Quaternion* q, Quaternion* r) { + f32 theta, scale; + assert(q && "QUATLogN(): NULL QuaternionPtr 'q' "); + assert(r && "QUATLogN(): NULL QuaternionPtr 'r' "); + + scale = (q->x * q->x) + (q->y * q->y) + (q->z * q->z); + + scale = sqrtf(scale); + theta = atan2f(scale, q->w); + + if (scale > 0.0f) { + scale = theta / scale; + } + + r->x = scale * q->x; + r->y = scale * q->y; + r->z = scale * q->z; + r->w = 0.0f; +} + + +void C_QUATMakeClosest(const Quaternion* q, const Quaternion* qto, Quaternion* r) { + f32 dot; + assert(q && "QUATMakeClosest(): NULL QuaternionPtr 'q' "); + assert(qto && "QUATMakeClosest(): NULL QuaternionPtr 'qto' "); + assert(r && "QUATMakeClosest(): NULL QuaternionPtr 'r' "); + + dot = (q->x * qto->x) + (q->y * qto->y) + (q->z * qto->z) + (q->w * qto->w); + if (dot < 0.0f) { + r->x = -q->x; + r->y = -q->y; + r->z = -q->z; + r->w = -q->w; + } else { + *r = *q; + } +} + +void C_QUATRotAxisRad(Quaternion* r, const Vec* axis, f32 rad) { + f32 half, sh, ch; + Vec nAxis; + + assert(r && "QUATRotAxisRad(): NULL QuaternionPtr 'r' "); + assert(axis && "QUATRotAxisRad(): NULL VecPtr 'axis' "); + + VECNormalize(axis, &nAxis); + + half = rad * 0.5f; + sh = sinf(half); + ch = cosf(half); + + r->x = sh * nAxis.x; + r->y = sh * nAxis.y; + r->z = sh * nAxis.z; + r->w = ch; +} + +void C_QUATMtx(Quaternion* r, const Mtx m) { + f32 tr,s; + s32 i, j, k; + s32 nxt[3] = {1, 2, 0}; + f32 q[3]; + + assert(r && "QUATMtx(): NULL QuaternionPtr 'r' "); + assert(m && "QUATMtx(): NULL MtxPtr 'm' "); + + tr = m[0][0] + m[1][1] + m[2][2]; + if (tr > 0.0f) { + s = sqrtf(tr + 1.0f); + r->w = s * 0.5f; + s = 0.5f / s; + + r->x = (m[2][1] - m[1][2]) * s; + r->y = (m[0][2] - m[2][0]) * s; + r->z = (m[1][0] - m[0][1]) * s; + } else { + i = 0; + if (m[1][1] > m[0][0]) { + i = 1; + } + + if (m[2][2] > m[i][i]) { + i = 2; + } + + j = nxt[i]; + k = nxt[j]; + + s = sqrtf((m[i][i] - (m[j][j] + m[k][k])) + 1.0f); + q[i] = s * 0.5f; + + if (s != 0.0f) { + s = 0.5f / s; + } + + r->w = (m[k][j] - m[j][k]) * s; + q[j] = (m[i][j] + m[j][i]) * s; + q[k] = (m[i][k] + m[k][i]) * s; + + r->x = q[0]; + r->y = q[1]; + r->z = q[2]; + } +} + +void C_QUATLerp(const Quaternion* p, const Quaternion* q, Quaternion* r, f32 t) { + assert(p && "QUATLerp(): NULL QuaternionPtr 'p' "); + assert(q && "QUATLerp(): NULL QuaternionPtr 'q' "); + assert(r && "QUATLerp(): NULL QuaternionPtr 'r' "); + + r->x = t * (q->x - p->x) + p->x; + r->y = t * (q->y - p->y) + p->y; + r->z = t * (q->z - p->z) + p->z; + r->w = t * (q->w - p->w) + p->w; +} + +void C_QUATSlerp(const Quaternion* p, const Quaternion* q, Quaternion* r, f32 t) { + f32 theta, sin_th, cos_th; + f32 tp, tq; + + assert(p && "QUATSlerp(): NULL QuaternionPtr 'p' "); + assert(q && "QUATSlerp(): NULL QuaternionPtr 'q' "); + assert(r && "QUATSlerp(): NULL QuaternionPtr 'r' "); + + cos_th = p->x * q->x + p->y * q->y + p->z * q->z + p->w * q->w; + tq = 1.0f; + + if (cos_th < 0.0f) { + cos_th = -cos_th; + tq = -tq; + } + + if (cos_th <= 0.99999f) { + theta = acosf(cos_th); + sin_th = sinf(theta); + + tp = sinf((1.0f - t) * theta) / sin_th; + tq *= sinf(t * theta) / sin_th; + } else { + tp = 1.0f - t; + tq *= t; + } + + r->x = (tp * p->x) + (tq * q->x); + r->y = (tp * p->y) + (tq * q->y); + r->z = (tp * p->z) + (tq * q->z); + r->w = (tp * p->w) + (tq * q->w); +} + +void C_QUATSquad(const Quaternion* p, const Quaternion* a, const Quaternion* b, const Quaternion* q, Quaternion* r, f32 t) { + Quaternion pq, ab; + f32 t2; + + assert(p && "QUATSquad(): NULL QuaternionPtr 'p' "); + assert(a && "QUATSquad(): NULL QuaternionPtr 'a' "); + assert(b && "QUATSquad(): NULL QuaternionPtr 'b' "); + assert(q && "QUATSquad(): NULL QuaternionPtr 'q' "); + assert(r && "QUATSquad(): NULL QuaternionPtr 'r' "); + + t2 = 2.0f * t * (1.0f - t); + C_QUATSlerp(p, q, &pq, t); + C_QUATSlerp(a, b, &ab, t); + C_QUATSlerp(&pq, &ab, r, t2); +} + +void C_QUATCompA(const Quaternion* qprev, const Quaternion* q, const Quaternion* qnext, Quaternion* a) { + Quaternion qm, qp, lqm, lqp, qpqm, exq; + + assert(qprev && "QUATCompA(): NULL QuaternionPtr 'qprev' "); + assert(q && "QUATCompA(): NULL QuaternionPtr 'q' "); + assert(qnext && "QUATCompA(): NULL QuaternionPtr 'qnext' "); + assert(a && "QUATCompA(): NULL QuaternionPtr 'a' "); + + C_QUATDivide(qprev, q, &qm); + C_QUATLogN(&qm, &lqm); + C_QUATDivide(qnext, q, &qp); + C_QUATLogN(&qp, &lqp); + C_QUATAdd(&lqp, &lqm, &qpqm); + C_QUATScale(&qpqm, &qpqm, -0.25f); + C_QUATExp(&qpqm, &exq); + C_QUATMultiply(q, &exq, a); +} diff --git a/aurora-main/lib/dolphin/mtx/vec.c b/aurora-main/lib/dolphin/mtx/vec.c new file mode 100644 index 0000000..6dc1467 --- /dev/null +++ b/aurora-main/lib/dolphin/mtx/vec.c @@ -0,0 +1,191 @@ +#include +#include +#include +#include + +void C_VECAdd(const Vec* a, const Vec* b, Vec* ab) { + assert(a && "VECAdd(): NULL VecPtr 'a' "); + assert(b && "VECAdd(): NULL VecPtr 'b' "); + assert(ab && "VECAdd(): NULL VecPtr 'ab' "); + ab->x = a->x + b->x; + ab->y = a->y + b->y; + ab->z = a->z + b->z; +} + +void C_VECSubtract(const Vec* a, const Vec* b, Vec* a_b) { + assert(a && "VECSubtract(): NULL VecPtr 'a' "); + assert(b && "VECSubtract(): NULL VecPtr 'b' "); + assert(a_b && "VECSubtract(): NULL VecPtr 'a_b' "); + a_b->x = a->x - b->x; + a_b->y = a->y - b->y; + a_b->z = a->z - b->z; +} + +void C_VECScale(const Vec* src, Vec* dst, f32 scale) { + assert(src && "VECScale(): NULL VecPtr 'src' "); + assert(dst && "VECScale(): NULL VecPtr 'dst' "); + dst->x = (src->x * scale); + dst->y = (src->y * scale); + dst->z = (src->z * scale); +} + +void C_VECNormalize(const Vec* src, Vec* unit) { + f32 mag; + + assert(src && "VECNormalize(): NULL VecPtr 'src' "); + assert(unit && "VECNormalize(): NULL VecPtr 'unit' "); + + mag = (src->z * src->z) + ((src->x * src->x) + (src->y * src->y)); + assert(0.0f != mag && "VECNormalize(): zero magnitude vector "); + + mag = 1.0f/ sqrtf(mag); + unit->x = src->x * mag; + unit->y = src->y * mag; + unit->z = src->z * mag; +} + +void PSVECNormalize(const Vec* src, Vec* unit) { + f32 sqsum; + f32 rsqrt; + + assert(src && "VECNormalize(): NULL VecPtr 'src' "); + assert(unit && "VECNormalize(): NULL VecPtr 'unit' "); + + sqsum = (src->z * src->z + src->x * src->x) + src->y * src->y; + assert(0.0f != sqsum && "VECNormalize(): zero magnitude vector "); + + rsqrt = ppc_rsqrte(sqsum); + unit->x = src->x * rsqrt; + unit->y = src->y * rsqrt; + unit->z = src->z * rsqrt; +} + +f32 C_VECSquareMag(const Vec* v) { + f32 sqmag; + + assert(v && "VECMag(): NULL VecPtr 'v' "); + + sqmag = v->z * v->z + ((v->x * v->x) + (v->y * v->y)); + return sqmag; +} + +f32 PSVECSquareMag(const Vec* v) { + assert(v && "VECMag(): NULL VecPtr 'v' "); + return (v->z * v->z + v->x * v->x) + v->y * v->y; +} + +f32 C_VECMag(const Vec* v) { + return sqrtf(C_VECSquareMag(v)); +} + +f32 PSVECMag(const Vec* v) { + f32 sqmag; + sqmag = PSVECSquareMag(v); + if (sqmag == 0.0f) { + return 0.0f; + } + return sqmag * ppc_rsqrte(sqmag); +} + +f32 C_VECDotProduct(const Vec* a, const Vec* b) { + f32 dot; + + assert(a && "VECDotProduct(): NULL VecPtr 'a' "); + assert(b && "VECDotProduct(): NULL VecPtr 'b' "); + dot = (a->z * b->z) + ((a->x * b->x) + (a->y * b->y)); + return dot; +} + +void C_VECCrossProduct(const Vec* a, const Vec* b, Vec* axb) { + Vec vTmp; + + assert(a && "VECCrossProduct(): NULL VecPtr 'a' "); + assert(b && "VECCrossProduct(): NULL VecPtr 'b' "); + assert(axb && "VECCrossProduct(): NULL VecPtr 'axb' "); + + vTmp.x = (a->y * b->z) - (a->z * b->y); + vTmp.y = (a->z * b->x) - (a->x * b->z); + vTmp.z = (a->x * b->y) - (a->y * b->x); + axb->x = vTmp.x; + axb->y = vTmp.y; + axb->z = vTmp.z; +} + +void C_VECHalfAngle(const Vec* a, const Vec* b, Vec* half) { + Vec aTmp; + Vec bTmp; + Vec hTmp; + + assert(a && "VECHalfAngle(): NULL VecPtr 'a' "); + assert(b && "VECHalfAngle(): NULL VecPtr 'b' "); + assert(half && "VECHalfAngle(): NULL VecPtr 'half' "); + + aTmp.x = -a->x; + aTmp.y = -a->y; + aTmp.z = -a->z; + bTmp.x = -b->x; + bTmp.y = -b->y; + bTmp.z = -b->z; + + VECNormalize(&aTmp, &aTmp); + VECNormalize(&bTmp, &bTmp); + VECAdd(&aTmp, &bTmp, &hTmp); + + if (VECDotProduct(&hTmp, &hTmp) > 0.0f) { + VECNormalize(&hTmp, half); + return; + } + *half = hTmp; +} + +void C_VECReflect(const Vec* src, const Vec* normal, Vec* dst) { + f32 cosA; + Vec uI; + Vec uN; + + assert(src && "VECReflect(): NULL VecPtr 'src' "); + assert(normal && "VECReflect(): NULL VecPtr 'normal' "); + assert(dst && "VECReflect(): NULL VecPtr 'dst' "); + + uI.x = -src->x; + uI.y = -src->y; + uI.z = -src->z; + + VECNormalize(&uI, &uI); + VECNormalize(normal, &uN); + + cosA = VECDotProduct(&uI, &uN); + dst->x = (2.0f * uN.x * cosA) - uI.x; + dst->y = (2.0f * uN.y * cosA) - uI.y; + dst->z = (2.0f * uN.z * cosA) - uI.z; + VECNormalize(dst, dst); +} + +f32 C_VECSquareDistance(const Vec* a, const Vec* b) { + Vec diff; + + diff.x = a->x - b->x; + diff.y = a->y - b->y; + diff.z = a->z - b->z; + return (diff.z * diff.z) + ((diff.x * diff.x) + (diff.y * diff.y)); +} + +f32 PSVECSquareDistance(const Vec* a, const Vec* b) { + f32 dx = a->x - b->x; + f32 dy = a->y - b->y; + f32 dz = a->z - b->z; + return (dx * dx + dy * dy) + dz * dz; +} + +f32 C_VECDistance(const Vec* a, const Vec* b) { + return sqrtf(C_VECSquareDistance(a, b)); +} + +f32 PSVECDistance(const Vec* a, const Vec* b) { + f32 sqdist; + sqdist = PSVECSquareDistance(a, b); + if (sqdist == 0.0f) { + return 0.0f; + } + return sqdist * ppc_rsqrte(sqdist); +} diff --git a/aurora-main/lib/dolphin/os/OSAddress.cpp b/aurora-main/lib/dolphin/os/OSAddress.cpp new file mode 100644 index 0000000..8ca1b4a --- /dev/null +++ b/aurora-main/lib/dolphin/os/OSAddress.cpp @@ -0,0 +1,41 @@ +#include "internal.hpp" +#include + +#include + +#if NDEBUG +#undef OSPhysicalToCached +#undef OSPhysicalToUncached +#undef OSCachedToPhysical +#undef OSUncachedToPhysical +#undef OSCachedToUncached +#undef OSUncachedToCached +#endif + +void* OSPhysicalToCached(u32 paddr) { + assert (paddr <= aurora::g_config.mem1Size); + return POINTER_ADD(MEM1Start, paddr); +} + +void* OSPhysicalToUncached(u32 paddr) { + return OSPhysicalToCached(paddr); +} + +u32 OSCachedToPhysical(void* caddr) { + assert (caddr >= MEM1Start && caddr <= MEM1End); + + return (reinterpret_cast(caddr) - reinterpret_cast(MEM1Start)); +} + +u32 OSUncachedToPhysical(void* ucaddr) { + return OSCachedToPhysical(ucaddr); +} + +void* OSCachedToUncached(void* caddr) { + return caddr; +} + +void* OSUncachedToCached(void* ucaddr) { + return ucaddr; +} + diff --git a/aurora-main/lib/dolphin/os/OSAlloc.cpp b/aurora-main/lib/dolphin/os/OSAlloc.cpp new file mode 100644 index 0000000..790e300 --- /dev/null +++ b/aurora-main/lib/dolphin/os/OSAlloc.cpp @@ -0,0 +1,564 @@ +#include + +#include +#include + +#include "../../logging.hpp" + +extern "C" volatile OSHeapHandle __OSCurrHeap = -1; + +namespace { + +constexpr u32 kAlignment = 32; +constexpr u32 kHeaderSize = 32; +constexpr u32 kMinObjectSize = 64; + +struct HeapDesc; + +struct alignas(32) Cell { + Cell* prev; + Cell* next; + s32 size; + HeapDesc* owner; +}; + +static_assert(sizeof(Cell) == kHeaderSize, "Cell header must stay 32 bytes"); + +struct HeapDesc { + s32 size; + Cell* freeList; + Cell* allocated; +}; + +static aurora::Module AllocLog("aurora::os::alloc"); + +static HeapDesc* sHeapArray = nullptr; +static int sNumHeaps = 0; +static u8* sArenaStart = nullptr; +static u8* sArenaEnd = nullptr; + +static uintptr_t roundUp32(const uintptr_t value) { + return (value + (kAlignment - 1)) & ~(static_cast(kAlignment - 1)); +} + +static uintptr_t roundDown32(const uintptr_t value) { + return value & ~(static_cast(kAlignment - 1)); +} + +static bool inArena(const void* ptr) { + if (sArenaStart == nullptr || sArenaEnd == nullptr) { + return false; + } + const auto p = reinterpret_cast(ptr); + return p >= reinterpret_cast(sArenaStart) && p < reinterpret_cast(sArenaEnd); +} + +static bool validHeapHandle(const OSHeapHandle heap) { + return sHeapArray != nullptr && heap >= 0 && heap < sNumHeaps && sHeapArray[heap].size >= 0; +} + +static Cell* addFront(Cell* list, Cell* cell) { + cell->prev = nullptr; + cell->next = list; + if (list != nullptr) { + list->prev = cell; + } + return cell; +} + +static Cell* extract(Cell* list, Cell* cell) { + if (cell->next != nullptr) { + cell->next->prev = cell->prev; + } + if (cell->prev != nullptr) { + cell->prev->next = cell->next; + return list; + } + return cell->next; +} + +static bool containsCell(Cell* list, Cell* cell) { + for (Cell* it = list; it != nullptr; it = it->next) { + if (it == cell) { + return true; + } + } + return false; +} + +static Cell* insertAndCoalesce(Cell* list, Cell* cell) { + Cell* prev = nullptr; + Cell* next = list; + while (next != nullptr && next < cell) { + prev = next; + next = next->next; + } + + cell->prev = prev; + cell->next = next; + if (prev != nullptr) { + prev->next = cell; + } else { + list = cell; + } + if (next != nullptr) { + next->prev = cell; + } + + if (cell->next != nullptr) { + auto* right = cell->next; + if (reinterpret_cast(cell) + cell->size == reinterpret_cast(right)) { + cell->size += right->size; + cell->next = right->next; + if (right->next != nullptr) { + right->next->prev = cell; + } + } + } + + if (cell->prev != nullptr) { + auto* left = cell->prev; + if (reinterpret_cast(left) + left->size == reinterpret_cast(cell)) { + left->size += cell->size; + left->next = cell->next; + if (cell->next != nullptr) { + cell->next->prev = left; + } + return list; + } + } + + return list; +} + +static bool validateBlockRange(const uintptr_t start, const uintptr_t end) { + if (start >= end) { + return false; + } + if (sArenaStart == nullptr || sArenaEnd == nullptr) { + return false; + } + return start >= reinterpret_cast(sArenaStart) + && end <= reinterpret_cast(sArenaEnd) + && (end - start) >= kMinObjectSize; +} + +static void dropTinyCell(HeapDesc& hd, Cell* cell) { + hd.freeList = extract(hd.freeList, cell); + hd.size -= cell->size; +} + +static void carveRangeFromHeap(HeapDesc& hd, uintptr_t carveStart, uintptr_t carveEnd) { + Cell* cell = hd.freeList; + while (cell != nullptr) { + Cell* nextCell = cell->next; + const auto cellStart = reinterpret_cast(cell); + const auto cellEnd = cellStart + static_cast(cell->size); + + const auto overlapStart = carveStart > cellStart ? carveStart : cellStart; + const auto overlapEnd = carveEnd < cellEnd ? carveEnd : cellEnd; + if (overlapStart >= overlapEnd) { + cell = nextCell; + continue; + } + + const auto removed = static_cast(overlapEnd - overlapStart); + hd.size -= removed; + + const bool cutHead = overlapStart == cellStart; + const bool cutTail = overlapEnd == cellEnd; + + if (cutHead && cutTail) { + hd.freeList = extract(hd.freeList, cell); + } else if (cutHead) { + auto* newCell = reinterpret_cast(overlapEnd); + newCell->size = static_cast(cellEnd - overlapEnd); + newCell->owner = nullptr; + newCell->prev = cell->prev; + newCell->next = cell->next; + if (newCell->prev != nullptr) { + newCell->prev->next = newCell; + } else { + hd.freeList = newCell; + } + if (newCell->next != nullptr) { + newCell->next->prev = newCell; + } + if (newCell->size < static_cast(kMinObjectSize)) { + dropTinyCell(hd, newCell); + } + } else if (cutTail) { + cell->size = static_cast(overlapStart - cellStart); + if (cell->size < static_cast(kMinObjectSize)) { + dropTinyCell(hd, cell); + } + } else { + const auto leftSize = static_cast(overlapStart - cellStart); + const auto rightSize = static_cast(cellEnd - overlapEnd); + if (leftSize >= static_cast(kMinObjectSize) && rightSize >= static_cast(kMinObjectSize)) { + auto* right = reinterpret_cast(overlapEnd); + right->size = rightSize; + right->owner = nullptr; + right->prev = cell; + right->next = cell->next; + if (right->next != nullptr) { + right->next->prev = right; + } + cell->next = right; + cell->size = leftSize; + } else if (leftSize >= static_cast(kMinObjectSize)) { + cell->size = leftSize; + } else if (rightSize >= static_cast(kMinObjectSize)) { + auto* right = reinterpret_cast(overlapEnd); + right->size = rightSize; + right->owner = nullptr; + right->prev = cell->prev; + right->next = cell->next; + if (right->prev != nullptr) { + right->prev->next = right; + } else { + hd.freeList = right; + } + if (right->next != nullptr) { + right->next->prev = right; + } + } else { + hd.freeList = extract(hd.freeList, cell); + } + } + + cell = nextCell; + } +} + +} // namespace + +extern "C" { + +void* OSInitAlloc(void* arenaStart, void* arenaEnd, int maxHeaps) { + if (arenaStart == nullptr || arenaEnd == nullptr || maxHeaps <= 0) { + return nullptr; + } + + auto start = reinterpret_cast(arenaStart); + auto end = reinterpret_cast(arenaEnd); + if (start >= end) { + return nullptr; + } + + const auto arrayBytes = static_cast(maxHeaps) * sizeof(HeapDesc); + if ((end - start) < arrayBytes + kMinObjectSize) { + return nullptr; + } + + sHeapArray = reinterpret_cast(arenaStart); + sNumHeaps = maxHeaps; + for (int i = 0; i < sNumHeaps; ++i) { + sHeapArray[i].size = -1; + sHeapArray[i].freeList = nullptr; + sHeapArray[i].allocated = nullptr; + } + + __OSCurrHeap = -1; + sArenaStart = reinterpret_cast(roundUp32(start + arrayBytes)); + sArenaEnd = reinterpret_cast(roundDown32(end)); + if (sArenaEnd <= sArenaStart || static_cast(sArenaEnd - sArenaStart) < kMinObjectSize) { + sHeapArray = nullptr; + sNumHeaps = 0; + sArenaStart = nullptr; + sArenaEnd = nullptr; + return nullptr; + } + + return sArenaStart; +} + +OSHeapHandle OSCreateHeap(void* start, void* end) { + if (sHeapArray == nullptr) { + return -1; + } + + const auto blockStart = roundUp32(reinterpret_cast(start)); + const auto blockEnd = roundDown32(reinterpret_cast(end)); + if (!validateBlockRange(blockStart, blockEnd)) { + return -1; + } + + for (OSHeapHandle heap = 0; heap < sNumHeaps; ++heap) { + auto& hd = sHeapArray[heap]; + if (hd.size >= 0) { + continue; + } + + hd.size = static_cast(blockEnd - blockStart); + hd.allocated = nullptr; + hd.freeList = reinterpret_cast(blockStart); + hd.freeList->prev = nullptr; + hd.freeList->next = nullptr; + hd.freeList->size = hd.size; + hd.freeList->owner = nullptr; + return heap; + } + + return -1; +} + +void OSDestroyHeap(OSHeapHandle heap) { + if (!validHeapHandle(heap)) { + return; + } + + auto& hd = sHeapArray[heap]; + hd.size = -1; + hd.freeList = nullptr; + hd.allocated = nullptr; + if (__OSCurrHeap == heap) { + __OSCurrHeap = -1; + } +} + +void OSAddToHeap(OSHeapHandle heap, void* start, void* end) { + if (!validHeapHandle(heap)) { + return; + } + + const auto blockStart = roundUp32(reinterpret_cast(start)); + const auto blockEnd = roundDown32(reinterpret_cast(end)); + if (!validateBlockRange(blockStart, blockEnd)) { + return; + } + + auto& hd = sHeapArray[heap]; + auto* cell = reinterpret_cast(blockStart); + cell->prev = nullptr; + cell->next = nullptr; + cell->size = static_cast(blockEnd - blockStart); + cell->owner = nullptr; + hd.freeList = insertAndCoalesce(hd.freeList, cell); + hd.size += cell->size; +} + +void* OSAllocFromHeap(OSHeapHandle heap, u32 size) { + if (!validHeapHandle(heap) || size == 0) { + return nullptr; + } + + auto& hd = sHeapArray[heap]; + const auto requested = static_cast(roundUp32(static_cast(size) + kHeaderSize)); + + Cell* cell = hd.freeList; + while (cell != nullptr && cell->size < requested) { + cell = cell->next; + } + if (cell == nullptr) { + return nullptr; + } + + const auto leftover = cell->size - requested; + if (leftover < static_cast(kMinObjectSize)) { + hd.freeList = extract(hd.freeList, cell); + } else { + auto* split = reinterpret_cast(reinterpret_cast(cell) + requested); + split->size = leftover; + split->owner = nullptr; + split->prev = cell->prev; + split->next = cell->next; + if (split->prev != nullptr) { + split->prev->next = split; + } else { + hd.freeList = split; + } + if (split->next != nullptr) { + split->next->prev = split; + } + cell->size = requested; + } + + cell->owner = &hd; + hd.allocated = addFront(hd.allocated, cell); + return reinterpret_cast(cell) + kHeaderSize; +} + +void OSFreeToHeap(OSHeapHandle heap, void* ptr) { + if (!validHeapHandle(heap) || ptr == nullptr) { + return; + } + if (!inArena(ptr) || (reinterpret_cast(ptr) & (kAlignment - 1)) != 0) { + return; + } + + auto& hd = sHeapArray[heap]; + auto* cell = reinterpret_cast(reinterpret_cast(ptr) - kHeaderSize); + if (cell->owner != &hd || !containsCell(hd.allocated, cell)) { + return; + } + + hd.allocated = extract(hd.allocated, cell); + cell->owner = nullptr; + hd.freeList = insertAndCoalesce(hd.freeList, cell); +} + +OSHeapHandle OSSetCurrentHeap(OSHeapHandle heap) { + const auto prev = __OSCurrHeap; + if (heap == -1 || validHeapHandle(heap)) { + __OSCurrHeap = heap; + } + return prev; +} + +void* OSAllocFixed(void* rstart, void* rend) { + if (sHeapArray == nullptr || rstart == nullptr || rend == nullptr) { + return nullptr; + } + + for (int i = 0; i < sNumHeaps; ++i) { + if (sHeapArray[i].size >= 0 && sHeapArray[i].allocated != nullptr) { + return nullptr; + } + } + + const auto fixedStart = roundDown32(reinterpret_cast(rstart)); + const auto fixedEnd = roundUp32(reinterpret_cast(rend)); + if (fixedStart >= fixedEnd) { + return nullptr; + } + if (fixedStart < reinterpret_cast(sArenaStart) + || fixedEnd > reinterpret_cast(sArenaEnd)) { + return nullptr; + } + + for (int i = 0; i < sNumHeaps; ++i) { + auto& hd = sHeapArray[i]; + if (hd.size >= 0) { + carveRangeFromHeap(hd, fixedStart, fixedEnd); + } + } + + return reinterpret_cast(fixedStart); +} + +s32 OSCheckHeap(OSHeapHandle heap) { + if (!validHeapHandle(heap)) { + return -1; + } + + auto& hd = sHeapArray[heap]; + s32 total = 0; + s32 freeBytes = 0; + + if (hd.allocated != nullptr && hd.allocated->prev != nullptr) { + return -1; + } + + for (Cell* cell = hd.allocated; cell != nullptr; cell = cell->next) { + if (!inArena(cell) + || (reinterpret_cast(cell) & (kAlignment - 1)) != 0 + || cell->size < static_cast(kMinObjectSize) + || (cell->size & (kAlignment - 1)) != 0 + || cell->owner != &hd + || (cell->next != nullptr && cell->next->prev != cell)) { + return -1; + } + total += cell->size; + if (total <= 0 || total > hd.size) { + return -1; + } + } + + if (hd.freeList != nullptr && hd.freeList->prev != nullptr) { + return -1; + } + + for (Cell* cell = hd.freeList; cell != nullptr; cell = cell->next) { + if (!inArena(cell) + || (reinterpret_cast(cell) & (kAlignment - 1)) != 0 + || cell->size < static_cast(kMinObjectSize) + || (cell->size & (kAlignment - 1)) != 0 + || cell->owner != nullptr + || (cell->next != nullptr && cell->next->prev != cell)) { + return -1; + } + if (cell->next != nullptr) { + if (reinterpret_cast(cell) + static_cast(cell->size) + > reinterpret_cast(cell->next)) { + return -1; + } + } + + total += cell->size; + freeBytes += cell->size - static_cast(kHeaderSize); + if (total <= 0 || total > hd.size) { + return -1; + } + } + + if (total != hd.size) { + return -1; + } + return freeBytes; +} + +u32 OSReferentSize(void* ptr) { + if (ptr == nullptr || !inArena(ptr) || (reinterpret_cast(ptr) & (kAlignment - 1)) != 0) { + return 0; + } + auto* cell = reinterpret_cast(reinterpret_cast(ptr) - kHeaderSize); + if (cell->owner == nullptr) { + return 0; + } + return static_cast(cell->size - static_cast(kHeaderSize)); +} + +void OSDumpHeap(OSHeapHandle heap) { + AllocLog.info("OSDumpHeap({})", heap); + if (!validHeapHandle(heap)) { + AllocLog.info("--------Invalid"); + return; + } + + auto& hd = sHeapArray[heap]; + if (OSCheckHeap(heap) < 0) { + AllocLog.info("--------Broken"); + return; + } + + AllocLog.info("addr\tsize\t\tend\t\tprev\t\tnext"); + AllocLog.info("--------Allocated"); + for (Cell* cell = hd.allocated; cell != nullptr; cell = cell->next) { + AllocLog.info("{}\t{}\t{}\t{}\t{}", + reinterpret_cast(cell), + cell->size, + reinterpret_cast(reinterpret_cast(cell) + cell->size), + reinterpret_cast(cell->prev), + reinterpret_cast(cell->next)); + } + + AllocLog.info("--------Free"); + for (Cell* cell = hd.freeList; cell != nullptr; cell = cell->next) { + AllocLog.info("{}\t{}\t{}\t{}\t{}", + reinterpret_cast(cell), + cell->size, + reinterpret_cast(reinterpret_cast(cell) + cell->size), + reinterpret_cast(cell->prev), + reinterpret_cast(cell->next)); + } +} + +void OSVisitAllocated(void (*visitor)(void*, u32)) { + if (visitor == nullptr || sHeapArray == nullptr) { + return; + } + + for (int heap = 0; heap < sNumHeaps; ++heap) { + auto& hd = sHeapArray[heap]; + if (hd.size < 0) { + continue; + } + for (Cell* cell = hd.allocated; cell != nullptr; cell = cell->next) { + visitor(reinterpret_cast(cell) + kHeaderSize, + static_cast(cell->size - static_cast(kHeaderSize))); + } + } +} + +} // extern "C" diff --git a/aurora-main/lib/dolphin/os/OSArena.cpp b/aurora-main/lib/dolphin/os/OSArena.cpp new file mode 100644 index 0000000..5eeb861 --- /dev/null +++ b/aurora-main/lib/dolphin/os/OSArena.cpp @@ -0,0 +1,58 @@ +#include + +#include "internal.hpp" + +#include + +#define ROUND64(n, a) (((u64)(n) + (a)-1) & ~(u64)((a)-1)) +#define TRUNC64(n, a) (((u64)(n)) & ~(u64)((a)-1)) + +static void* ArenaLow; +static void* ArenaHigh; + +void* OSGetArenaHi() { + return ArenaHigh; +} + +void* OSGetArenaLo() { + return ArenaLow; +} + +void OSSetArenaHi(void* newHi) { + assert(newHi <= MEM1End && newHi >= MEM1Start); + ArenaHigh = newHi; +} + +void OSSetArenaLo(void* newLo) { + assert(newLo <= MEM1End && newLo >= MEM1Start); + ArenaLow = newLo; +} + +void* OSAllocFromArenaLo(const u32 size, const u32 align) { + void* ptr = OSGetArenaLo(); + auto arenaLo = static_cast(ptr = reinterpret_cast(ROUND64(ptr, align))); + arenaLo += size; + arenaLo = reinterpret_cast(ROUND64(arenaLo, align)); + OSSetArenaLo(arenaLo); + return ptr; +} + +void* OSAllocFromArenaHi(const u32 size, const u32 align) { + void* ptr; + + auto arenaHi = static_cast(OSGetArenaHi()); + arenaHi = reinterpret_cast(TRUNC64(arenaHi, align)); + arenaHi -= size; + arenaHi = static_cast(ptr = reinterpret_cast(TRUNC64(arenaHi, align))); + OSSetArenaHi(arenaHi); + return ptr; +} + +void AuroraInitArena() { + if (MEM1Start == nullptr) { + return; + } + + OSSetArenaLo(static_cast(MEM1Start) + ARENA_START_OFFSET); + OSSetArenaHi(MEM1End); +} diff --git a/aurora-main/lib/dolphin/os/OSBootInfo.cpp b/aurora-main/lib/dolphin/os/OSBootInfo.cpp new file mode 100644 index 0000000..e13af46 --- /dev/null +++ b/aurora-main/lib/dolphin/os/OSBootInfo.cpp @@ -0,0 +1,12 @@ +#include "internal.hpp" +#include + +void AuroraFillBootInfo() { + if (OSBaseAddress == 0) { + return; + } + + const auto info = static_cast(OSPhysicalToCached(0)); + + info->memorySize = aurora::g_config.mem1Size; +} diff --git a/aurora-main/lib/dolphin/os/OSInit.cpp b/aurora-main/lib/dolphin/os/OSInit.cpp new file mode 100644 index 0000000..361dd32 --- /dev/null +++ b/aurora-main/lib/dolphin/os/OSInit.cpp @@ -0,0 +1,18 @@ +#include "internal.hpp" + +#include + +static bool AlreadyInitialized; + +void OSInit() { + if (AlreadyInitialized) { + return; + } + + AlreadyInitialized = true; + + AuroraOSInitMemory(); + AuroraFillBootInfo(); + AuroraInitClock(); + AuroraInitArena(); +} diff --git a/aurora-main/lib/dolphin/os/OSMemory.cpp b/aurora-main/lib/dolphin/os/OSMemory.cpp new file mode 100644 index 0000000..dc5c429 --- /dev/null +++ b/aurora-main/lib/dolphin/os/OSMemory.cpp @@ -0,0 +1,148 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#endif + +#include "fmt/base.h" + +#include + +#include "internal.hpp" +#include +#include "dolphin/types.h" + +#if !NDEBUG && (INTPTR_MAX > INT32_MAX) +#define GUARD_MEMORY 1 +#endif + +uintptr_t OSBaseAddress = 0; + +void* MEM1Start; +void* MEM1End; + +static void GuardGCMemory(); +static void* AllocMEM1(u32 size); + +void AuroraOSInitMemory() { + GuardGCMemory(); + + if (aurora::g_config.mem1Size > 0) { + MEM1Start = AllocMEM1(aurora::g_config.mem1Size); + MEM1End = reinterpret_cast(reinterpret_cast(MEM1Start) + aurora::g_config.mem1Size); + OSBaseAddress = reinterpret_cast(MEM1Start); + } +} + +#if GUARD_MEMORY +static uintptr_t GetAllocationGranularity() { +#if _WIN32 + SYSTEM_INFO sysInfo; + GetSystemInfo(&sysInfo); + + return sysInfo.dwAllocationGranularity; +#else + // TODO: posix impl + return 0; +#endif +} + +static void TryGuardRegion(const uintptr_t start, const uintptr_t end, char const* const name) { +#if _WIN32 + assert(start != 0); + const auto addr = VirtualAlloc( + reinterpret_cast(start), + end - start, + MEM_RESERVE, + PAGE_NOACCESS); + + if (addr == nullptr) { + Log.debug("Unable to guard memory region: {}", name); + } else { + assert(addr == reinterpret_cast(start)); + Log.debug("Successfully guarded memory range: {:08X}-{:08X} ({})", start, end, name); + } +#else + // TODO: posix impl +#endif +} + +static void GuardGCMemory() { + // Reserve the normal GC/Wii memory map so accesses are 100% guaranteed to fail. + // https://www.gc-forever.com/yagcd/chap5.html#sec5.11 + // https://wiibrew.org/wiki/Memory_map + + // We can't quite map at address 0 (for good reasons) but we *can* map at the next granularity over! + TryGuardRegion(0x00000000 + GetAllocationGranularity(), 0x017fffff, "MEM1 Physical"); + TryGuardRegion(0x80000000, 0x817fffff, "MEM1 Logical (cached)"); + TryGuardRegion(0xC0000000, 0xC17fffff, "MEM1 Logical (uncached)"); + TryGuardRegion(0x10000000, 0x18000000, "MEM2 Physical"); + TryGuardRegion(0x90000000, 0x98000000, "MEM2 Logical (cached)"); + TryGuardRegion(0xD0000000, 0xD8000000, "MEM2 Logical (uncached)"); + TryGuardRegion(0x08000000, 0x08300000, "EFB Physical"); + TryGuardRegion(0xC8000000, 0xC8300000, "EFB Logical"); + TryGuardRegion(0x0D000000, 0x0D008000, "Hollywood HW registers Physical"); + TryGuardRegion(0xCD000000, 0xCD008000, "Hollywood HW registers Logical"); + TryGuardRegion(0x0C000000, 0x0C008020, "Broadway/GC HW registers Physical"); + TryGuardRegion(0xCC000000, 0xCC008020, "Broadway/GC HW registers Logical"); + TryGuardRegion(0xe0000000, 0xe0003fff, "GC L2 cache"); + TryGuardRegion(0xfff00000, 0xffffffff, "GC IPL"); +} +#else +static void GuardGCMemory() { } +#endif + +#if _WIN64 && !NDEBUG +static void* AllocMEM1(u32 size) { + // Allocate an entire 32-bit's worth of memory and allocate the real MEM1 in that. + // This way, if a 64-bit pointer gets truncated to 32-bit, it will still fall in our guard pages. + + void* bulkChunk = VirtualAlloc( + nullptr, + 8ll * 1024 * 1024 * 1024, + MEM_RESERVE, + PAGE_NOACCESS); + + if (bulkChunk == nullptr) { + DWORD err = GetLastError(); + fmt::memory_buffer msg; + fmt::format_system_error( + msg, + static_cast(err), + "Failed to allocate bulk chunk for MEM1"); + Log.fatal("{}", fmt::to_string(msg)); + } + + uintptr_t memSpace = (reinterpret_cast(bulkChunk) | 0xFFFFFFFF) + 1; + void* mem1Address = reinterpret_cast(memSpace + 0x80000000); + + Log.debug("Reserved memory map at {:016X}-{:016X}", memSpace, memSpace + 0xFFFFFFFF); + Log.debug( + "MEM1 at {:016X}-{:016X}", + reinterpret_cast(mem1Address), + reinterpret_cast(mem1Address) + size); + + void* result = VirtualAlloc(mem1Address, size, MEM_COMMIT, PAGE_READWRITE); + if (result == nullptr) { + DWORD err = GetLastError(); + fmt::memory_buffer msg; + fmt::format_system_error( + msg, + static_cast(err), + "Failed to commit memory for MEM1"); + Log.fatal("{}", fmt::to_string(msg)); + } + + assert(result == mem1Address); + return result; +} +#else +static void* AllocMEM1(u32 size) { + return calloc(1, size); +} +#endif + +u32 OSGetPhysicalMemSize() { + const auto info = static_cast(OSPhysicalToCached(0)); + return info->memorySize; +} diff --git a/aurora-main/lib/dolphin/os/OSTime.cpp b/aurora-main/lib/dolphin/os/OSTime.cpp new file mode 100644 index 0000000..37689ef --- /dev/null +++ b/aurora-main/lib/dolphin/os/OSTime.cpp @@ -0,0 +1,178 @@ +#include +#include + +#include "internal.hpp" +#include + +static const int YearDays[MONTH_MAX] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; + +static const int LeapYearDays[MONTH_MAX] = {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335}; + +namespace chrono = std::chrono; +using SystemTime = chrono::time_point; +using TickDuration = chrono::duration>; + +static const SystemTime startupTime = chrono::system_clock::now(); +static const chrono::time_point startupSteadyTime = chrono::steady_clock::now(); + +OSTick OSGetTick() { + return OSGetTime() & 0xFFFFFFFF; +} + +OSTime OSGetTime() { + // System time is provided in the number of timer ticks since 2000-01-01 00:00:00 + // Use time_t arithmetic to avoid chrono duration_cast overflow issues on some platforms. + + // GCN epoch: 2000-01-01 00:00:00 UTC = 946684800 seconds after Unix epoch + static constexpr s64 gcnEpochUnix = 946684800LL; + + // Get current wall-clock time + auto elapsed = chrono::steady_clock::now() - startupSteadyTime; + auto currentTime = startupTime + chrono::duration_cast(elapsed); + + // Convert to seconds since Unix epoch, then offset to GCN epoch + auto sinceUnix = chrono::duration_cast(currentTime.time_since_epoch()); + s64 totalMicros = sinceUnix.count(); + + // Apply local timezone offset + std::time_t wallClock = chrono::system_clock::to_time_t(currentTime); + std::tm localTm{}; + std::tm gmTm{}; +#if defined(_WIN32) + localtime_s(&localTm, &wallClock); + gmtime_s(&gmTm, &wallClock); +#else + localtime_r(&wallClock, &localTm); + gmtime_r(&wallClock, &gmTm); +#endif + // Compute UTC offset in seconds + s64 utcOffsetSec = static_cast(mktime(&localTm)) - static_cast(mktime(&gmTm)); + + s64 secondsSinceGcnEpoch = (totalMicros / 1000000LL) - gcnEpochUnix + utcOffsetSec; + s64 remainderMicros = totalMicros % 1000000LL; + + s64 ticksFromSeconds = secondsSinceGcnEpoch * static_cast(OS_TIMER_CLOCK); + s64 ticksFromRemainder = remainderMicros * static_cast(OS_TIMER_CLOCK) / 1000000LL; + + return ticksFromSeconds + ticksFromRemainder; +} + +void AuroraInitClock() { + if (OSBaseAddress == 0) { + return; + } + + __OSBusClock = OS_TIMER_CLOCK * OS_TIMER_CLOCK_DIVIDER; +} + + +static int IsLeapYear(int year) { + return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); +} + +static int GetYearDays(int year, int mon) { + const int* md = (IsLeapYear(year)) ? LeapYearDays : YearDays; + + return md[mon]; +} + +static int GetLeapDays(int year) { + ASSERT(0 <= year); + + if (year < 1) { + return 0; + } + return (year + 3) / 4 - (year - 1) / 100 + (year - 1) / 400; +} + +static void GetDates(int days, OSCalendarTime* td) { + int year; + int n; + int month; + const int* md; + + ASSERT(0 <= days); + + td->wday = (days + 6) % WEEK_DAY_MAX; + + for (year = days / YEAR_DAY_MAX; + days < (n = year * YEAR_DAY_MAX + GetLeapDays(year)); year--) { + ; + } + + days -= n; + td->year = year; + td->yday = days; + + md = IsLeapYear(year) ? LeapYearDays : YearDays; + for (month = MONTH_MAX; days < md[--month];) { + ; + } + td->mon = month; + td->mday = days - md[month] + 1; +} + +void OSTicksToCalendarTime(OSTime ticks, OSCalendarTime* td) { + int days; + int secs; + OSTime d; + + d = ticks % OS_SEC_TO_TICKS(1); + if (d < 0) { + d += OS_SEC_TO_TICKS(1); + ASSERT(0 <= d); + } + + td->usec = OS_TICKS_TO_USEC(d) % USEC_MAX; + td->msec = OS_TICKS_TO_MSEC(d) % MSEC_MAX; + + ASSERT(0 <= td->usec); + ASSERT(0 <= td->msec); + + ticks -= d; + + ASSERT(ticks % OSSecondsToTicks(1) == 0); + ASSERT(0 <= OSTicksToSeconds(ticks) / 86400 + BIAS && OSTicksToSeconds(ticks) / 86400 + BIAS <= INT_MAX); + + days = (OS_TICKS_TO_SEC(ticks) / SECS_IN_DAY) + BIAS; + secs = OS_TICKS_TO_SEC(ticks) % SECS_IN_DAY; + if (secs < 0) { + days -= 1; + secs += SECS_IN_DAY; + ASSERT(0 <= secs); + } + + GetDates(days, td); + td->hour = secs / 60 / 60; + td->min = secs / 60 % 60; + td->sec = secs % 60; +} + +OSTime OSCalendarTimeToTicks(OSCalendarTime* td) { + OSTime secs; + int ov_mon; + int mon; + int year; + + ov_mon = td->mon / MONTH_MAX; + mon = td->mon - (ov_mon * MONTH_MAX); + + if (mon < 0) { + mon += MONTH_MAX; + ov_mon--; + } + + ASSERT((ov_mon <= 0 && 0 <= td->year + ov_mon) || (0 < ov_mon && td->year <= INT_MAX - ov_mon)); + + year = td->year + ov_mon; + + secs = (OSTime)SECS_IN_YEAR * year + + (OSTime)SECS_IN_DAY * (GetLeapDays(year) + GetYearDays(year, mon) + td->mday - 1) + + (OSTime)SECS_IN_HOUR * td->hour + + (OSTime)SECS_IN_MIN * td->min + + td->sec - + (OSTime)0xEB1E1BF80ULL; + + return OS_SEC_TO_TICKS(secs) + OS_MSEC_TO_TICKS((OSTime)td->msec) + + OS_USEC_TO_TICKS((OSTime)td->usec); +} diff --git a/aurora-main/lib/dolphin/os/internal.hpp b/aurora-main/lib/dolphin/os/internal.hpp new file mode 100644 index 0000000..b4311ea --- /dev/null +++ b/aurora-main/lib/dolphin/os/internal.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "../../internal.hpp" +#include + +static aurora::Module Log("aurora::os"); + +constexpr uintptr_t ARENA_START_OFFSET = 0x4000; + +extern void* MEM1Start; +extern void* MEM1End; + +void AuroraOSInitMemory(); +void AuroraFillBootInfo(); +void AuroraInitClock(); +void AuroraInitArena(); diff --git a/aurora-main/lib/dolphin/pad/pad.cpp b/aurora-main/lib/dolphin/pad/pad.cpp new file mode 100644 index 0000000..6ae8ecb --- /dev/null +++ b/aurora-main/lib/dolphin/pad/pad.cpp @@ -0,0 +1,1729 @@ +#include "../../input.hpp" +#include "../../internal.hpp" +#include +#include +#include + +#include +#include +#include + +namespace { +constexpr int32_t k_mappingsFileVersion = 3; + +std::array g_defaultButtonsStandard{{ + {SDL_GAMEPAD_BUTTON_SOUTH, PAD_BUTTON_A}, + {SDL_GAMEPAD_BUTTON_EAST, PAD_BUTTON_B}, + {SDL_GAMEPAD_BUTTON_WEST, PAD_BUTTON_X}, + {SDL_GAMEPAD_BUTTON_NORTH, PAD_BUTTON_Y}, + {SDL_GAMEPAD_BUTTON_START, PAD_BUTTON_START}, + {SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, PAD_TRIGGER_Z}, + {PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_L}, + {PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_R}, + {SDL_GAMEPAD_BUTTON_DPAD_UP, PAD_BUTTON_UP}, + {SDL_GAMEPAD_BUTTON_DPAD_DOWN, PAD_BUTTON_DOWN}, + {SDL_GAMEPAD_BUTTON_DPAD_LEFT, PAD_BUTTON_LEFT}, + {SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT}, +}}; + +std::array g_defaultButtonsXBox360{{ + {SDL_GAMEPAD_BUTTON_SOUTH, PAD_BUTTON_A}, + {SDL_GAMEPAD_BUTTON_EAST, PAD_BUTTON_B}, + {SDL_GAMEPAD_BUTTON_WEST, PAD_BUTTON_X}, + {SDL_GAMEPAD_BUTTON_NORTH, PAD_BUTTON_Y}, + {SDL_GAMEPAD_BUTTON_START, PAD_BUTTON_START}, + {SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, PAD_TRIGGER_Z}, + {PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_L}, + {PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_R}, + {SDL_GAMEPAD_BUTTON_DPAD_UP, PAD_BUTTON_UP}, + {SDL_GAMEPAD_BUTTON_DPAD_DOWN, PAD_BUTTON_DOWN}, + {SDL_GAMEPAD_BUTTON_DPAD_LEFT, PAD_BUTTON_LEFT}, + {SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT}, +}}; + +std::array g_defaultButtonsXBoxOne{{ + {SDL_GAMEPAD_BUTTON_SOUTH, PAD_BUTTON_A}, + {SDL_GAMEPAD_BUTTON_EAST, PAD_BUTTON_B}, + {SDL_GAMEPAD_BUTTON_WEST, PAD_BUTTON_X}, + {SDL_GAMEPAD_BUTTON_NORTH, PAD_BUTTON_Y}, + {SDL_GAMEPAD_BUTTON_START, PAD_BUTTON_START}, + {SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, PAD_TRIGGER_Z}, + {PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_L}, + {PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_R}, + {SDL_GAMEPAD_BUTTON_DPAD_UP, PAD_BUTTON_UP}, + {SDL_GAMEPAD_BUTTON_DPAD_DOWN, PAD_BUTTON_DOWN}, + {SDL_GAMEPAD_BUTTON_DPAD_LEFT, PAD_BUTTON_LEFT}, + {SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT}, +}}; + +std::array g_defaultButtonsPS3{{ + {SDL_GAMEPAD_BUTTON_SOUTH, PAD_BUTTON_A}, + {SDL_GAMEPAD_BUTTON_EAST, PAD_BUTTON_B}, + {SDL_GAMEPAD_BUTTON_WEST, PAD_BUTTON_X}, + {SDL_GAMEPAD_BUTTON_NORTH, PAD_BUTTON_Y}, + {SDL_GAMEPAD_BUTTON_START, PAD_BUTTON_START}, + {SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, PAD_TRIGGER_Z}, + {PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_L}, + {PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_R}, + {SDL_GAMEPAD_BUTTON_DPAD_UP, PAD_BUTTON_UP}, + {SDL_GAMEPAD_BUTTON_DPAD_DOWN, PAD_BUTTON_DOWN}, + {SDL_GAMEPAD_BUTTON_DPAD_LEFT, PAD_BUTTON_LEFT}, + {SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT}, +}}; + +std::array g_defaultButtonsPS4{{ + {SDL_GAMEPAD_BUTTON_SOUTH, PAD_BUTTON_A}, + {SDL_GAMEPAD_BUTTON_EAST, PAD_BUTTON_B}, + {SDL_GAMEPAD_BUTTON_WEST, PAD_BUTTON_X}, + {SDL_GAMEPAD_BUTTON_NORTH, PAD_BUTTON_Y}, + {SDL_GAMEPAD_BUTTON_START, PAD_BUTTON_START}, + {SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, PAD_TRIGGER_Z}, + {PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_L}, + {PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_R}, + {SDL_GAMEPAD_BUTTON_DPAD_UP, PAD_BUTTON_UP}, + {SDL_GAMEPAD_BUTTON_DPAD_DOWN, PAD_BUTTON_DOWN}, + {SDL_GAMEPAD_BUTTON_DPAD_LEFT, PAD_BUTTON_LEFT}, + {SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT}, +}}; + +std::array g_defaultButtonsPS5{{ + {SDL_GAMEPAD_BUTTON_SOUTH, PAD_BUTTON_A}, + {SDL_GAMEPAD_BUTTON_EAST, PAD_BUTTON_B}, + {SDL_GAMEPAD_BUTTON_WEST, PAD_BUTTON_X}, + {SDL_GAMEPAD_BUTTON_NORTH, PAD_BUTTON_Y}, + {SDL_GAMEPAD_BUTTON_START, PAD_BUTTON_START}, + {SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, PAD_TRIGGER_Z}, + {PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_L}, + {PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_R}, + {SDL_GAMEPAD_BUTTON_DPAD_UP, PAD_BUTTON_UP}, + {SDL_GAMEPAD_BUTTON_DPAD_DOWN, PAD_BUTTON_DOWN}, + {SDL_GAMEPAD_BUTTON_DPAD_LEFT, PAD_BUTTON_LEFT}, + {SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT}, +}}; + +std::array g_defaultButtonsGamecube{{ + {SDL_GAMEPAD_BUTTON_SOUTH, PAD_BUTTON_A}, + {SDL_GAMEPAD_BUTTON_EAST, PAD_BUTTON_X}, + {SDL_GAMEPAD_BUTTON_WEST, PAD_BUTTON_B}, + {SDL_GAMEPAD_BUTTON_NORTH, PAD_BUTTON_Y}, + {SDL_GAMEPAD_BUTTON_START, PAD_BUTTON_START}, + {SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, PAD_TRIGGER_Z}, + {SDL_GAMEPAD_BUTTON_MISC3, PAD_TRIGGER_L}, + {SDL_GAMEPAD_BUTTON_MISC4, PAD_TRIGGER_R}, + {SDL_GAMEPAD_BUTTON_DPAD_UP, PAD_BUTTON_UP}, + {SDL_GAMEPAD_BUTTON_DPAD_DOWN, PAD_BUTTON_DOWN}, + {SDL_GAMEPAD_BUTTON_DPAD_LEFT, PAD_BUTTON_LEFT}, + {SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT}, +}}; + +std::array g_defaultButtonsNSOGamecube{{ + {SDL_GAMEPAD_BUTTON_SOUTH, PAD_BUTTON_A}, + {SDL_GAMEPAD_BUTTON_EAST, PAD_BUTTON_X}, + {SDL_GAMEPAD_BUTTON_WEST, PAD_BUTTON_B}, + {SDL_GAMEPAD_BUTTON_NORTH, PAD_BUTTON_Y}, + {SDL_GAMEPAD_BUTTON_START, PAD_BUTTON_START}, + {SDL_GAMEPAD_BUTTON_BACK, PAD_TRIGGER_Z}, + {SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, PAD_TRIGGER_L}, + {SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, PAD_TRIGGER_R}, + {SDL_GAMEPAD_BUTTON_DPAD_UP, PAD_BUTTON_UP}, + {SDL_GAMEPAD_BUTTON_DPAD_DOWN, PAD_BUTTON_DOWN}, + {SDL_GAMEPAD_BUTTON_DPAD_LEFT, PAD_BUTTON_LEFT}, + {SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT}, +}}; + +std::array g_defaultButtonsProCon{{ + {SDL_GAMEPAD_BUTTON_SOUTH, PAD_BUTTON_A}, + {SDL_GAMEPAD_BUTTON_EAST, PAD_BUTTON_B}, + {SDL_GAMEPAD_BUTTON_WEST, PAD_BUTTON_X}, + {SDL_GAMEPAD_BUTTON_NORTH, PAD_BUTTON_Y}, + {SDL_GAMEPAD_BUTTON_START, PAD_BUTTON_START}, + {SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, PAD_TRIGGER_Z}, + {PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_L}, + {PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_R}, + {SDL_GAMEPAD_BUTTON_DPAD_UP, PAD_BUTTON_UP}, + {SDL_GAMEPAD_BUTTON_DPAD_DOWN, PAD_BUTTON_DOWN}, + {SDL_GAMEPAD_BUTTON_DPAD_LEFT, PAD_BUTTON_LEFT}, + {SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT}, +}}; + +std::array g_defaultButtonsJoyConRight{{ + {SDL_GAMEPAD_BUTTON_SOUTH, PAD_BUTTON_A}, + {SDL_GAMEPAD_BUTTON_EAST, PAD_BUTTON_B}, + {SDL_GAMEPAD_BUTTON_WEST, PAD_BUTTON_X}, + {SDL_GAMEPAD_BUTTON_NORTH, PAD_BUTTON_Y}, + {SDL_GAMEPAD_BUTTON_START, PAD_BUTTON_START}, + {SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, PAD_TRIGGER_Z}, + {PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_L}, + {PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_R}, + {SDL_GAMEPAD_BUTTON_DPAD_UP, PAD_BUTTON_UP}, + {SDL_GAMEPAD_BUTTON_DPAD_DOWN, PAD_BUTTON_DOWN}, + {SDL_GAMEPAD_BUTTON_DPAD_LEFT, PAD_BUTTON_LEFT}, + {SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT}, +}}; + +std::array g_defaultButtonsJoyConLeft{{ + {SDL_GAMEPAD_BUTTON_SOUTH, PAD_BUTTON_A}, + {SDL_GAMEPAD_BUTTON_EAST, PAD_BUTTON_B}, + {SDL_GAMEPAD_BUTTON_WEST, PAD_BUTTON_X}, + {SDL_GAMEPAD_BUTTON_NORTH, PAD_BUTTON_Y}, + {SDL_GAMEPAD_BUTTON_START, PAD_BUTTON_START}, + {SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, PAD_TRIGGER_Z}, + {PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_L}, + {PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_R}, + {SDL_GAMEPAD_BUTTON_DPAD_UP, PAD_BUTTON_UP}, + {SDL_GAMEPAD_BUTTON_DPAD_DOWN, PAD_BUTTON_DOWN}, + {SDL_GAMEPAD_BUTTON_DPAD_LEFT, PAD_BUTTON_LEFT}, + {SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT}, +}}; + +std::array g_defaultButtonsJoyPair{{ + {SDL_GAMEPAD_BUTTON_SOUTH, PAD_BUTTON_A}, + {SDL_GAMEPAD_BUTTON_EAST, PAD_BUTTON_B}, + {SDL_GAMEPAD_BUTTON_WEST, PAD_BUTTON_X}, + {SDL_GAMEPAD_BUTTON_NORTH, PAD_BUTTON_Y}, + {SDL_GAMEPAD_BUTTON_START, PAD_BUTTON_START}, + {SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, PAD_TRIGGER_Z}, + {PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_L}, + {PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_R}, + {SDL_GAMEPAD_BUTTON_DPAD_UP, PAD_BUTTON_UP}, + {SDL_GAMEPAD_BUTTON_DPAD_DOWN, PAD_BUTTON_DOWN}, + {SDL_GAMEPAD_BUTTON_DPAD_LEFT, PAD_BUTTON_LEFT}, + {SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT}, +}}; + +std::array g_defaultKeys{{ + {PAD_KEY_INVALID, PAD_BUTTON_A}, + {PAD_KEY_INVALID, PAD_BUTTON_B}, + {PAD_KEY_INVALID, PAD_BUTTON_X}, + {PAD_KEY_INVALID, PAD_BUTTON_Y}, + {PAD_KEY_INVALID, PAD_BUTTON_START}, + {PAD_KEY_INVALID, PAD_TRIGGER_Z}, + {PAD_KEY_INVALID, PAD_TRIGGER_L}, + {PAD_KEY_INVALID, PAD_TRIGGER_R}, + {PAD_KEY_INVALID, PAD_BUTTON_UP}, + {PAD_KEY_INVALID, PAD_BUTTON_DOWN}, + {PAD_KEY_INVALID, PAD_BUTTON_LEFT}, + {PAD_KEY_INVALID, PAD_BUTTON_RIGHT}, +}}; + +std::array g_defaultKeyAxis{{ + {PAD_KEY_INVALID, PAD_AXIS_LEFT_X_POS, 0}, + {PAD_KEY_INVALID, PAD_AXIS_LEFT_X_NEG, 0}, + {PAD_KEY_INVALID, PAD_AXIS_LEFT_Y_POS, 0}, + {PAD_KEY_INVALID, PAD_AXIS_LEFT_Y_NEG, 0}, + {PAD_KEY_INVALID, PAD_AXIS_RIGHT_X_POS, 0}, + {PAD_KEY_INVALID, PAD_AXIS_RIGHT_X_NEG, 0}, + {PAD_KEY_INVALID, PAD_AXIS_RIGHT_Y_POS, 0}, + {PAD_KEY_INVALID, PAD_AXIS_RIGHT_Y_NEG, 0}, + {PAD_KEY_INVALID, PAD_AXIS_TRIGGER_L, 0}, + {PAD_KEY_INVALID, PAD_AXIS_TRIGGER_R, 0}, +}}; + +std::array g_defaultAxes{{ + {{SDL_GAMEPAD_AXIS_LEFTX, AXIS_SIGN_POSITIVE}, SDL_GAMEPAD_BUTTON_INVALID, PAD_AXIS_LEFT_X_POS}, + {{SDL_GAMEPAD_AXIS_LEFTX, AXIS_SIGN_NEGATIVE}, SDL_GAMEPAD_BUTTON_INVALID, PAD_AXIS_LEFT_X_NEG}, + // SDL's gamepad y-axis is inverted from GC's + {{SDL_GAMEPAD_AXIS_LEFTY, AXIS_SIGN_NEGATIVE}, SDL_GAMEPAD_BUTTON_INVALID, PAD_AXIS_LEFT_Y_POS}, + {{SDL_GAMEPAD_AXIS_LEFTY, AXIS_SIGN_POSITIVE}, SDL_GAMEPAD_BUTTON_INVALID, PAD_AXIS_LEFT_Y_NEG}, + {{SDL_GAMEPAD_AXIS_RIGHTX, AXIS_SIGN_POSITIVE}, SDL_GAMEPAD_BUTTON_INVALID, PAD_AXIS_RIGHT_X_POS}, + {{SDL_GAMEPAD_AXIS_RIGHTX, AXIS_SIGN_NEGATIVE}, SDL_GAMEPAD_BUTTON_INVALID, PAD_AXIS_RIGHT_X_NEG}, + // see above + {{SDL_GAMEPAD_AXIS_RIGHTY, AXIS_SIGN_NEGATIVE}, SDL_GAMEPAD_BUTTON_INVALID, PAD_AXIS_RIGHT_Y_POS}, + {{SDL_GAMEPAD_AXIS_RIGHTY, AXIS_SIGN_POSITIVE}, SDL_GAMEPAD_BUTTON_INVALID, PAD_AXIS_RIGHT_Y_NEG}, + {{SDL_GAMEPAD_AXIS_LEFT_TRIGGER, AXIS_SIGN_POSITIVE}, SDL_GAMEPAD_BUTTON_INVALID, PAD_AXIS_TRIGGER_L}, + {{SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, AXIS_SIGN_POSITIVE}, SDL_GAMEPAD_BUTTON_INVALID, PAD_AXIS_TRIGGER_R}, +}}; + +template +constexpr const std::array& toStdArray(const T (&array)[N]) { + static_assert(sizeof(array) == sizeof(std::array)); + return reinterpret_cast&>(array); +} + +struct PADKeyboardState { + std::array m_buttonMapping{}; + std::array m_axisMapping{}; + bool m_mappingsSet = false; +}; + +std::array g_keyboardBindings; + +struct PADCLampRegion { + uint8_t minTrigger; + uint8_t maxTrigger; + int8_t minStick; + int8_t maxStick; + int8_t xyStick; + int8_t minSubstick; + int8_t maxSubstick; + int8_t xySubstick; + int8_t radStick; + int8_t radSubstick; +}; + +constexpr PADCLampRegion ClampRegion{ + // Triggers + 30, + 180, + + // Left stick + 15, + 72, + 40, + + // Right stick + 15, + 59, + 31, + + // Stick radii + 56, + 44, +}; + +bool g_initialized; +bool g_keyboardBindingsLoaded = false; +bool g_blockPAD = false; +bool g_suppressHeldOnRead = false; +std::array g_suppressedButtons{}; +std::array g_suppressLeftTrigger{}; +std::array g_suppressRightTrigger{}; + +bool is_mouse_scancode(const s32 scancode) { return scancode < PAD_KEY_INVALID; } +bool is_mouse_button_pressed(const s32 scancode) { + const int32_t buttonNum = -(scancode + 1); + if (buttonNum < 1 || buttonNum > 5) { + return false; + } + float x, y; + const auto buttons = SDL_GetMouseState(&x, &y); + return (buttons & 1u << (buttonNum - 1)) != 0u; +} +} // namespace + +void PADSetSpec(u32 spec [[maybe_unused]]) {} + +static void load_keyboard_bindings(); +static void save_keyboard_bindings(); + +// ReSharper disable once CppDFAConstantFunctionResult +BOOL PADInit() { + if (g_initialized) { + return true; + } + g_initialized = true; + + std::ranges::for_each(g_keyboardBindings, [](auto& state) { + state.m_buttonMapping = g_defaultKeys; + state.m_axisMapping = g_defaultKeyAxis; + }); + + return true; +} + +BOOL PADRecalibrate(u32 mask [[maybe_unused]]) { return true; } + +BOOL PADReset(u32 mask [[maybe_unused]]) { return true; } + +void PADSetAnalogMode(u32 mode [[maybe_unused]]) {} + +aurora::input::GameController* __PADGetControllerForIndex(const u32 idx) /* NOLINT(*-reserved-identifier) */ +{ + if (idx >= aurora::input::g_GameControllers.size()) { + return nullptr; + } + + uint32_t tmp = 0; + auto iter = aurora::input::g_GameControllers.begin(); + while (tmp < idx) { + ++iter; + ++tmp; + } + if (iter == aurora::input::g_GameControllers.end()) { + return nullptr; + } + + return &iter->second; +} + +u32 PADCount() { return aurora::input::g_GameControllers.size(); } + +const char* PADGetNameForControllerIndex(const u32 idx) { + const auto* ctrl = __PADGetControllerForIndex(idx); + if (ctrl == nullptr) { + return nullptr; + } + + return SDL_GetGamepadName(ctrl->m_controller); +} + +void PADSetPortForIndex(const u32 idx, const u32 port) { + const auto* ctrl = __PADGetControllerForIndex(idx); + if (ctrl == nullptr) { + return; + } + + const int32_t oldPort = SDL_GetGamepadPlayerIndex(ctrl->m_controller); + if (const auto* dest = aurora::input::get_controller_for_player(port); dest != nullptr && dest != ctrl) { + SDL_SetGamepadPlayerIndex(dest->m_controller, -1); + } + if (oldPort >= 0 && oldPort != port) { + aurora::input::persist_controller_for_player(oldPort, nullptr); + } + SDL_SetGamepadPlayerIndex(ctrl->m_controller, static_cast(port)); + aurora::input::persist_controller_for_player(port, ctrl); +} + +int32_t PADGetIndexForPort(const u32 port) { + const auto* ctrl = aurora::input::get_controller_for_player(port); + if (ctrl == nullptr) { + return -1; + } + int32_t index = 0; + for (auto iter = aurora::input::g_GameControllers.begin(); iter != aurora::input::g_GameControllers.end(); + ++iter, ++index) { + if (&iter->second == ctrl) { + break; + } + } + + return index; +} + +void PADClearPort(const u32 port) { + aurora::input::persist_controller_for_player(port, nullptr); + const auto* ctrl = aurora::input::get_controller_for_player(port); + if (ctrl == nullptr) { + return; + } + SDL_SetGamepadPlayerIndex(ctrl->m_controller, -1); +} + +// Secondary bindings live only in memory; the runtime re-applies them from its +// config after every (re)load, so any mapping reset also clears them. +static void reset_alt_button_mapping(aurora::input::GameController* controller) { + for (size_t i = 0; i < PAD_BUTTON_COUNT; ++i) { + controller->m_altButtonMapping[i] = {PAD_NATIVE_BUTTON_INVALID, controller->m_buttonMapping[i].padButton}; + } +} + +void __PADSetDefaultMapping(aurora::input::GameController* controller) /* NOLINT(*-reserved-identifier) */ +{ + switch (SDL_GetGamepadType(controller->m_controller)) { + case SDL_GAMEPAD_TYPE_XBOX360: + controller->m_buttonMapping = g_defaultButtonsXBox360; + break; + case SDL_GAMEPAD_TYPE_XBOXONE: + controller->m_buttonMapping = g_defaultButtonsXBoxOne; + break; + case SDL_GAMEPAD_TYPE_STANDARD: + controller->m_buttonMapping = g_defaultButtonsStandard; + break; + case SDL_GAMEPAD_TYPE_PS3: + controller->m_buttonMapping = g_defaultButtonsPS3; + break; + case SDL_GAMEPAD_TYPE_PS4: + controller->m_buttonMapping = g_defaultButtonsPS4; + break; + case SDL_GAMEPAD_TYPE_PS5: + controller->m_buttonMapping = g_defaultButtonsPS5; + break; + case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO: + if (controller->m_pid == 0x2073) { + controller->m_buttonMapping = g_defaultButtonsNSOGamecube; + } else { + controller->m_buttonMapping = g_defaultButtonsProCon; + } + break; + case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT: + controller->m_buttonMapping = g_defaultButtonsJoyConRight; + break; + case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_LEFT: + controller->m_buttonMapping = g_defaultButtonsJoyConLeft; + break; + case SDL_GAMEPAD_TYPE_GAMECUBE: + controller->m_buttonMapping = g_defaultButtonsGamecube; + break; + default: + controller->m_buttonMapping = g_defaultButtonsStandard; + break; + } + reset_alt_button_mapping(controller); +} + +static bool is_valid_native_axis(const PADSignedNativeAxis axis) { + return axis.nativeAxis == -1 || + (axis.nativeAxis >= 0 && axis.nativeAxis < SDL_GAMEPAD_AXIS_COUNT && + (axis.sign == AXIS_SIGN_POSITIVE || axis.sign == AXIS_SIGN_NEGATIVE)); +} + +static PADDeadZones default_dead_zones(const aurora::input::GameController& controller) { + return { + .emulateTriggers = !(controller.m_isGameCube || + (SDL_GetGamepadType(controller.m_controller) == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO && + controller.m_pid == 0x2073)), + .useDeadzones = true, + .stickDeadZone = 8000, + .substickDeadZone = 8000, + .leftTriggerActivationZone = 31150, + .rightTriggerActivationZone = 31150, + }; +} + +static void sanitize_dead_zones(aurora::input::GameController* controller, const int32_t playerIndex) { + PADDeadZones& deadZones = controller->m_deadZones; + const bool invalid = + deadZones.stickDeadZone > SDL_JOYSTICK_AXIS_MAX || deadZones.substickDeadZone > SDL_JOYSTICK_AXIS_MAX || + deadZones.leftTriggerActivationZone > SDL_JOYSTICK_AXIS_MAX || + deadZones.rightTriggerActivationZone > SDL_JOYSTICK_AXIS_MAX; + if (!invalid) { + return; + } + + aurora::input::Log.warn("__PADLoadMapping port={}: invalid deadzone data in file, resetting to defaults", + playerIndex); + deadZones = default_dead_zones(*controller); +} + +void __PADLoadMapping(aurora::input::GameController* controller) /* NOLINT(*-reserved-identifier) */ { + int32_t playerIndex = SDL_GetGamepadPlayerIndex(controller->m_controller); + if (playerIndex == -1) { + return; + } + + std::string basePath{aurora::g_config.userPath}; + if (!controller->m_mappingLoaded) { + __PADSetDefaultMapping(controller); + controller->m_axisMapping = g_defaultAxes; + } + + controller->m_mappingLoaded = true; + + const auto path = fmt::format("{}/{}_{:04X}_{:04X}.controller", basePath, PADGetName(playerIndex), controller->m_vid, + controller->m_pid); + SDL_IOStream* file = SDL_IOFromFile(path.c_str(), "rb"); + if (file == nullptr) { + return; + } + + uint32_t magic = 0; + SDL_ReadU32LE(file, &magic); + if (magic != SBIG('CTRL')) { + aurora::input::Log.warn("Invalid controller mapping magic!"); + SDL_CloseIO(file); + return; + } + + uint32_t version = 0; + SDL_ReadU32LE(file, &version); + if (version != k_mappingsFileVersion) { + aurora::input::Log.warn("Invalid controller mapping version! (Expected {0}, found {1})", k_mappingsFileVersion, + version); + SDL_CloseIO(file); + return; + } + + bool isGameCube = false; + SDL_ReadIO(file, &isGameCube, sizeof(bool)); + SDL_SeekIO(file, SDL_TellIO(file) + 31 & ~31, SDL_IO_SEEK_SET); + const auto dataStart = SDL_TellIO(file); + if (dataStart == -1) { + aurora::input::Log.warn("Unable to seek in controller bindings! Path: \"{}\"", path); + SDL_CloseIO(file); + return; + } + if (isGameCube) { + constexpr uint32_t dzSecLen = sizeof(PADDeadZones); + constexpr uint32_t btnSecLen = sizeof(PADButtonMapping) * PAD_BUTTON_COUNT; + constexpr uint32_t axisSecLen = sizeof(PADAxisMapping) * PAD_AXIS_COUNT; + SDL_SeekIO(file, dataStart + (dzSecLen + btnSecLen + axisSecLen) * playerIndex, SDL_IO_SEEK_SET); + } + + SDL_ReadIO(file, &controller->m_deadZones, sizeof(PADDeadZones)); + SDL_ReadIO(file, &controller->m_buttonMapping, sizeof(PADButtonMapping) * PAD_BUTTON_COUNT); + SDL_ReadIO(file, &controller->m_axisMapping, sizeof(PADAxisMapping) * PAD_AXIS_COUNT); + if (!isGameCube) { + SDL_ReadIO(file, &controller->m_rumbleIntensityLow, sizeof(u16)); + SDL_ReadIO(file, &controller->m_rumbleIntensityHigh, sizeof(u16)); + } + SDL_CloseIO(file); + sanitize_dead_zones(controller, playerIndex); + + bool axisCorrupt = false; + for (uint32_t i = 0; i < PAD_AXIS_COUNT; ++i) { + if (controller->m_axisMapping[i].padAxis != static_cast(i) || + !is_valid_native_axis(controller->m_axisMapping[i].nativeAxis)) { + axisCorrupt = true; + break; + } + } + if (axisCorrupt) { + aurora::input::Log.warn("__PADLoadMapping port={}: corrupt axis data in file, resetting axes to defaults", + playerIndex); + controller->m_axisMapping = g_defaultAxes; + } + + bool buttonCorrupt = false; + for (uint32_t i = 0; i < PAD_BUTTON_COUNT; ++i) { + if (controller->m_buttonMapping[i].padButton == 0) { + buttonCorrupt = true; + break; + } + } + if (buttonCorrupt) { + aurora::input::Log.warn("__PADLoadMapping port={}: corrupt button data in file, resetting buttons to defaults", + playerIndex); + __PADSetDefaultMapping(controller); + } + reset_alt_button_mapping(controller); +} + +static void EnsureMappingLoaded(aurora::input::GameController* controller) { + if (!controller->m_mappingLoaded) { + __PADLoadMapping(controller); + } +} + +static Sint16 _get_axis_value(const aurora::input::GameController* controller, // NOLINT(*-reserved-identifier) + PADAxis axis) { + const auto iter = + std::ranges::find_if(controller->m_axisMapping, [axis](const auto& pair) { return pair.padAxis == axis; }); + if (iter == controller->m_axisMapping.end()) { + return 0; + } + + if (iter->nativeAxis.nativeAxis != -1) { + const auto [nativeAxis, sign] = iter->nativeAxis; + const auto value = SDL_GetGamepadAxis(controller->m_controller, static_cast(nativeAxis)); + if (sign == AXIS_SIGN_POSITIVE) { + return value > 0 ? value : 0; + } + if (value >= 0) { + return 0; + } + // Clamp before negating so SDL's -32768 minimum fits in Sint16. + return static_cast(value == SDL_JOYSTICK_AXIS_MIN ? SDL_JOYSTICK_AXIS_MAX : -value); + } + + assert(iter->nativeButton != -1); + if (SDL_GetGamepadButton(controller->m_controller, static_cast(iter->nativeButton))) { + return SDL_JOYSTICK_AXIS_MAX; + } + return 0; +} + +static void neutralize_status(PADStatus& status) { + status.button = 0; + status.stickX = 0; + status.stickY = 0; + status.substickX = 0; + status.substickY = 0; + status.triggerLeft = 0; + status.triggerRight = 0; + status.analogA = 0; + status.analogB = 0; +} + +static void apply_unblock_suppression(PADStatus& status, const u32 port, const bool captureHeldInput) { + if (captureHeldInput) { + g_suppressedButtons[port] |= status.button; + g_suppressLeftTrigger[port] = g_suppressLeftTrigger[port] || status.triggerLeft > ClampRegion.minTrigger; + g_suppressRightTrigger[port] = g_suppressRightTrigger[port] || status.triggerRight > ClampRegion.minTrigger; + } + + g_suppressedButtons[port] &= status.button; + status.button &= ~g_suppressedButtons[port]; + + if (g_suppressLeftTrigger[port]) { + if (status.triggerLeft <= ClampRegion.minTrigger) { + g_suppressLeftTrigger[port] = false; + } else { + status.triggerLeft = 0; + } + } + + if (g_suppressRightTrigger[port]) { + if (status.triggerRight <= ClampRegion.minTrigger) { + g_suppressRightTrigger[port] = false; + } else { + status.triggerRight = 0; + } + } +} + +u32 PADRead(PADStatus* status) { + if (!g_keyboardBindingsLoaded) { + g_keyboardBindingsLoaded = true; + load_keyboard_bindings(); + } + + int numKeys = 0; + const bool* kbState = SDL_GetKeyboardState(&numKeys); + const bool captureHeldInput = g_suppressHeldOnRead && !g_blockPAD; + g_suppressHeldOnRead = false; + + uint32_t rumbleSupport = 0; + for (uint32_t i = 0; i < PAD_CHANMAX; ++i) { + memset(&status[i], 0, sizeof(PADStatus)); + auto controller = aurora::input::get_controller_for_player(i); + if (controller == nullptr && !g_keyboardBindings[i].m_mappingsSet) { + status[i].err = PAD_ERR_NO_CONTROLLER; + g_suppressedButtons[i] = 0; + g_suppressLeftTrigger[i] = false; + g_suppressRightTrigger[i] = false; + continue; + } + + status[i].err = PAD_ERR_NONE; + if (g_keyboardBindings[i].m_mappingsSet) { + std::ranges::for_each( + g_keyboardBindings[i].m_buttonMapping, [&kbState, &i, &status](const PADKeyButtonBinding& mapping) { + if (mapping.scancode > PAD_KEY_INVALID && kbState[mapping.scancode]) { + status[i].button |= mapping.padButton; + } else if (is_mouse_scancode(mapping.scancode) && is_mouse_button_pressed(mapping.scancode)) { + status[i].button |= mapping.padButton; + } + }); + + int lx = 0, ly = 0, rx = 0, ry = 0, tl = 0, tr = 0; + for (const auto& binding : g_keyboardBindings[i].m_axisMapping) { + bool pressed = false; + if (binding.scancode > PAD_KEY_INVALID) { + pressed = binding.scancode < numKeys && kbState[binding.scancode]; + } else if (is_mouse_scancode(binding.scancode)) { + pressed = is_mouse_button_pressed(binding.scancode); + } + if (!pressed) { + continue; + } + switch (binding.padAxis) { + case PAD_AXIS_LEFT_X_POS: + lx += 127; + break; + case PAD_AXIS_LEFT_X_NEG: + lx -= 127; + break; + case PAD_AXIS_LEFT_Y_POS: + ly += 127; + break; + case PAD_AXIS_LEFT_Y_NEG: + ly -= 127; + break; + case PAD_AXIS_RIGHT_X_POS: + rx += 127; + break; + case PAD_AXIS_RIGHT_X_NEG: + rx -= 127; + break; + case PAD_AXIS_RIGHT_Y_POS: + ry += 127; + break; + case PAD_AXIS_RIGHT_Y_NEG: + ry -= 127; + break; + case PAD_AXIS_TRIGGER_L: + tl += 255; + break; + case PAD_AXIS_TRIGGER_R: + tr += 255; + break; + default: + break; + } + } + status[i].stickX = static_cast(std::clamp(static_cast(status[i].stickX) + lx, -127, 127)); + status[i].stickY = static_cast(std::clamp(static_cast(status[i].stickY) + ly, -127, 127)); + status[i].substickX = static_cast(std::clamp(static_cast(status[i].substickX) + rx, -127, 127)); + status[i].substickY = static_cast(std::clamp(static_cast(status[i].substickY) + ry, -127, 127)); + status[i].triggerLeft = static_cast(std::min(static_cast(status[i].triggerLeft) + tl, 255)); + status[i].triggerRight = static_cast(std::min(static_cast(status[i].triggerRight) + tr, 255)); + } + + if (controller) { + EnsureMappingLoaded(controller); + bool leftTriggerSet = false; + bool rightTriggerSet = false; + std::ranges::for_each(controller->m_buttonMapping, [&controller, &i, &status, &leftTriggerSet, + &rightTriggerSet](const auto& mapping) { + if (SDL_GetGamepadButton(controller->m_controller, static_cast(mapping.nativeButton))) { + status[i].button |= mapping.padButton; + } + + if (mapping.padButton == PAD_TRIGGER_L && mapping.nativeButton != PAD_NATIVE_BUTTON_INVALID) { + leftTriggerSet = true; + } + if (mapping.padButton == PAD_TRIGGER_R && mapping.nativeButton != PAD_NATIVE_BUTTON_INVALID) { + rightTriggerSet = true; + } + }); + + std::ranges::for_each(controller->m_altButtonMapping, [&controller, &i, &status, &leftTriggerSet, + &rightTriggerSet](const auto& mapping) { + if (mapping.nativeButton == PAD_NATIVE_BUTTON_INVALID) { + return; + } + if (SDL_GetGamepadButton(controller->m_controller, static_cast(mapping.nativeButton))) { + status[i].button |= mapping.padButton; + } + + if (mapping.padButton == PAD_TRIGGER_L) { + leftTriggerSet = true; + } + if (mapping.padButton == PAD_TRIGGER_R) { + rightTriggerSet = true; + } + }); + + // TODO: Add serializable mappings for these (probably not necessary)? + static constexpr std::array, PAD_EXT_BUTTON_COUNT> kExtButtonMappings{{ + {SDL_GAMEPAD_BUTTON_BACK, PAD_BUTTON_BACK}, + {SDL_GAMEPAD_BUTTON_GUIDE, PAD_BUTTON_GUIDE}, + {SDL_GAMEPAD_BUTTON_MISC1, PAD_BUTTON_MISC1}, + {SDL_GAMEPAD_BUTTON_MISC2, PAD_BUTTON_MISC2}, + {SDL_GAMEPAD_BUTTON_MISC3, PAD_BUTTON_MISC3}, + {SDL_GAMEPAD_BUTTON_MISC4, PAD_BUTTON_MISC4}, + {SDL_GAMEPAD_BUTTON_MISC5, PAD_BUTTON_MISC5}, + {SDL_GAMEPAD_BUTTON_MISC6, PAD_BUTTON_MISC6}, + {SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1, PAD_BUTTON_RIGHT_PADDLE1}, + {SDL_GAMEPAD_BUTTON_LEFT_PADDLE1, PAD_BUTTON_LEFT_PADDLE1}, + {SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2, PAD_BUTTON_RIGHT_PADDLE2}, + {SDL_GAMEPAD_BUTTON_LEFT_PADDLE2, PAD_BUTTON_LEFT_PADDLE2}, + {SDL_GAMEPAD_BUTTON_RIGHT_STICK, PAD_BUTTON_RIGHT_STICK}, + {SDL_GAMEPAD_BUTTON_LEFT_STICK, PAD_BUTTON_LEFT_STICK}, + {SDL_GAMEPAD_BUTTON_TOUCHPAD, PAD_BUTTON_TOUCHPAD}, + }}; + + for (const auto& [native, button] : kExtButtonMappings) { + if (SDL_GetGamepadButton(controller->m_controller, native)) { + status[i].extButton |= button; + } + } + + const auto xlPos = _get_axis_value(controller, PAD_AXIS_LEFT_X_POS); + const auto xlNeg = _get_axis_value(controller, PAD_AXIS_LEFT_X_NEG); + const auto ylPos = _get_axis_value(controller, PAD_AXIS_LEFT_Y_POS); + const auto ylNeg = _get_axis_value(controller, PAD_AXIS_LEFT_Y_NEG); + + auto xl = static_cast(xlPos - xlNeg); + auto yl = static_cast(ylPos - ylNeg); + if (controller->m_deadZones.useDeadzones) { + if (std::abs(xl) > controller->m_deadZones.stickDeadZone) { + xl /= 256; + } else { + xl = 0; + } + if (std::abs(yl) > controller->m_deadZones.stickDeadZone) { + yl /= 256; + } else { + yl = 0; + } + } else { + xl /= 256; + yl /= 256; + } + + status[i].stickX = static_cast(xl); + status[i].stickY = static_cast(yl); + + const auto xrPos = _get_axis_value(controller, PAD_AXIS_RIGHT_X_POS); + const auto xrNeg = _get_axis_value(controller, PAD_AXIS_RIGHT_X_NEG); + const auto yrPos = _get_axis_value(controller, PAD_AXIS_RIGHT_Y_POS); + const auto yrNeg = _get_axis_value(controller, PAD_AXIS_RIGHT_Y_NEG); + + auto xr = static_cast(xrPos - xrNeg); + auto yr = static_cast(yrPos - yrNeg); + if (controller->m_deadZones.useDeadzones) { + if (std::abs(xr) > controller->m_deadZones.substickDeadZone) { + xr /= 256; + } else { + xr = 0; + } + + if (std::abs(yr) > controller->m_deadZones.substickDeadZone) { + yr /= 256; + } else { + yr = 0; + } + } else { + xr /= 256; + yr /= 256; + } + + status[i].substickX = static_cast(xr); + status[i].substickY = static_cast(yr); + + Sint16 tl = std::max(static_cast(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_L)); + Sint16 tr = std::max(static_cast(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_R)); + + if (controller->m_deadZones.emulateTriggers) { + if (!leftTriggerSet && tl > controller->m_deadZones.leftTriggerActivationZone) { + status[i].button |= PAD_TRIGGER_L; + } + if (!rightTriggerSet && tr > controller->m_deadZones.rightTriggerActivationZone) { + status[i].button |= PAD_TRIGGER_R; + } + } + tl /= 128; + tr /= 128; + + status[i].triggerLeft = static_cast(tl); + status[i].triggerRight = static_cast(tr); + + if (controller->m_hasRumble) { + rumbleSupport |= PAD_CHAN0_BIT >> i; + } + + // Update the LED colors when they exist and the controller is read (which should happen once per frame in most + // games) + if (controller->m_hasRgbLed && controller->m_isColorDirty) { + SDL_SetGamepadLED(controller->m_controller, controller->m_ledRed, controller->m_ledGreen, + controller->m_ledBlue); + controller->m_isColorDirty = false; + } + } + + if (g_blockPAD) { + neutralize_status(status[i]); + } else { + apply_unblock_suppression(status[i], i, captureHeldInput); + } + } + return rumbleSupport; +} + +void PADControlMotor(const u32 chan, const u32 cmd) { + const auto controller = aurora::input::get_controller_for_player(chan); + const auto instance = aurora::input::get_instance_for_player(chan); + if (controller == nullptr) { + return; + } + + if (controller->m_isGameCube) { + if (cmd == PAD_MOTOR_STOP) { + aurora::input::controller_rumble(instance, 0, 1, 0); + } else if (cmd == PAD_MOTOR_RUMBLE) { + aurora::input::controller_rumble(instance, 1, 1, 0); + } else if (cmd == PAD_MOTOR_STOP_HARD) { + aurora::input::controller_rumble(instance, 0, 0, 0); + } + } else { + if (cmd == PAD_MOTOR_STOP) { + aurora::input::controller_rumble(instance, 0, 0, 1); + } else if (cmd == PAD_MOTOR_RUMBLE) { + aurora::input::controller_rumble(instance, controller->m_rumbleIntensityLow, controller->m_rumbleIntensityHigh, + 0); + } else if (cmd == PAD_MOTOR_STOP_HARD) { + aurora::input::controller_rumble(instance, 0, 0, 0); + } + } +} + +void PADControlAllMotors(const u32* cmdArr) { + for (u32 i = 0; i < PAD_CHANMAX; ++i) { + PADControlMotor(i, cmdArr[i]); + } +} + +void ClampTrigger(u8* trigger, const u8 min, const u8 max) { + if (*trigger <= min) { + *trigger = 0; + } else { + if (*trigger > max) { + *trigger = max; + } + *trigger -= min; + } +} + +void ClampCircle(s8* px, s8* py, const s8 radius, const s8 min) { + int x = *px; // NOLINT(*-str34-c) + int y = *py; // NOLINT(*-str34-c) + + if (-min < x && x < min) { + x = 0; + } else if (0 < x) { + x -= min; + } else { + x += min; + } + + if (-min < y && y < min) { + y = 0; + } else if (0 < y) { + y -= min; + } else { + y += min; + } + + if (const int squared = x * x + y * y; radius * radius < squared) { + const auto length = static_cast(std::sqrt(squared)); + x = x * radius / length; + y = y * radius / length; + } + + *px = static_cast(x); + *py = static_cast(y); +} + +void ClampStick(s8* px, s8* py, const s8 max, const s8 xy, const s8 min) { + int32_t x = *px; // NOLINT(*-str34-c) + int32_t y = *py; // NOLINT(*-str34-c) + + int32_t signX = 0; + if (0 <= x) { + signX = 1; + } else { + signX = -1; + x = -x; + } + + int8_t signY = 0; + if (0 <= y) { + signY = 1; + } else { + signY = -1; + y = -y; + } + + if (x <= min) { + x = 0; + } else { + x -= min; + } + if (y <= min) { + y = 0; + } else { + y -= min; + } + + if (x == 0 && y == 0) { + *px = *py = 0; + return; + } + + x = x * max / (INT8_MAX - min); + y = y * max / (INT8_MAX - min); + + if (xy * y <= xy * x) { + if (const int32_t d = xy * x + (max - xy) * y; xy * max < d) { + x = xy * max * x / d; + y = xy * max * y / d; + } + } else { + if (const int32_t d = xy * y + (max - xy) * x; xy * max < d) { + x = xy * max * x / d; + y = xy * max * y / d; + } + } + + *px = static_cast(signX * x); + *py = static_cast(signY * y); +} + +void PADClamp(PADStatus* status) { + for (uint32_t i = 0; i < PAD_CHANMAX; ++i) { + if (status[i].err != PAD_ERR_NONE) { + continue; + } + + ClampStick(&status[i].stickX, &status[i].stickY, ClampRegion.maxStick, ClampRegion.xyStick, ClampRegion.minStick); + ClampStick(&status[i].substickX, &status[i].substickY, ClampRegion.maxSubstick, ClampRegion.xySubstick, + ClampRegion.minSubstick); + ClampTrigger(&status[i].triggerLeft, ClampRegion.minTrigger, ClampRegion.maxTrigger); + ClampTrigger(&status[i].triggerRight, ClampRegion.minTrigger, ClampRegion.maxTrigger); + } +} + +void PADClampCircle(PADStatus* status) { + for (uint32_t i = 0; i < PAD_CHANMAX; ++i) { + if (status[i].err != PAD_ERR_NONE) { + continue; + } + + ClampCircle(&status[i].stickX, &status[i].stickY, ClampRegion.radStick, ClampRegion.minStick); + ClampCircle(&status[i].substickX, &status[i].substickY, ClampRegion.radSubstick, ClampRegion.minSubstick); + ClampTrigger(&status[i].triggerLeft, ClampRegion.minTrigger, ClampRegion.maxTrigger); + ClampTrigger(&status[i].triggerRight, ClampRegion.minTrigger, ClampRegion.maxTrigger); + } +} + +void PADGetVidPid(const u32 port, u32* vid, u32* pid) { + *vid = 0; + *pid = 0; + const auto* controller = aurora::input::get_controller_for_player(port); + if (controller == nullptr) { + return; + } + + *vid = controller->m_vid; + *pid = controller->m_pid; +} + +const char* PADGetName(const u32 port) { + const auto* controller = aurora::input::get_controller_for_player(port); + if (controller == nullptr) { + return nullptr; + } + + return SDL_GetGamepadName(controller->m_controller); +} + +void PADSetButtonMapping(const u32 port, const PADButtonMapping mapping) { + auto* controller = aurora::input::get_controller_for_player(port); + if (controller == nullptr) { + return; + } + + const auto iter = std::ranges::find_if(controller->m_buttonMapping, + [mapping](const auto& pair) { return mapping.padButton == pair.padButton; }); + if (iter == controller->m_buttonMapping.end()) { + return; + } + + *iter = mapping; +} + +void PADSetAllButtonMappings(const u32 port, const PADButtonMapping buttons[PAD_BUTTON_COUNT]) { + for (uint32_t i = 0; i < PAD_BUTTON_COUNT; ++i) { + PADSetButtonMapping(port, buttons[i]); + } +} + +PADButtonMapping* PADGetButtonMappings(const u32 port, u32* buttonCount) { + auto* controller = aurora::input::get_controller_for_player(port); + if (controller == nullptr) { + *buttonCount = 0; + return nullptr; + } + + EnsureMappingLoaded(controller); + + *buttonCount = PAD_BUTTON_COUNT; + return controller->m_buttonMapping.data(); +} + +void PADSetAltButtonMapping(const u32 port, const PADButtonMapping mapping) { + auto* controller = aurora::input::get_controller_for_player(port); + if (controller == nullptr) { + return; + } + + const auto iter = std::ranges::find_if(controller->m_altButtonMapping, + [mapping](const auto& pair) { return mapping.padButton == pair.padButton; }); + if (iter == controller->m_altButtonMapping.end()) { + return; + } + + *iter = mapping; +} + +PADButtonMapping* PADGetAltButtonMappings(const u32 port, u32* buttonCount) { + auto* controller = aurora::input::get_controller_for_player(port); + if (controller == nullptr) { + *buttonCount = 0; + return nullptr; + } + + EnsureMappingLoaded(controller); + *buttonCount = PAD_BUTTON_COUNT; + return controller->m_altButtonMapping.data(); +} + +void PADSetAxisMapping(const u32 port, const PADAxisMapping mapping) { + auto* controller = aurora::input::get_controller_for_player(port); + if (controller == nullptr) { + return; + } + + const auto iter = std::ranges::find_if(controller->m_axisMapping, + [mapping](const auto& pair) { return mapping.padAxis == pair.padAxis; }); + if (iter == controller->m_axisMapping.end()) { + return; + } + + *iter = mapping; +} + +void PADSetAllAxisMappings(const u32 port, const PADAxisMapping axes[PAD_AXIS_COUNT]) { + for (uint32_t i = 0; i < PAD_AXIS_COUNT; ++i) { + PADSetAxisMapping(port, axes[i]); + } +} + +PADAxisMapping* PADGetAxisMappings(const u32 port, u32* axisCount) { + auto* controller = aurora::input::get_controller_for_player(port); + if (controller == nullptr) { + *axisCount = 0; + return nullptr; + } + + EnsureMappingLoaded(controller); + *axisCount = PAD_AXIS_COUNT; + return controller->m_axisMapping.data(); +} + +BOOL PADSetKeyButtonBinding(const u32 port, const PADKeyButtonBinding binding) { + if (port >= PAD_MAX_CONTROLLERS) { + return FALSE; + } + + for (auto& state = g_keyboardBindings[port]; auto& [scancode, padButton] : state.m_buttonMapping) { + if (padButton == binding.padButton) { + scancode = binding.scancode; + return TRUE; + } + } + + return FALSE; +} + +BOOL PADSetKeyButtonBindings(const u32 port, PADKeyButtonBinding bindings[PAD_BUTTON_COUNT]) { + for (uint32_t i = 0; i < PAD_BUTTON_COUNT; ++i) { + if (!PADSetKeyButtonBinding(port, bindings[i])) { + return FALSE; + } + } + return TRUE; +} + +PADKeyButtonBinding* PADGetKeyButtonBindings(const u32 port, u32* buttonCount) { + if (port >= PAD_MAX_CONTROLLERS || !g_keyboardBindings[port].m_mappingsSet) { + return nullptr; + } + auto& state = g_keyboardBindings[port]; + *buttonCount = PAD_BUTTON_COUNT; + return state.m_buttonMapping.data(); +} + +BOOL PADSetKeyAxisBinding(const u32 port, const PADKeyAxisBinding binding) { + if (port >= PAD_MAX_CONTROLLERS) { + return FALSE; + } + + for (auto& state = g_keyboardBindings[port]; auto& b : state.m_axisMapping) { + if (b.padAxis == binding.padAxis) { + b.scancode = binding.scancode; + return TRUE; + } + } + + return FALSE; +} +BOOL PADSetKeyAxisBindings(const u32 port, PADKeyAxisBinding bindings[PAD_BUTTON_COUNT]) { + for (uint32_t i = 0; i < PAD_AXIS_COUNT; ++i) { + if (!PADSetKeyAxisBinding(port, bindings[i])) { + return FALSE; + } + } + return TRUE; +} + +PADKeyAxisBinding* PADGetKeyAxisBindings(const u32 port, u32* axisCount) { + if (port >= PAD_MAX_CONTROLLERS || !g_keyboardBindings[port].m_mappingsSet) { + return nullptr; + } + auto& state = g_keyboardBindings[port]; + *axisCount = PAD_AXIS_COUNT; + return state.m_axisMapping.data(); +} + +void PADSetKeyboardActive(const u32 port, const BOOL active) { + if (port >= PAD_MAX_CONTROLLERS) { + return; + } + g_keyboardBindings[port].m_mappingsSet = active != FALSE; +} + +void PADClearKeyBindings(const u32 port) { + if (port >= PAD_MAX_CONTROLLERS) { + return; + } + g_keyboardBindings[port].m_buttonMapping = g_defaultKeys; + g_keyboardBindings[port].m_axisMapping = g_defaultKeyAxis; + g_keyboardBindings[port].m_mappingsSet = false; +} + +constexpr uint32_t k_keyboardMagic = SBIG('KBND'); +constexpr int32_t k_keyboardVersion = 3; + +static void load_keyboard_bindings() { + const auto filePath = std::filesystem::path{aurora::g_config.userPath} / "keyboard_bindings.dat"; + SDL_IOStream* file = SDL_IOFromFile(filePath.string().c_str(), "rb"); + if (file == nullptr) { + return; + } + + uint32_t magic = 0; + SDL_ReadU32LE(file, &magic); + if (magic != k_keyboardMagic) { + aurora::input::Log.warn("keyboard_bindings.dat: invalid magic"); + SDL_CloseIO(file); + return; + } + + uint32_t version = 0; + SDL_ReadU32LE(file, &version); + if (version != k_keyboardVersion) { + aurora::input::Log.warn("keyboard_bindings.dat: version mismatch (expected {}, got {})", k_keyboardVersion, + version); + SDL_CloseIO(file); + return; + } + + const int64_t dataStart = SDL_TellIO(file) + 31 & ~31; + SDL_SeekIO(file, dataStart, SDL_IO_SEEK_SET); + + for (uint32_t port = 0; port < g_keyboardBindings.size(); ++port) { + auto& [buttonMapping, axisMapping, mappingsSet] = g_keyboardBindings[port]; + SDL_ReadIO(file, &mappingsSet, sizeof(bool)); + SDL_ReadIO(file, buttonMapping.data(), sizeof(PADKeyButtonBinding) * PAD_BUTTON_COUNT); + SDL_ReadIO(file, axisMapping.data(), sizeof(PADKeyAxisBinding) * PAD_AXIS_COUNT); + + bool kbButtonCorrupt = false; + for (uint32_t i = 0; i < PAD_BUTTON_COUNT; ++i) { + if (buttonMapping[i].padButton != g_defaultKeys[i].padButton) { + kbButtonCorrupt = true; + break; + } + } + if (kbButtonCorrupt) { + aurora::input::Log.warn("keyboard_bindings.dat port={}: corrupt button identifiers, resetting to defaults", port); + buttonMapping = g_defaultKeys; + } + + bool kbAxisCorrupt = false; + for (uint32_t i = 0; i < PAD_AXIS_COUNT; ++i) { + if (axisMapping[i].padAxis != g_defaultKeyAxis[i].padAxis) { + kbAxisCorrupt = true; + break; + } + } + if (kbAxisCorrupt) { + aurora::input::Log.warn("keyboard_bindings.dat port={}: corrupt axis identifiers, resetting to defaults", port); + axisMapping = g_defaultKeyAxis; + } + + if (mappingsSet) { + const bool anyBound = + std::ranges::any_of(buttonMapping, + [](const PADKeyButtonBinding& b) { return b.scancode != PAD_KEY_INVALID; }) || + std::ranges::any_of(axisMapping, [](const PADKeyAxisBinding& b) { return b.scancode != PAD_KEY_INVALID; }); + if (!anyBound) { + mappingsSet = false; + } + } + } + SDL_CloseIO(file); +} + +static void save_keyboard_bindings() { + const auto filePath = std::filesystem::path{aurora::g_config.userPath} / "keyboard_bindings.dat"; + SDL_IOStream* file = SDL_IOFromFile(filePath.string().c_str(), "wb"); + if (file == nullptr) { + aurora::input::Log.warn("save_keyboard_bindings: failed to open {} for writing", filePath.string()); + return; + } + + SDL_WriteU32LE(file, k_keyboardMagic); + SDL_WriteS32LE(file, k_keyboardVersion); + + const int64_t dataStart = SDL_TellIO(file) + 31 & ~31; + SDL_SeekIO(file, dataStart, SDL_IO_SEEK_SET); + + for (const auto& [buttonMapping, axisMapping, mappingsSet] : g_keyboardBindings) { + SDL_WriteU8(file, mappingsSet); + SDL_WriteIO(file, buttonMapping.data(), sizeof(PADKeyButtonBinding) * PAD_BUTTON_COUNT); + SDL_WriteIO(file, axisMapping.data(), sizeof(PADKeyAxisBinding) * PAD_AXIS_COUNT); + } + SDL_CloseIO(file); +} + +void __PADWriteDeadZones(SDL_IOStream* file, // NOLINT(*-reserved-identifier) + const aurora::input::GameController& controller) { + SDL_WriteIO(file, &controller.m_deadZones, sizeof(PADDeadZones)); +} + +void PADSerializeMappings() { + const std::filesystem::path basePath{aurora::g_config.userPath}; + + for (auto& controller : aurora::input::g_GameControllers | std::views::values) { + EnsureMappingLoaded(&controller); + const auto filePath = + basePath / fmt::format("{}_{:04X}_{:04X}.controller", aurora::input::controller_name(controller.m_index), + controller.m_vid, controller.m_pid); + std::string filePathStr = filePath.string(); + + // don't truncate the file if it already exists + const char* openMode = std::filesystem::exists(filePath) ? "r+b" : "wb"; + SDL_IOStream* file = SDL_IOFromFile(filePathStr.c_str(), openMode); + if (file == nullptr) { + return; + } + SDL_SeekIO(file, 0, SDL_IO_SEEK_SET); + + // write header + constexpr uint32_t magic = SBIG('CTRL'); + SDL_WriteU32LE(file, magic); + SDL_WriteU32LE(file, k_mappingsFileVersion); + SDL_WriteU8(file, controller.m_isGameCube); + + // start writing data at next 32-byte aligned offset + const int64_t dataStart = SDL_TellIO(file) + 31 & ~31; + if (dataStart == -1) { + aurora::input::Log.warn("Unable to seek in controller bindings! Path: \"{}\"", filePath.string()); + return; + } + SDL_SeekIO(file, dataStart, SDL_IO_SEEK_SET); + if (controller.m_isGameCube) { + // GameCube adapters expose 4 input devices with the same vid/pid, we store all 4 in the same file + const auto port = aurora::input::player_index(controller.m_index); + constexpr int64_t dzSecLen = sizeof(PADDeadZones); + constexpr int64_t btnSecLen = sizeof(PADButtonMapping) * PAD_BUTTON_COUNT; + constexpr int64_t axisSecLen = sizeof(PADAxisMapping) * PAD_AXIS_COUNT; + // skip to offset in file for this particular port + SDL_SeekIO(file, dataStart + (dzSecLen + btnSecLen + axisSecLen) * port, SDL_IO_SEEK_SET); + } + __PADWriteDeadZones(file, controller); + SDL_WriteIO(file, controller.m_buttonMapping.data(), sizeof(PADButtonMapping) * PAD_BUTTON_COUNT); + SDL_WriteIO(file, controller.m_axisMapping.data(), sizeof(PADAxisMapping) * PAD_AXIS_COUNT); + + if (!controller.m_isGameCube) { + SDL_WriteIO(file, &controller.m_rumbleIntensityLow, sizeof(u16)); + SDL_WriteIO(file, &controller.m_rumbleIntensityHigh, sizeof(u16)); + } + SDL_CloseIO(file); + } + + save_keyboard_bindings(); +} + +PADDeadZones* PADGetDeadZones(const u32 port) { + auto* controller = aurora::input::get_controller_for_player(port); + if (controller == nullptr) { + return nullptr; + } + return &controller->m_deadZones; +} + +static constexpr std::array, PAD_BUTTON_COUNT> skButtonNames = {{ + {PAD_BUTTON_LEFT, "Left"sv}, + {PAD_BUTTON_RIGHT, "Right"sv}, + {PAD_BUTTON_DOWN, "Down"sv}, + {PAD_BUTTON_UP, "Up"sv}, + {PAD_TRIGGER_Z, "Z"sv}, + {PAD_TRIGGER_R, "R"sv}, + {PAD_TRIGGER_L, "L"sv}, + {PAD_BUTTON_A, "A"sv}, + {PAD_BUTTON_B, "B"sv}, + {PAD_BUTTON_X, "X"sv}, + {PAD_BUTTON_Y, "Y"sv}, + {PAD_BUTTON_START, "Start"sv}, +}}; + +static constexpr std::array, PAD_AXIS_COUNT> skAxisNames = {{ + {PAD_AXIS_LEFT_X_POS, "Left X+"sv}, + {PAD_AXIS_LEFT_X_NEG, "Left X-"sv}, + {PAD_AXIS_LEFT_Y_POS, "Left Y+"sv}, + {PAD_AXIS_LEFT_Y_NEG, "Left Y-"sv}, + {PAD_AXIS_RIGHT_X_POS, "Right X+"sv}, + {PAD_AXIS_RIGHT_X_NEG, "Right X-"sv}, + {PAD_AXIS_RIGHT_Y_POS, "Right Y+"sv}, + {PAD_AXIS_RIGHT_Y_NEG, "Right Y-"sv}, + {PAD_AXIS_TRIGGER_L, "Trigger L"sv}, + {PAD_AXIS_TRIGGER_R, "Trigger R"sv}, +}}; + +static constexpr std::array, PAD_AXIS_COUNT> skAxisDirLabels = {{ + {PAD_AXIS_LEFT_X_POS, "Right"sv}, + {PAD_AXIS_LEFT_X_NEG, "Left"sv}, + {PAD_AXIS_LEFT_Y_POS, "Up"sv}, + {PAD_AXIS_LEFT_Y_NEG, "Down"sv}, + {PAD_AXIS_RIGHT_X_POS, "Right"sv}, + {PAD_AXIS_RIGHT_X_NEG, "Left"sv}, + {PAD_AXIS_RIGHT_Y_POS, "Up"sv}, + {PAD_AXIS_RIGHT_Y_NEG, "Down"sv}, + {PAD_AXIS_TRIGGER_L, "N/A"sv}, + {PAD_AXIS_TRIGGER_R, "N/A"sv}, +}}; + +const char* PADGetButtonName(const PADButton button) { + + if (const auto iter = + std::ranges::find_if(skButtonNames, [&button](const auto& pair) { return button == pair.first; }); + iter != skButtonNames.end()) { + return iter->second.data(); + } + + return nullptr; +} + +const char* PADGetNativeButtonName(u32 button) { + return SDL_GetGamepadStringForButton(static_cast(button)); +} + +const char* PADGetAxisName(const PADAxis axis) { + if (const auto it = std::ranges::find_if(skAxisNames, [&axis](const auto& pair) { return axis == pair.first; }); + it != skAxisNames.end()) { + return it->second.data(); + } + + return nullptr; +} + +const char* PADGetAxisDirectionLabel(const PADAxis axis) { + if (const auto it = std::ranges::find_if(skAxisDirLabels, [&axis](const auto& pair) { return axis == pair.first; }); + it != skAxisDirLabels.end()) { + return it->second.data(); + } + + return nullptr; +} + +const char* PADGetNativeAxisName(PADSignedNativeAxis axis) { + return SDL_GetGamepadStringForAxis(static_cast(axis.nativeAxis)); +} + +int32_t PADGetNativeButtonPressed(const u32 port) { + const auto* controller = aurora::input::get_controller_for_player(port); + if (controller == nullptr) { + return -1; + } + + for (int32_t i = 0; i < SDL_GAMEPAD_BUTTON_COUNT; ++i) { + if (SDL_GetGamepadButton(controller->m_controller, static_cast(i)) != 0u) { + return i; + } + } + return -1; +} + +PADSignedNativeAxis PADGetNativeAxisPulled(const u32 port) { + const auto* controller = aurora::input::get_controller_for_player(port); + if (controller == nullptr) { + return {-1, AXIS_SIGN_POSITIVE}; + } + + for (int32_t i = 0; i < SDL_GAMEPAD_AXIS_COUNT; ++i) { + const auto axisVal = SDL_GetGamepadAxis(controller->m_controller, static_cast(i)); + if (axisVal >= 16384) { + return {i, AXIS_SIGN_POSITIVE}; + } + + if (axisVal <= -16384) { + // SDL3 triggers rest at -32768, so skip their negative direction. + if (i == SDL_GAMEPAD_AXIS_LEFT_TRIGGER || i == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) { + continue; + } + return {i, AXIS_SIGN_NEGATIVE}; + } + } + return {-1, AXIS_SIGN_POSITIVE}; +} + +void PADRestoreDefaultMapping(const u32 port) { + auto* controller = aurora::input::get_controller_for_player(port); + if (controller == nullptr) { + return; + } + __PADSetDefaultMapping(controller); + controller->m_axisMapping = g_defaultAxes; +} + +void PADBlockInput(const bool block) { + if (g_blockPAD && !block) { + g_suppressHeldOnRead = true; + } + g_blockPAD = block; +} + +SDL_Gamepad* PADGetSDLGamepadForIndex(const u32 index) { + const auto* ctrl = __PADGetControllerForIndex(index); + if (ctrl == nullptr) { + return nullptr; + } + + return ctrl->m_controller; +} + +void PADSetDefaultMapping(const PADDefaultMapping* mapping, const PADControllerType type) { + if (g_initialized) { + aurora::input::Log.fatal("PADSetDefaultMapping called after PADInit()!"); + } + + switch (type) { + case PAD_TYPE_STANDARD: + g_defaultButtonsStandard = toStdArray(mapping->buttons); + break; + case PAD_TYPE_XBOX360: + g_defaultButtonsXBox360 = toStdArray(mapping->buttons); + break; + case PAD_TYPE_XBOXONE: + g_defaultButtonsXBoxOne = toStdArray(mapping->buttons); + break; + case PAD_TYPE_PS3: + g_defaultButtonsPS3 = toStdArray(mapping->buttons); + break; + case PAD_TYPE_PS4: + g_defaultButtonsPS4 = toStdArray(mapping->buttons); + break; + case PAD_TYPE_PS5: + g_defaultButtonsPS5 = toStdArray(mapping->buttons); + break; + case PAD_TYPE_SWITCH_PROCON: + g_defaultButtonsProCon = toStdArray(mapping->buttons); + break; + case PAD_TYPE_JOYCON_LEFT: + g_defaultButtonsJoyConLeft = toStdArray(mapping->buttons); + break; + case PAD_TYPE_JOYCON_RIGHT: + g_defaultButtonsJoyConRight = toStdArray(mapping->buttons); + break; + case PAD_TYPE_JOYCON_PAIR: + g_defaultButtonsJoyPair = toStdArray(mapping->buttons); + break; + case PAD_TYPE_GAMECUBE: + g_defaultButtonsGamecube = toStdArray(mapping->buttons); + break; + case PAD_TYPE_NSO_GAMECUBE: + g_defaultButtonsNSOGamecube = toStdArray(mapping->buttons); + break; + default: + break; + } + g_defaultAxes = toStdArray(mapping->axes); +} + +BOOL PADSetColor(const u32 port, const u8 red, const u8 green, const u8 blue) { + const auto ctrl = aurora::input::get_controller_for_player(port); + if (ctrl == nullptr) { + return FALSE; + } + + ctrl->m_ledRed = red; + ctrl->m_ledGreen = green; + ctrl->m_ledBlue = blue; + ctrl->m_isColorDirty = true; + return true; +} + +BOOL PADGetColor(const u32 port, u8* red, u8* green, u8* blue) { + const auto ctrl = aurora::input::get_controller_for_player(port); + if (ctrl == nullptr) { + return FALSE; + } + + *red = ctrl->m_ledRed; + *green = ctrl->m_ledGreen; + *blue = ctrl->m_ledBlue; + return TRUE; +} + +BOOL PADSetSensorEnabled(const u32 port, const PADSensorType sensor, const BOOL enabled) { + const auto* ctrl = aurora::input::get_controller_for_player(port); + + if (ctrl == nullptr) { + return FALSE; + } + + return SDL_SetGamepadSensorEnabled(ctrl->m_controller, static_cast(sensor), enabled ? true : false) + ? TRUE + : FALSE; +} + +BOOL PADHasSensor(const u32 port, const PADSensorType sensor) { + const auto* ctrl = aurora::input::get_controller_for_player(port); + if (ctrl == nullptr) { + return FALSE; + } + + return SDL_GamepadHasSensor(ctrl->m_controller, static_cast(sensor)) ? TRUE : FALSE; +} + +BOOL PADGetSensorData(const u32 port, const PADSensorType sensor, f32* data, const int nValues) { + const auto* ctrl = aurora::input::get_controller_for_player(port); + if (ctrl == nullptr) { + return FALSE; + } + + return SDL_GetGamepadSensorData(ctrl->m_controller, static_cast(sensor), data, nValues); +} + +BOOL PADSetRumbleIntensity(const u32 port, const u16 low, const u16 high) { + auto* ctrl = aurora::input::get_controller_for_player(port); + if (ctrl == nullptr || ctrl->m_isGameCube || !ctrl->m_hasRumble) { + return FALSE; + } + ctrl->m_rumbleIntensityLow = low; + ctrl->m_rumbleIntensityHigh = high; + + return TRUE; +} + +BOOL PADGetRumbleIntensity(const u32 port, u16* low, u16* high) { + const auto* ctrl = aurora::input::get_controller_for_player(port); + if (ctrl == nullptr || ctrl->m_isGameCube || !ctrl->m_hasRumble) { + *low = 0; + *high = 0; + return FALSE; + } + + *low = ctrl->m_rumbleIntensityLow; + *high = ctrl->m_rumbleIntensityHigh; + return TRUE; +} + +BOOL PADSupportsRumbleIntensity(const u32 port) { + const auto* ctrl = aurora::input::get_controller_for_player(port); + if (ctrl == nullptr) { + return FALSE; + } + + return !ctrl->m_isGameCube && ctrl->m_hasRumble; +} + +BOOL PADIsGCAdapter(const u32 port) { + const auto* ctrl = aurora::input::get_controller_for_player(port); + if (ctrl == nullptr) { + return FALSE; + } + return ctrl->m_isGameCube; +} + +PADBatteryState PADGetBatteryState(const u32 port, f32* perc) { + const auto* ctrl = aurora::input::get_controller_for_player(port); + if (ctrl == nullptr) { + return PAD_BATTERYSTATE_ERROR; + } + + int tmp = 0; + const auto ret = SDL_GetGamepadPowerInfo(ctrl->m_controller, &tmp); + if (tmp != -1) { + *perc = static_cast(tmp) / 100.f; + } else { + *perc = static_cast(tmp); + } + return static_cast(ret); +} + +PADControllerType PADGetControllerType(const u32 port) { + const auto* ctrl = aurora::input::get_controller_for_player(port); + if (ctrl == nullptr) { + return PAD_TYPE_UNKNOWN; + } + + auto type = SDL_GetGamepadType(ctrl->m_controller); + return static_cast(type); +} + +PADControllerType PADGetControllerTypeForIndex(const u32 index) { + const auto* ctrl = __PADGetControllerForIndex(index); + if (ctrl == nullptr) { + return PAD_TYPE_UNKNOWN; + } + + auto type = SDL_GetGamepadType(ctrl->m_controller); + if (type == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO && ctrl->m_pid == 0x2073) { + return PAD_TYPE_NSO_GAMECUBE; + } + return static_cast(type); +} diff --git a/aurora-main/lib/dolphin/si/si.cpp b/aurora-main/lib/dolphin/si/si.cpp new file mode 100644 index 0000000..6164105 --- /dev/null +++ b/aurora-main/lib/dolphin/si/si.cpp @@ -0,0 +1,20 @@ +#include +#include "../../input.hpp" + +#include + +uint32_t SIProbe(int32_t chan) { + auto* const controller = aurora::input::get_controller_for_player(chan); + if (controller == nullptr) { + return SI_ERROR_NO_RESPONSE; + } + + if (controller->m_isGameCube) { + auto level = SDL_GetJoystickPowerInfo(SDL_GetGamepadJoystick(controller->m_controller), nullptr); + if (level == SDL_POWERSTATE_UNKNOWN) { + return SI_GC_WAVEBIRD; + } + } + + return SI_GC_CONTROLLER; +} diff --git a/aurora-main/lib/dolphin/vi/vi.cpp b/aurora-main/lib/dolphin/vi/vi.cpp new file mode 100644 index 0000000..d036b86 --- /dev/null +++ b/aurora-main/lib/dolphin/vi/vi.cpp @@ -0,0 +1,96 @@ +#include + +#include "../../window.hpp" +#include "aurora/math.hpp" +#include "vi_internal.hpp" + +#include +#include +#include + +namespace aurora::vi { +std::optional g_renderMode; +namespace { +std::atomic g_presentAspectCorrection{1.f}; + +float calculate_present_aspect_correction(const GXRenderModeObj& rm) noexcept { + if (rm.viWidth == 0 || rm.viHeight == 0) { + return 1.f; + } + + // Dolphin derives the displayed aspect from the VI active region instead of assuming an exact + // 4:3 or 16:9 signal. These are its BT.601 active sample and line counts. + const auto format = (static_cast(rm.viTVmode) >> 2) & 0x7u; + const bool pal = format == VI_PAL || format == VI_DEBUG_PAL; + const float nominalActiveWidth = + pal ? 864.f * (52.f / 64.f) : 858.f * (52.655555f / 63.555555f); + const float nominalActiveHeight = pal ? 576.f : 486.f; + const float horizontalFill = static_cast(rm.viWidth) / nominalActiveWidth; + const float verticalFill = static_cast(rm.viHeight) / nominalActiveHeight; + return horizontalFill / verticalFill; +} +} // namespace + +Vec2 render_mode_size() noexcept { + if (!g_renderMode) { + return {640, 528}; + } + // GX has a fixed 640x528 EFB workspace. Games can configure a narrower visible framebuffer, but + // effects such as MKW's DOF use the remaining EFB area as scratch. + return {std::max(g_renderMode->fbWidth, 640), std::max(g_renderMode->efbHeight, 528)}; +} + +void configure(const GXRenderModeObj* rm) noexcept { + const auto oldSize = render_mode_size(); + if (rm == nullptr) { + g_renderMode.reset(); + } else { + g_renderMode = *rm; + g_presentAspectCorrection.store(calculate_present_aspect_correction(*rm), std::memory_order_release); + } + if (rm == nullptr) { + g_presentAspectCorrection.store(1.f, std::memory_order_release); + } + if (render_mode_size() != oldSize) { + window::request_frame_buffer_resize(); + } +} + +Vec2 configured_fb_size() noexcept { + return render_mode_size(); +} + +Vec2 visible_fb_size() noexcept { + if (!g_renderMode) { + return {640, 528}; + } + return {g_renderMode->fbWidth, g_renderMode->efbHeight}; +} + +float present_aspect_correction() noexcept { + return g_presentAspectCorrection.load(std::memory_order_acquire); +} +} // namespace aurora::vi + +extern "C" { +void VIInit() {} +void VIConfigure(const GXRenderModeObj* rm) { aurora::vi::configure(rm); } +void VIConfigurePan(u16 xOrg, u16 yOrg, u16 width, u16 height) { + (void)xOrg; + (void)yOrg; + (void)width; + (void)height; +} +u32 VIGetTvFormat() { return 0; } +void VIFlush() {} + +void VISetWindowTitle(const char* title) { aurora::window::set_title(title); } +void VISetWindowFullscreen(bool fullscreen) { aurora::window::set_fullscreen(fullscreen); } +bool VIGetWindowFullscreen() { return aurora::window::get_fullscreen(); } +void VISetWindowSize(uint32_t width, uint32_t height) { aurora::window::set_window_size(width, height); } +void VISetWindowPosition(uint32_t x, uint32_t y) { aurora::window::set_window_position(x, y); } +void VICenterWindow() { aurora::window::center_window(); } +void VISetFrameBufferScale(float scale) { aurora::window::set_frame_buffer_scale(scale); } +void VILockAspectRatio(int width, int height) { aurora::window::lock_present_aspect_ratio(width, height); } +void VIUnlockAspectRatio() { aurora::window::unlock_present_aspect_ratio(); } +} diff --git a/aurora-main/lib/dolphin/vi/vi_internal.hpp b/aurora-main/lib/dolphin/vi/vi_internal.hpp new file mode 100644 index 0000000..d4fe1f6 --- /dev/null +++ b/aurora-main/lib/dolphin/vi/vi_internal.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +#include + +#include + +namespace aurora::vi { +void configure(const GXRenderModeObj* rm) noexcept; +Vec2 configured_fb_size() noexcept; +Vec2 visible_fb_size() noexcept; +float present_aspect_correction() noexcept; +} // namespace aurora::vi diff --git a/aurora-main/lib/fs_helper.hpp b/aurora-main/lib/fs_helper.hpp new file mode 100644 index 0000000..ffb3ffa --- /dev/null +++ b/aurora-main/lib/fs_helper.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +/** + * Converts a std::filesystem::path to a std::string, UTF-8, without exploding on Windows. + */ +inline std::string fs_path_to_string(const std::filesystem::path& path) { + const auto u8str = path.u8string(); + return { reinterpret_cast(u8str.c_str()) }; +} diff --git a/aurora-main/lib/gfx/clear.cpp b/aurora-main/lib/gfx/clear.cpp new file mode 100644 index 0000000..f5964f9 --- /dev/null +++ b/aurora-main/lib/gfx/clear.cpp @@ -0,0 +1,162 @@ +#include "clear.hpp" + +#include "../webgpu/gpu.hpp" +#include "tracy/Tracy.hpp" + +#include +#include + +namespace { +wgpu::ColorWriteMask clear_write_mask(bool clearColor, bool clearAlpha) { + auto writeMask = wgpu::ColorWriteMask::None; + if (clearColor) { + writeMask |= wgpu::ColorWriteMask::Red | wgpu::ColorWriteMask::Green | wgpu::ColorWriteMask::Blue; + } + if (clearAlpha) { + writeMask |= wgpu::ColorWriteMask::Alpha; + } + return writeMask; +} +} // namespace + +namespace aurora::gfx::clear { + +using webgpu::g_device; +using webgpu::g_graphicsConfig; + +wgpu::RenderPipeline create_pipeline(const PipelineConfig& config) { + ZoneScoped; + wgpu::ShaderSourceWGSL sourceDescriptor{}; + sourceDescriptor.code = R"""( +struct VertexOutput { + @builtin(position) pos: vec4, +}; + +struct ClearUniform { + depth: f32, +}; + +struct FragmentOutput { + @location(0) color: vec4, + @builtin(frag_depth) depth: f32, +}; + +@group(1) @binding(0) var clear: ClearUniform; + +var pos: array, 3> = array, 3>( + vec2(-1.0, 1.0), + vec2(-1.0, -3.0), + vec2(3.0, 1.0), +); + +@vertex +fn vs_main(@builtin(vertex_index) vtxIdx: u32) -> VertexOutput { + var out: VertexOutput; + out.pos = vec4(pos[vtxIdx], 0.0, 1.0); + return out; +} + +@fragment +fn fs_main() -> FragmentOutput { + var out: FragmentOutput; + out.color = vec4(1.0); + out.depth = clear.depth; + return out; +} +)"""; + const wgpu::ShaderModuleDescriptor moduleDescriptor{ + .nextInChain = &sourceDescriptor, + .label = "EFB Clear Module", + }; + auto module = g_device.CreateShaderModule(&moduleDescriptor); + const std::array layouts{ + g_staticBindGroupLayout, + g_uniformBindGroupLayout, + }; + const wgpu::PipelineLayoutDescriptor layoutDescriptor{ + .label = "EFB Clear Pipeline Layout", + .bindGroupLayoutCount = layouts.size(), + .bindGroupLayouts = layouts.data(), + }; + auto pipelineLayout = g_device.CreatePipelineLayout(&layoutDescriptor); + constexpr wgpu::BlendState blendState{ + .color = + wgpu::BlendComponent{ + .operation = wgpu::BlendOperation::Add, + .srcFactor = wgpu::BlendFactor::Constant, + .dstFactor = wgpu::BlendFactor::Zero, + }, + .alpha = + wgpu::BlendComponent{ + .operation = wgpu::BlendOperation::Add, + .srcFactor = wgpu::BlendFactor::Constant, + .dstFactor = wgpu::BlendFactor::Zero, + }, + }; + const wgpu::ColorTargetState colorTarget{ + .format = g_graphicsConfig.surfaceConfiguration.format, + .blend = &blendState, + .writeMask = clear_write_mask(config.clearColor, config.clearAlpha), + }; + const wgpu::FragmentState fragmentState{ + .module = module, + .entryPoint = "fs_main", + .targetCount = 1, + .targets = &colorTarget, + }; + const wgpu::DepthStencilState depthStencil{ + .format = g_graphicsConfig.depthFormat, + .depthWriteEnabled = config.clearDepth, + .depthCompare = wgpu::CompareFunction::Always, + }; + const auto label = fmt::format("EFB Clear Pipeline (color {}, alpha {}, depth {})", config.clearColor, + config.clearAlpha, config.clearDepth); + const wgpu::RenderPipelineDescriptor pipelineDescriptor{ + .label = label.c_str(), + .layout = pipelineLayout, + .vertex = + wgpu::VertexState{ + .module = module, + .entryPoint = "vs_main", + }, + .primitive = + wgpu::PrimitiveState{ + .topology = wgpu::PrimitiveTopology::TriangleList, + }, + .depthStencil = &depthStencil, + .multisample = + wgpu::MultisampleState{ + .count = config.msaaSamples, + }, + .fragment = &fragmentState, + }; + return g_device.CreateRenderPipeline(&pipelineDescriptor); +} + +void render(const DrawData& data, const wgpu::RenderPassEncoder& pass, const wgpu::Extent3D& targetSize, + PipelineRef& currentPipeline) { + if (!bind_pipeline(data.pipeline, pass, currentPipeline)) { + return; + } + + const std::array offsets{data.uniformRange.offset}; + pass.SetBindGroup(1, g_uniformBindGroup, offsets.size(), offsets.data()); + pass.SetBlendConstant(&data.color); + pass.SetViewport(0.f, 0.f, static_cast(targetSize.width), static_cast(targetSize.height), 0.f, 1.f); + if (data.useScissor) { + const auto x = static_cast(std::clamp(data.scissor.x, 0, static_cast(targetSize.width))); + const auto y = static_cast(std::clamp(data.scissor.y, 0, static_cast(targetSize.height))); + const auto right = static_cast(std::clamp( + data.scissor.x + data.scissor.width, 0, static_cast(targetSize.width))); + const auto bottom = static_cast(std::clamp( + data.scissor.y + data.scissor.height, 0, static_cast(targetSize.height))); + if (right <= x || bottom <= y) { + return; + } + pass.SetScissorRect(x, y, right - x, bottom - y); + } else { + pass.SetScissorRect(0, 0, targetSize.width, targetSize.height); + } + pass.Draw(3); +} +} // namespace aurora::gfx::clear diff --git a/aurora-main/lib/gfx/clear.hpp b/aurora-main/lib/gfx/clear.hpp new file mode 100644 index 0000000..7aed97f --- /dev/null +++ b/aurora-main/lib/gfx/clear.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "common.hpp" + +#include + +namespace aurora::gfx::clear { +struct DrawData { + PipelineRef pipeline; + Range uniformRange; + wgpu::Color color; + float depth = 0.f; + bool useScissor = false; + ClipRect scissor{}; +}; + +constexpr uint32_t ClearPipelineConfigVersion = 3; +struct PipelineConfig { + uint32_t version = ClearPipelineConfigVersion; + uint32_t msaaSamples = 1; + bool clearColor = true; + bool clearAlpha = true; + bool clearDepth = true; + uint8_t _pad = 0; +}; +static_assert(std::has_unique_object_representations_v); + +wgpu::RenderPipeline create_pipeline(const PipelineConfig& config); +void render(const DrawData& data, const wgpu::RenderPassEncoder& pass, const wgpu::Extent3D& targetSize, + PipelineRef& currentPipeline); +} // namespace aurora::gfx::clear diff --git a/aurora-main/lib/gfx/common.cpp b/aurora-main/lib/gfx/common.cpp new file mode 100644 index 0000000..1bb1bb4 --- /dev/null +++ b/aurora-main/lib/gfx/common.cpp @@ -0,0 +1,1632 @@ +#include "common.hpp" +#include "../gx/shader_info.hpp" + +#include "clear.hpp" +#include "depth_peek.hpp" +#include "efb_ram_copy.hpp" +#include "../internal.hpp" +#include "../webgpu/gpu.hpp" +#include "../gx/pipeline.hpp" +#include "pipeline_cache.hpp" +#include "tex_copy_conv.hpp" +#include "tex_palette_conv.hpp" +#include "texture_replacement.hpp" +#include "texture.hpp" +#include "../window.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "tracy/Tracy.hpp" + +namespace aurora::gfx { +static Module Log("aurora::gfx"); + +using webgpu::g_device; +using webgpu::g_instance; +using webgpu::g_queue; + +#ifdef AURORA_GFX_DEBUG_GROUPS +std::vector g_debugGroupStack; +std::vector g_debugMarkers; +#endif + +constexpr uint64_t StagingBufferSize = UniformBufferSize + VertexBufferSize + IndexBufferSize + StorageBufferSize + + (UseTextureBuffer ? TextureUploadSize : 0); + +struct ShaderDrawCommand { + ShaderType type; + union { + clear::DrawData clear; + gx::DrawData gx; + }; +}; +enum class CommandType { + SetViewport, + SetScissor, + Draw, + DebugMarker, +}; +struct Command { + CommandType type; +#ifdef AURORA_GFX_DEBUG_GROUPS + std::vector debugGroupStack; +#endif + union Data { + Viewport setViewport; + ClipRect setScissor; + ShaderDrawCommand draw; + size_t debugMarkerIndex; + } data; +}; +} // namespace aurora::gfx + +void aurora_set_guest_write_hooks(AuroraGuestWriteGenerationCallback generation, + AuroraGuestWriteNotifyCallback notify) { + aurora::g_guestWriteGenerationHook = generation; + aurora::g_guestWriteNotifyHook = notify; +} + +namespace aurora { +// For types that we can't ensure are safe to hash with has_unique_object_representations, +// we create specialized methods to handle them. Note that these are highly dependent on +// the structure definition, which could easily change with Dawn updates. +template <> +inline HashType xxh3_hash(const WGPUBindGroupDescriptor& input, HashType seed) { + constexpr auto offset = offsetof(WGPUBindGroupDescriptor, layout); // skip nextInChain, label + const auto hash = xxh3_hash_s(reinterpret_cast(&input) + offset, + sizeof(WGPUBindGroupDescriptor) - offset - sizeof(void*) /* skip entries */, seed); + const size_t entryBytes = sizeof(WGPUBindGroupEntry) * input.entryCount; + // The entries are hashed unseeded so they stay off XXH3's seeded long path; + // the descriptor head still carries the caller's seed. + return hash_combine(hash, xxh3_hash_s(input.entries, entryBytes)); +} +template <> +inline HashType xxh3_hash(const wgpu::SamplerDescriptor& input, HashType seed) { + constexpr auto offset = offsetof(wgpu::SamplerDescriptor, addressModeU); // skip nextInChain, label + return xxh3_hash_s(reinterpret_cast(&input) + offset, + sizeof(wgpu::SamplerDescriptor) - offset - 2 /* skip padding */, seed); +} +} // namespace aurora + +namespace aurora::gfx { +namespace { +struct CachedBindGroup { + wgpu::BindGroup bindGroup; + uint32_t lastUsedFrame = 0; +}; + +constexpr uint32_t BindGroupCacheRetainFrames = 32; +constexpr uint32_t BindGroupCacheSweepPeriod = 16; +} // namespace + +static absl::flat_hash_map g_cachedBindGroups; +// Bind groups dropped from the cache by clear_caches() while a sealed frame's recorded draws +// still reference them. Ownership moves here and is released at the next seal. +static std::vector g_retiredBindGroups; +static absl::flat_hash_map g_cachedSamplers; + +static ByteBuffer g_verts; +static ByteBuffer g_uniforms; +static ByteBuffer g_indices; +static ByteBuffer g_storage; +static ByteBuffer g_textureUpload; +wgpu::Buffer g_vertexBuffer; +wgpu::Buffer g_uniformBuffer; +wgpu::Buffer g_indexBuffer; +wgpu::Buffer g_storageBuffer; +constexpr size_t FrameSlotCount = 3; +static std::array g_stagingBuffers; +static size_t currentStagingBuffer = 0; +enum class BufferMapState { + Unmapped, + Mapping, + Mapped, +}; +static std::atomic s_mappingState{BufferMapState::Unmapped}; +static wgpu::Limits g_cachedLimits; +// Advanced once per logical frame in the seal prologue, under the renderer GPU mutex and with the +// producer blocked, so every later reader sees a value that no longer moves. +static uint32_t g_frameIndex = UINT32_MAX; +wgpu::BindGroupLayout g_staticBindGroupLayout; +wgpu::BindGroup g_staticBindGroup; +wgpu::BindGroupLayout g_uniformBindGroupLayout; +wgpu::BindGroup g_uniformBindGroup; + +// for imgui debug +AuroraStats g_stats{}; +uint32_t g_drawCallCount = 0; +uint32_t g_mergedDrawCallCount = 0; + +using CommandList = std::vector; +struct RenderPass { + wgpu::TextureView colorView; + wgpu::TextureView resolveView; // MSAA resolve target; null if msaaSamples == 1 + wgpu::TextureView depthView; + wgpu::Texture copySourceTexture; + wgpu::TextureView copySourceView; + wgpu::TextureView copySourceDepthView; + wgpu::Extent3D targetSize; + uint32_t msaaSamples = 1; + + TextureHandle resolveTarget; + TextureHandle resolveSourceSnapshot; + GXTexFmt resolveFormat = GX_TF_RGBA8; + ClipRect resolveRect; + ClipRect resolveSnapshotRect; + Vec4 resolveSourceRect; + Range resolveUniformRange; + std::array resolveCopyFilterCoefficients{0, 64, 0}; + Vec4 clearColorValue{0.f, 0.f, 0.f, 0.f}; + float clearDepthValue = 1.f; + CommandList commands; + bool clearColor = true; + bool clearDepth = true; + // The resolve destination outlives the frame with no re-issue path (one-shot + // bake), so this pass may not drop draws even in skip-unready-pipelines mode. + bool requireReadyPipelines = false; + bool resolveHalfScale = false; + bool resolveCopyFilterActive = false; + bool resolveForceOpaqueAlpha = false; + bool resolveNeedsConversion = false; + bool resolveNeedsShaderSampling = false; + bool resolveLinearSampling = false; + bool snapshotColorResolveSource = false; + std::vector paletteConvs; +}; +static std::vector g_renderPasses; +static u32 g_currentRenderPass = UINT32_MAX; + +// Recycle command storage: discarding passes used to free their command lists too, so each frame +// rebuilt hundreds of KB from zero capacity. The passes themselves are cheap to recreate. +using CommandListPool = std::vector; +static CommandListPool g_commandListPool; +static constexpr size_t MaxPooledCommandLists = 32; +// The pool is the only recording storage both sides touch, a few pointer moves per frame, so a +// plain mutex is cheaper than the alternatives and keeps the vector's invariants. +static std::mutex g_commandListPoolMutex; + +static CommandList acquire_command_list() noexcept { + std::lock_guard lock{g_commandListPoolMutex}; + if (g_commandListPool.empty()) { + return {}; + } + CommandList list = std::move(g_commandListPool.back()); + g_commandListPool.pop_back(); + list.clear(); + return list; +} + +static void release_command_list(CommandList&& list) noexcept { + if (list.capacity() == 0) { + return; + } + std::lock_guard lock{g_commandListPoolMutex}; + if (g_commandListPool.size() >= MaxPooledCommandLists) { + return; + } + list.clear(); + g_commandListPool.emplace_back(std::move(list)); +} + +static void release_render_pass(RenderPass& pass) noexcept { release_command_list(std::move(pass.commands)); } + +static void recycle_render_passes(std::vector& passes) noexcept { + for (auto& pass : passes) { + release_render_pass(pass); + } + passes.clear(); +} + +struct SealedFrameData { + std::vector passes; +}; + +SealedFrame::SealedFrame() : m_data(std::make_unique()) {} +SealedFrame::~SealedFrame() = default; +SealedFrame::SealedFrame(SealedFrame&&) noexcept = default; +SealedFrame& SealedFrame::operator=(SealedFrame&&) noexcept = default; + +static RenderPass& push_render_pass(RenderPass&& pass) { + auto& out = g_renderPasses.emplace_back(std::move(pass)); + if (out.commands.capacity() == 0) { + out.commands = acquire_command_list(); + } + return out; +} +// The producer may build the next frame's GX stream while the worker prepares the target, and +// this is the only renderer-owned property that path queries, so publish it explicitly. +static std::atomic_bool g_inOffscreen{false}; +static std::optional g_suspendedEfbPass; +static Viewport g_suspendedEfbViewport; +static ClipRect g_suspendedEfbScissor; + +static void discard_suspended_efb_pass() noexcept { + if (g_suspendedEfbPass) { + release_render_pass(*g_suspendedEfbPass); + g_suspendedEfbPass.reset(); + } +} + +static bool has_current_render_pass() noexcept { return g_currentRenderPass < g_renderPasses.size(); } +static webgpu::TextureWithSampler g_offscreenColor; +static webgpu::TextureWithSampler g_offscreenDepth; + +struct ResolveSourceSnapshotPool { + TextureHandle entry; +}; + +// Snapshot resources live in the same safe frame slots as the mapped staging buffers, and copies +// within a frame are ordered, so one grow-only snapshot serves every resolve in that slot. +static std::array g_resolveSourceSnapshotPools; +static size_t g_recordingSnapshotSlot = 0; + +static TextureHandle new_resolve_source_snapshot(wgpu::Extent3D size, wgpu::TextureFormat format) noexcept { + const wgpu::TextureDescriptor textureDescriptor{ + .label = "GX Copy Source Snapshot", + .usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopyDst, + .dimension = wgpu::TextureDimension::e2D, + .size = size, + .format = format, + .mipLevelCount = 1, + .sampleCount = 1, + }; + auto texture = g_device.CreateTexture(&textureDescriptor); + constexpr wgpu::TextureViewDescriptor viewDescriptor{ + .label = "GX Copy Source Snapshot view", + .dimension = wgpu::TextureViewDimension::e2D, + }; + auto textureView = texture.CreateView(&viewDescriptor); + wgpu::TextureView sampleTextureView = textureView; + return std::make_shared(std::move(texture), std::move(sampleTextureView), std::move(textureView), size, + format, 1, GX_TF_RGBA8); +} + +static TextureHandle acquire_resolve_source_snapshot(uint32_t width, uint32_t height) noexcept { + auto& pool = g_resolveSourceSnapshotPools[g_recordingSnapshotSlot]; + const auto format = webgpu::g_graphicsConfig.surfaceConfiguration.format; + + if (pool.entry && pool.entry->size.width >= width && pool.entry->size.height >= height && + pool.entry->format == format) { + return pool.entry; + } + + const wgpu::Extent3D size{ + std::max(width, pool.entry ? pool.entry->size.width : 0u), + std::max(height, pool.entry ? pool.entry->size.height : 0u), + 1, + }; + pool.entry = new_resolve_source_snapshot(size, format); + return pool.entry; +} + +struct ResolveSamplingPlan { + bool needsConversion = false; + bool needsShaderSampling = false; + bool usesLinearSampling = false; +}; + +static ResolveSamplingPlan make_resolve_sampling_plan(const TextureHandle& target, GXTexFmt format, + const ClipRect& resolveRect, const Vec4& sourceRect, + bool halfScale, bool copyFilterActive, + bool forceOpaqueAlpha) noexcept { + const auto differs = [](float lhs, float rhs) { return std::abs(lhs - rhs) > 0.01f; }; + const auto differsFromDst = [&](uint32_t dst, float src) { + return differs(static_cast(dst), src); + }; + const bool sourceMatchesIntegerRect = + !differs(sourceRect.x(), static_cast(resolveRect.x)) && + !differs(sourceRect.y(), static_cast(resolveRect.y)) && + !differs(sourceRect.z(), static_cast(resolveRect.width)) && + !differs(sourceRect.w(), static_cast(resolveRect.height)); + const bool dstMatchesSource = target && !differsFromDst(target->size.width, sourceRect.z()) && + !differsFromDst(target->size.height, sourceRect.w()); + const bool needsShaderSampling = + halfScale || copyFilterActive || forceOpaqueAlpha || !sourceMatchesIntegerRect || !dstMatchesSource; + return { + .needsConversion = tex_copy_conv::needs_conversion(format), + .needsShaderSampling = needsShaderSampling, + .usesLinearSampling = halfScale || (needsShaderSampling && !copyFilterActive), + }; +} + +static ClipRect calculate_resolve_snapshot_rect(const wgpu::Extent3D& targetSize, + const Vec4& sourceRect, + const ResolveSamplingPlan& samplingPlan, + bool copyFilterActive) noexcept { + const ClipRect fullTarget{ + .x = 0, + .y = 0, + .width = static_cast(targetSize.width), + .height = static_cast(targetSize.height), + }; + // Shader-sampled and converted copies can feed effects that read outside the copy rectangle + // (MKW's DOF chain), so keep those full-target. Exact copies have a closed, crop-safe footprint. + if (samplingPlan.needsConversion || + samplingPlan.needsShaderSampling || + targetSize.width == 0 || targetSize.height == 0 || + !std::isfinite(sourceRect.x()) || !std::isfinite(sourceRect.y()) || + !std::isfinite(sourceRect.z()) || !std::isfinite(sourceRect.w()) || + sourceRect.z() <= 0.0f || sourceRect.w() <= 0.0f) { + return fullTarget; + } + + // Linear sampling can touch one neighbor on every edge. The vertical GX + // copy filter explicitly samples the previous and next rows. + const int32_t haloX = samplingPlan.usesLinearSampling ? 1 : 0; + const int32_t haloY = (samplingPlan.usesLinearSampling || copyFilterActive) ? 1 : 0; + const int32_t targetWidth = static_cast(targetSize.width); + const int32_t targetHeight = static_cast(targetSize.height); + const int32_t left = std::clamp(static_cast(std::floor(sourceRect.x())) - haloX, 0, targetWidth - 1); + const int32_t top = std::clamp(static_cast(std::floor(sourceRect.y())) - haloY, 0, targetHeight - 1); + const int32_t right = std::clamp(static_cast(std::ceil(sourceRect.x() + sourceRect.z())) + haloX, + left + 1, targetWidth); + const int32_t bottom = std::clamp(static_cast(std::ceil(sourceRect.y() + sourceRect.w())) + haloY, + top + 1, targetHeight); + return { + .x = left, + .y = top, + .width = right - left, + .height = bottom - top, + }; +} + +static void set_efb_targets(RenderPass& pass) { + pass.colorView = webgpu::g_frameBuffer.view; + pass.resolveView = webgpu::g_graphicsConfig.msaaSamples > 1 ? webgpu::g_frameBufferResolved.view : nullptr; + pass.depthView = webgpu::g_depthBuffer.view; + pass.copySourceTexture = + webgpu::g_graphicsConfig.msaaSamples > 1 ? webgpu::g_frameBufferResolved.texture : webgpu::g_frameBuffer.texture; + pass.copySourceView = + webgpu::g_graphicsConfig.msaaSamples > 1 ? webgpu::g_frameBufferResolved.view : webgpu::g_frameBuffer.view; + pass.copySourceDepthView = webgpu::g_depthBuffer.view; + pass.targetSize = webgpu::g_frameBuffer.size; + pass.msaaSamples = webgpu::g_graphicsConfig.msaaSamples; +} + +struct OffscreenCacheKey { + uint32_t width; + uint32_t height; + + bool operator==(const OffscreenCacheKey& rhs) const { return width == rhs.width && height == rhs.height; } + template + friend H AbslHashValue(H h, const OffscreenCacheKey& key) { + return H::combine(std::move(h), key.width, key.height); + } +}; +struct OffscreenCacheEntry { + webgpu::TextureWithSampler color; + webgpu::TextureWithSampler depth; +}; +static absl::flat_hash_map g_offscreenCache; +std::vector g_textureUploads; + +static inline void push_command(CommandType type, const Command::Data& data) { + if (!has_current_render_pass()) + UNLIKELY { + Log.warn("Dropping command {}", magic_enum::enum_name(type)); + return; + } + g_renderPasses[g_currentRenderPass].commands.push_back({ + .type = type, +#ifdef AURORA_GFX_DEBUG_GROUPS + .debugGroupStack = g_debugGroupStack, +#endif + .data = data, + }); +} + +template <> +gx::DrawData* get_last_draw_command() { + if (g_currentRenderPass >= g_renderPasses.size()) { + return nullptr; + } + auto& last = g_renderPasses[g_currentRenderPass].commands.back(); + if (last.type != CommandType::Draw || last.data.draw.type != ShaderType::GX) { + return nullptr; + } + return &last.data.draw.gx; +} + +static void push_draw_command(ShaderDrawCommand data) { + push_command(CommandType::Draw, Command::Data{.draw = data}); + ++g_drawCallCount; +} + +Vec2 get_frame_buffer_size() noexcept { + if (webgpu::g_frameBuffer.size.width != 0 && webgpu::g_frameBuffer.size.height != 0) { + return {webgpu::g_frameBuffer.size.width, webgpu::g_frameBuffer.size.height}; + } + const auto windowSize = window::get_window_size(); + return {windowSize.fb_width, windowSize.fb_height}; +} + +// Render-thread only: the frame worker recycles g_renderPasses in end_frame, so an off-thread +// read is a use-after-free rather than a stale value. Use get_frame_buffer_size() instead. +Vec2 get_render_target_size() noexcept { + if (g_currentRenderPass < g_renderPasses.size()) { + const auto& size = g_renderPasses[g_currentRenderPass].targetSize; + return {size.width, size.height}; + } + return get_frame_buffer_size(); +} + +static Viewport g_cachedViewport; +void set_viewport(const Viewport& cmd) noexcept { + if (cmd != g_cachedViewport) { + push_command(CommandType::SetViewport, Command::Data{.setViewport = cmd}); + g_cachedViewport = cmd; + } +} + +static ClipRect g_cachedScissor; +void set_scissor(const ClipRect& cmd) noexcept { + if (cmd != g_cachedScissor) { + push_command(CommandType::SetScissor, Command::Data{.setScissor = cmd}); + g_cachedScissor = cmd; + } +} + +template <> +void push_draw_command(clear::DrawData data) { + if (data.uniformRange.size == 0) { + const std::array clearUniform{ + std::clamp(data.depth, 0.f, 1.f), + 0.f, + 0.f, + 0.f, + }; + data.uniformRange = push_uniform(clearUniform); + } + push_draw_command(ShaderDrawCommand{.type = ShaderType::Clear, .clear = data}); +} + +template <> +PipelineRef pipeline_ref(const clear::PipelineConfig& config) { + return find_pipeline(ShaderType::Clear, config, [=] { return create_pipeline(config); }); +} + +void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool clearAlpha, bool clearDepth, + Vec4 clearColorValue, float clearDepthValue, GXTexFmt resolveFormat, + const Vec4* sourceRectPixels, bool halfScale, + const std::array* copyFilterCoefficients, bool forceOpaqueAlpha, + float copyFilterRowStride, bool clampTop, bool clampBottom, bool persistentCopy) { + // Resolve current render pass + if (!has_current_render_pass()) { + Log.warn("Dropping resolve pass without an active render pass"); + return; + } + auto& prevPass = g_renderPasses[g_currentRenderPass]; + const auto targetWidth = static_cast(prevPass.targetSize.width); + const auto targetHeight = static_cast(prevPass.targetSize.height); + if (targetWidth <= 0 || targetHeight <= 0) { + Log.warn("Dropping resolve pass with invalid target size {}x{}", targetWidth, targetHeight); + return; + } + Vec4 sourceRect = sourceRectPixels != nullptr + ? *sourceRectPixels + : Vec4{static_cast(rect.x), static_cast(rect.y), + static_cast(rect.width), static_cast(rect.height)}; + if (targetWidth > 0 && targetHeight > 0) { + const int32_t left = std::clamp(rect.x, 0, targetWidth - 1); + const int32_t top = std::clamp(rect.y, 0, targetHeight - 1); + const int32_t right = std::clamp(rect.x + rect.width, left + 1, targetWidth); + const int32_t bottom = std::clamp(rect.y + rect.height, top + 1, targetHeight); + rect = { + .x = left, + .y = top, + .width = right - left, + .height = bottom - top, + }; + + const float srcW = static_cast(targetWidth); + const float srcH = static_cast(targetHeight); + const float srcLeft = std::clamp(sourceRect.x(), 0.0f, srcW); + const float srcTop = std::clamp(sourceRect.y(), 0.0f, srcH); + const float srcRight = std::clamp(sourceRect.x() + sourceRect.z(), srcLeft, srcW); + const float srcBottom = std::clamp(sourceRect.y() + sourceRect.w(), srcTop, srcH); + sourceRect = {srcLeft, srcTop, std::max(srcRight - srcLeft, 1.0f), std::max(srcBottom - srcTop, 1.0f)}; + } + prevPass.resolveTarget = std::move(texture); + prevPass.requireReadyPipelines = persistentCopy; + prevPass.resolveRect = rect; + prevPass.resolveSourceRect = sourceRect; + prevPass.resolveFormat = resolveFormat; + prevPass.resolveHalfScale = halfScale; + prevPass.resolveForceOpaqueAlpha = forceOpaqueAlpha; + prevPass.resolveCopyFilterCoefficients = + copyFilterCoefficients != nullptr ? *copyFilterCoefficients : std::array{0, 64, 0}; + prevPass.resolveCopyFilterActive = prevPass.resolveCopyFilterCoefficients[0] != 0 || + prevPass.resolveCopyFilterCoefficients[1] != 64 || + prevPass.resolveCopyFilterCoefficients[2] != 0; + const auto samplingPlan = make_resolve_sampling_plan( + prevPass.resolveTarget, resolveFormat, rect, sourceRect, halfScale, + prevPass.resolveCopyFilterActive, forceOpaqueAlpha); + prevPass.resolveNeedsConversion = samplingPlan.needsConversion; + prevPass.resolveNeedsShaderSampling = samplingPlan.needsShaderSampling; + prevPass.resolveLinearSampling = samplingPlan.usesLinearSampling; + prevPass.snapshotColorResolveSource = + !gx::is_depth_format(resolveFormat) && (clearColor || clearAlpha || clearDepth); + Vec4 uniformSourceRect = sourceRect; + float srcW = static_cast(prevPass.targetSize.width); + float srcH = static_cast(prevPass.targetSize.height); + if (prevPass.snapshotColorResolveSource) { + prevPass.resolveSnapshotRect = calculate_resolve_snapshot_rect( + prevPass.targetSize, sourceRect, samplingPlan, prevPass.resolveCopyFilterActive); + prevPass.resolveSourceSnapshot = acquire_resolve_source_snapshot( + static_cast(prevPass.resolveSnapshotRect.width), + static_cast(prevPass.resolveSnapshotRect.height)); + uniformSourceRect = { + sourceRect.x() - static_cast(prevPass.resolveSnapshotRect.x), + sourceRect.y() - static_cast(prevPass.resolveSnapshotRect.y), + sourceRect.z(), sourceRect.w()}; + srcW = static_cast(prevPass.resolveSourceSnapshot->size.width); + srcH = static_cast(prevPass.resolveSourceSnapshot->size.height); + } + // GX's copy-clamp bits pin every vertical filter tap to the first or last source texel; without + // it a filtered copy at internal resolution samples an unrelated EFB row as a visible border. + const float clampTopPixels = clampTop ? uniformSourceRect.y() : 0.0f; + const float clampBottomPixels = + clampBottom ? uniformSourceRect.y() + uniformSourceRect.w() : srcH; + const float clampTopUv = (clampTopPixels + 0.5f) / srcH; + const float clampBottomUv = (clampBottomPixels - 0.5f) / srcH; + // Push UV transform uniform for tex_copy_conv (crop region in UV space) + const std::array resolveUniform{ + uniformSourceRect.x() / srcW, + uniformSourceRect.y() / srcH, + uniformSourceRect.z() / srcW, + uniformSourceRect.w() / srcH, + static_cast(prevPass.resolveCopyFilterCoefficients[0]), + static_cast(prevPass.resolveCopyFilterCoefficients[1]), + static_cast(prevPass.resolveCopyFilterCoefficients[2]), + prevPass.resolveCopyFilterActive ? 1.0f : 0.0f, + prevPass.resolveForceOpaqueAlpha ? 1.0f : 0.0f, + std::max(copyFilterRowStride, 1.0f), + clampTopUv, + clampBottomUv, + }; + prevPass.resolveUniformRange = push_uniform(resolveUniform); + const bool clearFullTarget = rect.x <= 0 && rect.y <= 0 && + rect.width >= static_cast(prevPass.targetSize.width) && + rect.height >= static_cast(prevPass.targetSize.height); + const bool useAttachmentColorClear = clearFullTarget && clearColor && clearAlpha; + const bool useAttachmentDepthClear = clearFullTarget && clearDepth; + + // Populate new render pass from previous + const auto msaaSamples = prevPass.msaaSamples; + RenderPass newPass{ + .colorView = prevPass.colorView, + .resolveView = prevPass.resolveView, + .depthView = prevPass.depthView, + .copySourceTexture = prevPass.copySourceTexture, + .copySourceView = prevPass.copySourceView, + .copySourceDepthView = prevPass.copySourceDepthView, + .targetSize = prevPass.targetSize, + .msaaSamples = msaaSamples, + .clearColorValue = clearColorValue, + .clearDepthValue = clearDepthValue, + .clearColor = useAttachmentColorClear, + .clearDepth = useAttachmentDepthClear, + }; + push_render_pass(std::move(newPass)); + ++g_currentRenderPass; + + if ((!useAttachmentColorClear && (clearColor || clearAlpha)) || (!useAttachmentDepthClear && clearDepth)) { + // GX copy clears cover the copied EFB rectangle, not always the whole target, so use a scissored + // clear draw unless the load op can clear all of it. + push_draw_command(clear::DrawData{ + .pipeline = pipeline_ref(clear::PipelineConfig{ + .msaaSamples = msaaSamples, + .clearColor = clearColor, + .clearAlpha = clearAlpha, + .clearDepth = clearDepth, + }), + .color = + wgpu::Color{ + .r = clearColorValue.x(), + .g = clearColorValue.y(), + .b = clearColorValue.z(), + .a = clearColorValue.w(), + }, + .depth = clearDepthValue, + .useScissor = !clearFullTarget, + .scissor = rect, + }); + } + push_command(CommandType::SetViewport, Command::Data{.setViewport = g_cachedViewport}); + push_command(CommandType::SetScissor, Command::Data{.setScissor = g_cachedScissor}); +} + +void queue_palette_conv(tex_palette_conv::ConvRequest req) { + if (!has_current_render_pass()) { + Log.warn("Dropping palette conversion without an active render pass"); + return; + } + g_renderPasses[g_currentRenderPass].paletteConvs.push_back(std::move(req)); +} + +bool is_offscreen() noexcept { return g_inOffscreen; } + +uint32_t get_sample_count() noexcept { + if (!has_current_render_pass()) { + return webgpu::g_graphicsConfig.msaaSamples; + } + return g_renderPasses[g_currentRenderPass].msaaSamples; +} + +void clear_caches() noexcept { + g_offscreenCache.clear(); + // Retire rather than free; see g_retiredBindGroups. + g_retiredBindGroups.reserve(g_retiredBindGroups.size() + g_cachedBindGroups.size()); + for (auto& entry : g_cachedBindGroups) { + g_retiredBindGroups.emplace_back(std::move(entry.second)); + } + g_cachedBindGroups.clear(); +} + +static OffscreenCacheEntry get_offscreen_textures(uint32_t width, uint32_t height) { + OffscreenCacheKey key{width, height}; + if (const auto it = g_offscreenCache.find(key); it != g_offscreenCache.end()) { + return it->second; + } + const auto colorFormat = webgpu::g_graphicsConfig.surfaceConfiguration.format; + const wgpu::Extent3D size{width, height, 1}; + const wgpu::TextureDescriptor colorDesc{ + .label = "Offscreen Color", + .usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc | + wgpu::TextureUsage::CopyDst, + .dimension = wgpu::TextureDimension::e2D, + .size = size, + .format = colorFormat, + .mipLevelCount = 1, + .sampleCount = 1, + }; + auto colorTexture = g_device.CreateTexture(&colorDesc); + auto colorView = colorTexture.CreateView(); + webgpu::TextureWithSampler color{ + .texture = std::move(colorTexture), + .view = std::move(colorView), + .size = size, + .format = colorFormat, + }; + const auto depthFormat = webgpu::g_graphicsConfig.depthFormat; + const wgpu::TextureDescriptor depthDesc{ + .label = "Offscreen Depth", + .usage = wgpu::TextureUsage::RenderAttachment, + .dimension = wgpu::TextureDimension::e2D, + .size = size, + .format = depthFormat, + .mipLevelCount = 1, + .sampleCount = 1, + }; + auto depthTexture = g_device.CreateTexture(&depthDesc); + auto depthView = depthTexture.CreateView(); + webgpu::TextureWithSampler depth{ + .texture = std::move(depthTexture), + .view = std::move(depthView), + .size = size, + .format = depthFormat, + }; + OffscreenCacheEntry entry{ + .color = std::move(color), + .depth = std::move(depth), + }; + auto [insertIt, _] = g_offscreenCache.emplace(key, std::move(entry)); + return insertIt->second; +} + +void begin_offscreen(uint32_t width, uint32_t height) { + ZoneScoped; + CHECK(g_currentRenderPass != UINT32_MAX, "begin_offscreen called outside of a frame"); + + // If the current EFB pass has no resolve target, its output is unobservable. + // Suspend it so that we can resume it after the offscreen pass. + if (!g_inOffscreen) { + auto& currentPass = g_renderPasses[g_currentRenderPass]; + if (!currentPass.resolveTarget) { + g_suspendedEfbPass = std::move(currentPass); + g_renderPasses.pop_back(); + --g_currentRenderPass; + } + g_suspendedEfbViewport = g_cachedViewport; + g_suspendedEfbScissor = g_cachedScissor; + } + + // Create offscreen textures + auto offscreenEntry = get_offscreen_textures(width, height); + g_offscreenColor = std::move(offscreenEntry.color); + g_offscreenDepth = std::move(offscreenEntry.depth); + + // Start a new pass with offscreen targets + RenderPass newPass{ + .colorView = g_offscreenColor.view, + .depthView = g_offscreenDepth.view, + .copySourceTexture = g_offscreenColor.texture, + .copySourceView = g_offscreenColor.view, + .copySourceDepthView = g_offscreenDepth.view, + .targetSize = {width, height, 1}, + .msaaSamples = 1, + .clearColorValue = {0.f, 0.f, 0.f, 0.f}, + .clearDepthValue = 1.f, + .clearColor = true, + .clearDepth = true, + }; + push_render_pass(std::move(newPass)); + ++g_currentRenderPass; + + g_inOffscreen = true; + + g_cachedViewport = {0.f, 0.f, static_cast(width), static_cast(height), 0.f, 1.f}; + g_cachedScissor = {0, 0, static_cast(width), static_cast(height)}; + push_command(CommandType::SetViewport, Command::Data{.setViewport = g_cachedViewport}); + push_command(CommandType::SetScissor, Command::Data{.setScissor = g_cachedScissor}); +} + +void end_offscreen() { + ZoneScoped; + CHECK(g_inOffscreen, "end_offscreen called without begin_offscreen"); + + g_inOffscreen = false; + g_offscreenColor = {}; + g_offscreenDepth = {}; + + // Resume suspended EFB pass, or start a new one (load existing content) + if (g_suspendedEfbPass) { + // Keeps its own recorded commands. + g_renderPasses.emplace_back(std::move(*g_suspendedEfbPass)); + g_suspendedEfbPass.reset(); + } else { + auto& pass = push_render_pass(RenderPass{}); + pass.clearColor = false; + pass.clearDepth = false; + } + ++g_currentRenderPass; + set_efb_targets(g_renderPasses[g_currentRenderPass]); + + g_cachedViewport = g_suspendedEfbViewport; + g_cachedScissor = g_suspendedEfbScissor; + push_command(CommandType::SetViewport, Command::Data{.setViewport = g_cachedViewport}); + push_command(CommandType::SetScissor, Command::Data{.setScissor = g_cachedScissor}); +} + +template <> +void push_draw_command(gx::DrawData data) { + push_draw_command(ShaderDrawCommand{.type = ShaderType::GX, .gx = data}); +} + +template <> +PipelineRef pipeline_ref(const gx::PipelineConfig& config) { + return find_pipeline(ShaderType::GX, config, [=] { return create_pipeline(config); }); +} + +void initialize() { + g_frameIndex = 0; + depth_peek::initialize(); + tex_copy_conv::initialize(); + tex_palette_conv::initialize(); + texture_replacement::initialize(); + + // For uniform & storage buffer offset alignments + g_device.GetLimits(&g_cachedLimits); + + const auto createBuffer = [](wgpu::Buffer& out, wgpu::BufferUsage usage, uint64_t size, const char* label) { + if (size <= 0) { + return; + } + const wgpu::BufferDescriptor descriptor{ + .label = label, + .usage = usage, + .size = size, + }; + out = g_device.CreateBuffer(&descriptor); + }; + createBuffer(g_uniformBuffer, wgpu::BufferUsage::Uniform | wgpu::BufferUsage::CopyDst, UniformBufferSize, + "Shared Uniform Buffer"); + createBuffer(g_vertexBuffer, wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopyDst, VertexBufferSize, + "Shared Vertex Buffer"); + createBuffer(g_indexBuffer, wgpu::BufferUsage::Index | wgpu::BufferUsage::CopyDst, IndexBufferSize, + "Shared Index Buffer"); + createBuffer(g_storageBuffer, wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopyDst, StorageBufferSize, + "Shared Storage Buffer"); + for (int i = 0; i < g_stagingBuffers.size(); ++i) { + const auto label = fmt::format("Staging Buffer {}", i); + createBuffer(g_stagingBuffers[i], wgpu::BufferUsage::MapWrite | wgpu::BufferUsage::CopySrc, StagingBufferSize, + label.c_str()); + } + currentStagingBuffer = 0; + s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release); + map_staging_buffer(); + + { + constexpr std::array layoutEntries{ + // Vertex data buffer + wgpu::BindGroupLayoutEntry{ + .binding = 0, + .visibility = wgpu::ShaderStage::Vertex, + .buffer = + wgpu::BufferBindingLayout{ + .type = wgpu::BufferBindingType::ReadOnlyStorage, + }, + }, + // Storage data buffer + wgpu::BindGroupLayoutEntry{ + .binding = 1, + .visibility = wgpu::ShaderStage::Vertex, + .buffer = + wgpu::BufferBindingLayout{ + .type = wgpu::BufferBindingType::ReadOnlyStorage, + }, + }, + }; + const wgpu::BindGroupLayoutDescriptor layoutDesc{ + .label = "Static bind group layout", + .entryCount = layoutEntries.size(), + .entries = layoutEntries.data(), + }; + g_staticBindGroupLayout = g_device.CreateBindGroupLayout(&layoutDesc); + const std::array entries{ + wgpu::BindGroupEntry{ + .binding = 0, + .buffer = g_vertexBuffer, + }, + wgpu::BindGroupEntry{ + .binding = 1, + .buffer = g_storageBuffer, + }, + }; + const wgpu::BindGroupDescriptor bindGroupDescriptor{ + .label = "Static bind group", + .layout = g_staticBindGroupLayout, + .entryCount = entries.size(), + .entries = entries.data(), + }; + g_staticBindGroup = g_device.CreateBindGroup(&bindGroupDescriptor); + } + + { + constexpr std::array layoutEntries{ + // Uniform buffer (dynamic offset) + wgpu::BindGroupLayoutEntry{ + .binding = 0, + .visibility = wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment, + .buffer = + wgpu::BufferBindingLayout{ + .type = wgpu::BufferBindingType::Uniform, + .hasDynamicOffset = true, + }, + }, + }; + const wgpu::BindGroupLayoutDescriptor layoutDesc{ + .label = "Uniform bind group layout", + .entryCount = layoutEntries.size(), + .entries = layoutEntries.data(), + }; + g_uniformBindGroupLayout = g_device.CreateBindGroupLayout(&layoutDesc); + const std::array entries{ + wgpu::BindGroupEntry{ + .binding = 0, + .buffer = g_uniformBuffer, + .size = gx::MaxUniformSize, + }, + }; + const wgpu::BindGroupDescriptor bindGroupDescriptor{ + .label = "Uniform bind group", + .layout = g_uniformBindGroupLayout, + .entryCount = entries.size(), + .entries = entries.data(), + }; + g_uniformBindGroup = g_device.CreateBindGroup(&bindGroupDescriptor); + } + + gx::initialize(); + initialize_pipeline_cache(); +} + +void shutdown() { + shutdown_pipeline_cache(); + gx::clear_shader_module_cache(); + efb_ram::shutdown(); + depth_peek::shutdown(); + tex_copy_conv::shutdown(); + tex_palette_conv::shutdown(); + texture_replacement::shutdown(); + gx::shutdown(); + + g_textureUploads.clear(); + g_cachedBindGroups.clear(); + g_retiredBindGroups.clear(); + g_cachedSamplers.clear(); + g_vertexBuffer = {}; + g_uniformBuffer = {}; + g_indexBuffer = {}; + g_storageBuffer = {}; + g_stagingBuffers.fill({}); + for (auto& pool : g_resolveSourceSnapshotPools) { + pool.entry.reset(); + } + discard_suspended_efb_pass(); + g_renderPasses.clear(); + g_commandListPool.clear(); + g_currentRenderPass = UINT32_MAX; + g_offscreenCache.clear(); + g_offscreenColor = {}; + g_offscreenDepth = {}; + g_staticBindGroup = {}; + g_staticBindGroupLayout = {}; + g_uniformBindGroup = {}; + g_uniformBindGroupLayout = {}; + g_inOffscreen = false; + g_frameIndex = UINT32_MAX; + currentStagingBuffer = 0; + s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release); +} + +void map_staging_buffer() { + auto expected = BufferMapState::Unmapped; + if (!s_mappingState.compare_exchange_strong(expected, BufferMapState::Mapping, std::memory_order_acq_rel, + std::memory_order_acquire)) { + return; + } + + g_stagingBuffers[currentStagingBuffer].MapAsync( + wgpu::MapMode::Write, 0, StagingBufferSize, wgpu::CallbackMode::AllowSpontaneous, + [](wgpu::MapAsyncStatus status, wgpu::StringView message) { + if (status == wgpu::MapAsyncStatus::CallbackCancelled || status == wgpu::MapAsyncStatus::Aborted) { + Log.warn("Buffer mapping {}: {}", magic_enum::enum_name(status), message); + s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release); + return; + } + ASSERT(status == wgpu::MapAsyncStatus::Success, "Buffer mapping failed: {} {}", magic_enum::enum_name(status), + message); + s_mappingState.store(BufferMapState::Mapped, std::memory_order_release); + }); +} + +static bool begin_frame_impl(bool clearEfb) { + ZoneScoped; + { + ZoneScopedN("Wait for buffer map"); + map_staging_buffer(); + while (true) { + const auto mappingState = s_mappingState.load(std::memory_order_acquire); + if (mappingState == BufferMapState::Mapped) { + break; + } + if (mappingState == BufferMapState::Unmapped) { + // Frame begin failed because the staging map was aborted; the caller's retry loop drops the frame + // and any one-shot bakes recorded into it. Rate-limited. + static uint32_t s_beginFrameMapFailCount = 0; + if (s_beginFrameMapFailCount < 64 || (s_beginFrameMapFailCount & 255) == 0) { + Log.warn("begin_frame aborted: staging buffer unmapped (frame={} occurrences={})", g_frameIndex, + s_beginFrameMapFailCount + 1); + } + ++s_beginFrameMapFailCount; + return false; + } + g_instance.ProcessEvents(); + } + } + g_recordingSnapshotSlot = currentStagingBuffer; + size_t bufferOffset = 0; + const auto& stagingBuf = g_stagingBuffers[currentStagingBuffer]; + const auto mapBuffer = [&](ByteBuffer& buf, uint64_t size) { + if (size <= 0) { + return; + } + buf = ByteBuffer{static_cast(stagingBuf.GetMappedRange(bufferOffset, size)), static_cast(size)}; + bufferOffset += size; + }; + mapBuffer(g_verts, VertexBufferSize); + mapBuffer(g_uniforms, UniformBufferSize); + mapBuffer(g_indices, IndexBufferSize); + mapBuffer(g_storage, StorageBufferSize); + if constexpr (UseTextureBuffer) { + mapBuffer(g_textureUpload, TextureUploadSize); + } + + g_drawCallCount = 0; + g_mergedDrawCallCount = 0; + if (clearEfb) { + gx::begin_frame_interpolation(); + } + discard_suspended_efb_pass(); + webgpu::clear_present_source_override(); + + push_render_pass(RenderPass{}); + set_efb_targets(g_renderPasses[0]); + g_renderPasses[0].clearColorValue = gx::g_gxState.clearColor; + g_renderPasses[0].clearDepthValue = gx::clear_depth_value(); + g_renderPasses[0].clearColor = clearEfb; + g_renderPasses[0].clearDepth = clearEfb; + g_currentRenderPass = 0; + // Refresh paired render viewport/scissor from logical state in case the FB size changed. + const auto mappedRenderState = gx::map_logical_render_state(); + const bool renderStateChanged = gx::g_gxState.renderViewport != mappedRenderState.viewport || + gx::g_gxState.renderScissor != mappedRenderState.scissor; + gx::g_gxState.renderViewport = mappedRenderState.viewport; + gx::g_gxState.renderScissor = mappedRenderState.scissor; + gx::g_gxState.stateDirty = gx::g_gxState.stateDirty || renderStateChanged; + g_cachedViewport = mappedRenderState.viewport; + g_cachedScissor = mappedRenderState.scissor; + push_command(CommandType::SetViewport, Command::Data{.setViewport = g_cachedViewport}); + push_command(CommandType::SetScissor, Command::Data{.setScissor = g_cachedScissor}); + begin_pipeline_frame(); + return true; +} + +bool begin_frame() { return begin_frame_impl(true); } + +bool resume_frame() { return begin_frame_impl(false); } + +void abort_frame() noexcept { + efb_ram::cancel(); + efb_ram::abort_async(); + g_verts.release(); + g_uniforms.release(); + g_indices.release(); + g_storage.release(); + if constexpr (UseTextureBuffer) { + g_textureUploads.clear(); + g_textureUpload.release(); + } + if (s_mappingState.load(std::memory_order_acquire) == BufferMapState::Mapped) { + // Pending interpolation tasks hold raw pointers into the mapped staging + // range; they must be dropped before the buffer is unmapped and rotated. + gx::drop_pending_frame_interpolation_uniforms(); + g_stagingBuffers[currentStagingBuffer].Unmap(); + s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release); + currentStagingBuffer = (currentStagingBuffer + 1) % g_stagingBuffers.size(); + map_staging_buffer(); + } + recycle_render_passes(g_renderPasses); + g_currentRenderPass = UINT32_MAX; + discard_suspended_efb_pass(); + g_inOffscreen = false; + for (auto& array : gx::g_gxState.arrays) { + array.cachedRange = {}; + } + webgpu::clear_present_source_override(); + end_pipeline_frame(); +} + +static void end_batch_impl(const wgpu::CommandEncoder& cmd, bool advanceFrame) { + ZoneScoped; + ASSERT(!g_inOffscreen, "end_frame called while offscreen rendering is active"); + if (advanceFrame) { + gx::finalize_frame_interpolation(); + } else { + // Mid-frame batch split: the staging buffer is about to be unmapped and rotated, so pending + // interpolation tasks pointing into it would dangle. Tie the clear to the rotation itself. + gx::drop_pending_frame_interpolation_uniforms(); + } + g_uniforms.append_zeroes(gx::MaxUniformSize); // Pad the end of the buffer + uint64_t bufferOffset = 0; + const auto writeBuffer = [&](ByteBuffer& buf, wgpu::Buffer& out, uint64_t size, std::string_view label) { + const auto writeSize = buf.size(); // Only need to copy this many bytes + if (writeSize > 0) { + cmd.CopyBufferToBuffer(g_stagingBuffers[currentStagingBuffer], bufferOffset, out, 0, AURORA_ALIGN(writeSize, 4)); + buf.release(); + } + bufferOffset += size; + return writeSize; + }; + g_stagingBuffers[currentStagingBuffer].Unmap(); + s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release); + g_stats.drawCallCount = g_drawCallCount; + g_stats.mergedDrawCallCount = g_mergedDrawCallCount; + g_stats.lastVertSize = writeBuffer(g_verts, g_vertexBuffer, VertexBufferSize, "Vertex"); + g_stats.lastUniformSize = writeBuffer(g_uniforms, g_uniformBuffer, UniformBufferSize, "Uniform"); + g_stats.lastIndexSize = writeBuffer(g_indices, g_indexBuffer, IndexBufferSize, "Index"); + g_stats.lastStorageSize = writeBuffer(g_storage, g_storageBuffer, StorageBufferSize, "Storage"); + if constexpr (UseTextureBuffer) { + g_stats.lastTextureUploadSize = g_textureUpload.size(); + { + // Perform texture copies + for (const auto& item : g_textureUploads) { + const wgpu::TexelCopyBufferInfo buf{ + .layout = + wgpu::TexelCopyBufferLayout{ + .offset = item.layout.offset + bufferOffset, + .bytesPerRow = AURORA_ALIGN(item.layout.bytesPerRow, 256), + .rowsPerImage = item.layout.rowsPerImage, + }, + .buffer = g_stagingBuffers[currentStagingBuffer], + }; + cmd.CopyBufferToTexture(&buf, &item.tex, &item.size); + } + g_textureUploads.clear(); + g_textureUpload.release(); + } + } + currentStagingBuffer = (currentStagingBuffer + 1) % g_stagingBuffers.size(); + map_staging_buffer(); + g_currentRenderPass = UINT32_MAX; + for (auto& array : gx::g_gxState.arrays) { + array.cachedRange = {}; + } + end_pipeline_frame(); + if (advanceFrame) { + ++g_frameIndex; + } +} + +void end_frame(const wgpu::CommandEncoder& cmd) { end_batch_impl(cmd, true); } + +void end_batch(const wgpu::CommandEncoder& cmd) { end_batch_impl(cmd, false); } + +uint32_t current_frame() noexcept { return g_frameIndex; } + +// The only place that erases from g_cachedBindGroups, whose handles the frame being encoded still +// holds, so it runs in the seal prologue with the renderer mutex held and the producer excluded. +void expire_bind_group_cache() noexcept { + if (g_cachedBindGroups.empty() || g_frameIndex == UINT32_MAX || g_frameIndex % BindGroupCacheSweepPeriod != 0) { + return; + } + + ZoneScoped; + for (auto it = g_cachedBindGroups.begin(); it != g_cachedBindGroups.end();) { + if (g_frameIndex - it->second.lastUsedFrame > BindGroupCacheRetainFrames) { + g_cachedBindGroups.erase(it++); + } else { + ++it; + } + } +} + +// Debug labels for the render passes. Formatting them per pass per slot costs a heap-allocating +// fmt::format for a name nothing reads outside a capture. +static const char* render_pass_label(u32 index) noexcept { + static constexpr std::array kRenderPassLabels{ + "Render pass 0", "Render pass 1", "Render pass 2", "Render pass 3", + "Render pass 4", "Render pass 5", "Render pass 6", "Render pass 7", + "Render pass 8", "Render pass 9", "Render pass 10", "Render pass 11", + "Render pass 12", "Render pass 13", "Render pass 14", "Render pass 15", + }; + return index < kRenderPassLabels.size() ? kRenderPassLabels[index] : "Render pass"; +} + +static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vector& passes, u32 idx, + int32_t interpolatedFrame); + +static void render_impl(std::vector& renderPasses, wgpu::CommandEncoder& cmd, int32_t interpolatedFrame, + bool finalize) { + ZoneScoped; + // Palette conversions, MSAA resolves and EFB copies depend on sealed frame state, not on the + // interpolation weight, so encode them on the native render and let replay slots sample them. + const bool encodeTextureBakes = interpolatedFrame < 0; + for (u32 i = 0; i < renderPasses.size(); ++i) { + const auto& passInfo = renderPasses[i]; + if (encodeTextureBakes) { + for (const auto& conv : passInfo.paletteConvs) { + tex_palette_conv::run(cmd, conv); + } + } + const bool hasRenderWork = passInfo.clearColor || passInfo.clearDepth || !passInfo.commands.empty(); + if (i == renderPasses.size() - 1) { + ASSERT(!passInfo.resolveTarget, "Final render pass must not have resolve target"); + } else if (!(passInfo.resolveTarget && encodeTextureBakes) && !hasRenderWork) { + // Skip only empty intermediate passes: offscreen and scratch passes with resolves still have to + // run for later samplers, and on a replay slot a resolve-only pass has nothing to encode. + continue; + } + + const std::array attachments{ + wgpu::RenderPassColorAttachment{ + .view = passInfo.colorView, + .resolveTarget = passInfo.resolveView, + .loadOp = passInfo.clearColor ? wgpu::LoadOp::Clear : wgpu::LoadOp::Load, + .storeOp = wgpu::StoreOp::Store, + .clearValue = + { + .r = passInfo.clearColorValue.x(), + .g = passInfo.clearColorValue.y(), + .b = passInfo.clearColorValue.z(), + .a = passInfo.clearColorValue.w(), + }, + }, + }; + const wgpu::RenderPassDepthStencilAttachment depthStencilAttachment{ + .view = passInfo.depthView, + .depthLoadOp = passInfo.clearDepth ? wgpu::LoadOp::Clear : wgpu::LoadOp::Load, + .depthStoreOp = wgpu::StoreOp::Store, + .depthClearValue = passInfo.clearDepthValue, + }; + const wgpu::RenderPassDescriptor renderPassDescriptor{ + .label = render_pass_label(i), + .colorAttachmentCount = attachments.size(), + .colorAttachments = attachments.data(), + .depthStencilAttachment = &depthStencilAttachment, + }; + + auto pass = cmd.BeginRenderPass(&renderPassDescriptor); + render_pass_impl(pass, renderPasses, i, interpolatedFrame); + pass.End(); + + if (finalize && i == renderPasses.size() - 1) { + depth_peek::encode_frame_snapshot(cmd, passInfo.copySourceDepthView, passInfo.targetSize, passInfo.msaaSamples); + } + + if (passInfo.resolveTarget) { + const bool isDepth = gx::is_depth_format(passInfo.resolveFormat); + wgpu::Texture resolveSourceTexture = passInfo.copySourceTexture; + wgpu::TextureView resolveSourceView = isDepth ? passInfo.copySourceDepthView : passInfo.copySourceView; + if (passInfo.snapshotColorResolveSource && passInfo.resolveSourceSnapshot) { + const wgpu::TexelCopyTextureInfo src{ + .texture = passInfo.copySourceTexture, + .origin = wgpu::Origin3D{ + .x = static_cast(passInfo.resolveSnapshotRect.x), + .y = static_cast(passInfo.resolveSnapshotRect.y), + }, + }; + const wgpu::TexelCopyTextureInfo dst{ + .texture = passInfo.resolveSourceSnapshot->texture, + }; + const wgpu::Extent3D size{ + .width = static_cast(passInfo.resolveSnapshotRect.width), + .height = static_cast(passInfo.resolveSnapshotRect.height), + .depthOrArrayLayers = 1, + }; + cmd.CopyTextureToTexture(&src, &dst, &size); + resolveSourceTexture = passInfo.resolveSourceSnapshot->texture; + resolveSourceView = passInfo.resolveSourceSnapshot->sampleTextureView; + } + if (isDepth && passInfo.msaaSamples > 1) { + Log.fatal("Depth tex copies from multisampled EFB targets are not supported"); + } + const tex_copy_conv::ConvRequest convReq{ + .fmt = passInfo.resolveFormat, + .srcView = resolveSourceView, + .uniformRange = passInfo.resolveUniformRange, + .dst = passInfo.resolveTarget, + .sampleFilter = passInfo.resolveLinearSampling + ? tex_copy_conv::SampleFilter::Linear + : tex_copy_conv::SampleFilter::Nearest, + .forceOpaqueAlpha = passInfo.resolveForceOpaqueAlpha, + }; + if (passInfo.resolveNeedsConversion) { + tex_copy_conv::run(cmd, convReq); + } else if (passInfo.resolveNeedsShaderSampling) { + tex_copy_conv::blit(cmd, convReq); + } else { + const wgpu::TexelCopyTextureInfo src{ + .texture = resolveSourceTexture, + .origin = + wgpu::Origin3D{ + .x = static_cast(passInfo.resolveRect.x - + (passInfo.snapshotColorResolveSource + ? passInfo.resolveSnapshotRect.x : 0)), + .y = static_cast(passInfo.resolveRect.y - + (passInfo.snapshotColorResolveSource + ? passInfo.resolveSnapshotRect.y : 0)), + }, + }; + const wgpu::TexelCopyTextureInfo dst{ + .texture = passInfo.resolveTarget->texture, + }; + const wgpu::Extent3D size{ + .width = static_cast(passInfo.resolveRect.width), + .height = static_cast(passInfo.resolveRect.height), + .depthOrArrayLayers = 1, + }; + cmd.CopyTextureToTexture(&src, &dst, &size); + } + } + } + if (finalize) { + recycle_render_passes(renderPasses); + } + +#if defined(AURORA_GFX_DEBUG_GROUPS) + if (finalize && !g_debugGroupStack.empty()) { + for (auto& it : std::ranges::reverse_view(g_debugGroupStack)) { + Log.warn("Debug group was not popped at end of frame: {}", it); + } + g_debugGroupStack.clear(); + } + + if (finalize && g_debugMarkers.size() > 0) { + g_debugMarkers.clear(); + } +#endif +} + +void seal_frame(SealedFrame& out) noexcept { + ZoneScoped; + // The encode that could still have been holding these has completed: the + // producer joins the worker's DONE phase before it seals another frame. + g_retiredBindGroups.clear(); + auto& passes = out.data().passes; + // The previous cycle already recycled these, so this normally just hands the empty vector, its + // capacity included, back to the producer. + recycle_render_passes(passes); + passes.swap(g_renderPasses); + g_currentRenderPass = UINT32_MAX; +} + +void render(SealedFrame& frame, wgpu::CommandEncoder& cmd, int32_t interpolatedFrame, bool finalize) { + render_impl(frame.data().passes, cmd, interpolatedFrame, finalize); +} + +void render(wgpu::CommandEncoder& cmd, int32_t interpolatedFrame, bool finalize) { + render_impl(g_renderPasses, cmd, interpolatedFrame, finalize); + if (finalize) { + g_currentRenderPass = UINT32_MAX; + expire_bind_group_cache(); + } +} + +void after_submit() noexcept { + depth_peek::after_submit(); + efb_ram::after_submit(); + // Retire this frame's completed GPU work. Dawn only reclaims destroyed resources inside a device + // tick, and a frame that never ticks keeps every released image and its memory for the run. + if (g_instance) { + g_instance.ProcessEvents(); + } +} + +static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vector& renderPasses, u32 idx, + int32_t interpolatedFrame) { + // Per-invocation, not per-process: two encoders can be recording at once. + gx::DrawEncodeState encodeState{}; + encodeState.boundTextureBindGroup = gx::g_emptyTextureBindGroup.Get(); +#ifdef AURORA_GFX_DEBUG_GROUPS + std::vector lastDebugGroupStack; +#endif + + // Bind static bind group for the whole pass + pass.SetBindGroup(0, g_staticBindGroup); + pass.SetBindGroup(2, gx::g_emptyTextureBindGroup); + + for (const auto& cmd : renderPasses[idx].commands) { +#ifdef AURORA_GFX_DEBUG_GROUPS + { + size_t firstDiff = lastDebugGroupStack.size(); + for (size_t i = 0; i < lastDebugGroupStack.size(); ++i) { + if (i >= cmd.debugGroupStack.size() || cmd.debugGroupStack[i] != lastDebugGroupStack[i]) { + firstDiff = i; + break; + } + } + for (size_t i = firstDiff; i < lastDebugGroupStack.size(); ++i) { + pass.PopDebugGroup(); + } + for (size_t i = firstDiff; i < cmd.debugGroupStack.size(); ++i) { + pass.PushDebugGroup(cmd.debugGroupStack[i].c_str()); + } + lastDebugGroupStack = cmd.debugGroupStack; + } +#endif + switch (cmd.type) { + case CommandType::SetViewport: { + const auto& vp = cmd.data.setViewport; + // WebGPU requires 0 <= minDepth <= maxDepth <= 1, and the guest's (near, far) order is already + // reproduced in clip space. Passing the raw swapped pair diverged per backend in release builds. + const float minDepth = std::clamp(std::min(vp.znear, vp.zfar), 0.0f, 1.0f); + const float maxDepth = std::clamp(std::max(vp.znear, vp.zfar), 0.0f, 1.0f); + pass.SetViewport(vp.left, vp.top, vp.width, vp.height, minDepth, maxDepth); + } break; + case CommandType::SetScissor: { + const auto& sc = cmd.data.setScissor; + const auto& size = renderPasses[idx].targetSize; + const auto left = std::clamp(sc.x, 0, static_cast(size.width)); + const auto top = std::clamp(sc.y, 0, static_cast(size.height)); + const auto right = + std::clamp(sc.x + sc.width, left, static_cast(size.width)); + const auto bottom = + std::clamp(sc.y + sc.height, top, static_cast(size.height)); + pass.SetScissorRect(static_cast(left), static_cast(top), + static_cast(right - left), static_cast(bottom - top)); + } break; + case CommandType::Draw: { + const auto& draw = cmd.data.draw; + switch (draw.type) { + case ShaderType::GX: { + const gfx::Range* uniformOverride = nullptr; + if (interpolatedFrame >= 0 && + static_cast(interpolatedFrame) < draw.gx.interpolatedUniformRanges.size() && + draw.gx.interpolatedUniformRanges[interpolatedFrame].size != 0) { + uniformOverride = &draw.gx.interpolatedUniformRanges[interpolatedFrame]; + } + gx::render(draw.gx, pass, encodeState, renderPasses[idx].requireReadyPipelines, uniformOverride); + } break; + case ShaderType::Clear: + clear::render(draw.clear, pass, renderPasses[idx].targetSize, encodeState.currentPipeline); + break; + } + } break; + case CommandType::DebugMarker: { +#if defined(AURORA_GFX_DEBUG_GROUPS) + pass.InsertDebugMarker(wgpu::StringView(g_debugMarkers[cmd.data.debugMarkerIndex])); +#endif + } break; + } + } + +#ifdef AURORA_GFX_DEBUG_GROUPS + for (size_t i = 0; i < lastDebugGroupStack.size(); ++i) { + pass.PopDebugGroup(); + } +#endif +} + +bool bind_pipeline(PipelineRef ref, const wgpu::RenderPassEncoder& pass, PipelineRef& currentPipeline, + bool requireReady) { + if (ref == currentPipeline) { + return true; + } + wgpu::RenderPipeline pipeline; + bool pipelineReady; + if (!skip_unready_pipelines()) { + pipelineReady = wait_pipeline(ref, pipeline); + } else if (requireReady) { + // The pass resolves into a persistent texture (a one-shot bake such as MKW's minimap), so a + // skipped draw would never be re-issued. These run behind loads, not mid-race. + pipelineReady = wait_pipeline_for_persistent_pass(ref, pipeline); + } else { + pipelineReady = try_pipeline(ref, pipeline); + } + if (!pipelineReady) { + return false; + } + pass.SetPipeline(pipeline); + currentPipeline = ref; + return true; +} + +static inline Range push(ByteBuffer& target, const uint8_t* data, size_t length, size_t alignment) { + size_t padding = 0; + if (alignment != 0) { + const size_t remainder = length % alignment; + if (remainder != 0) { + padding = alignment - remainder; + } + } + auto begin = target.size(); + if (length == 0) { + length = alignment; + target.append_zeroes(alignment); + } else { + target.append(data, length); + if (padding > 0) { + target.append_zeroes(padding); + } + } + return {static_cast(begin), static_cast(length + padding)}; +} +static inline Range map(ByteBuffer& target, size_t length, size_t alignment) { + size_t padding = 0; + if (alignment != 0) { + const size_t remainder = length % alignment; + if (remainder != 0) { + padding = alignment - remainder; + } + } + auto begin = target.size(); + if (length == 0) { + length = alignment; + target.append_zeroes(length); + } else { + // The caller fills [0, length); only the alignment padding needs clearing, + // otherwise it hands the next frame's readers whatever was there before. + target.append_uninitialized(length); + target.append_zeroes(padding); + } + return {static_cast(begin), static_cast(length + padding)}; +} +Range push_verts(const uint8_t* data, size_t length) { return push(g_verts, data, length, 0); } +Range push_indices(const uint8_t* data, size_t length) { return push(g_indices, data, length, 0); } +Range push_uniform(const uint8_t* data, size_t length) { + return push(g_uniforms, data, length, g_cachedLimits.minUniformBufferOffsetAlignment); +} +Range push_storage(const uint8_t* data, size_t length) { + return push(g_storage, data, length, g_cachedLimits.minStorageBufferOffsetAlignment); +} +Range push_texture_data(const uint8_t* data, size_t length, u32 bytesPerRow, u32 rowsPerImage) { + // For CopyBufferToTexture, we need an alignment of 256 per row (see Dawn kTextureBytesPerRowAlignment) + const auto copyBytesPerRow = AURORA_ALIGN(bytesPerRow, 256); + const auto range = map(g_textureUpload, copyBytesPerRow * rowsPerImage, 0); + u8* dst = g_textureUpload.data() + range.offset; + for (u32 i = 0; i < rowsPerImage; ++i) { + memcpy(dst, data, bytesPerRow); + data += bytesPerRow; + dst += copyBytesPerRow; + } + return range; +} +std::pair map_verts(size_t length) { + const auto range = map(g_verts, length, 4); + return {ByteBuffer{g_verts.data() + range.offset, range.size}, range}; +} +std::pair map_indices(size_t length) { + const auto range = map(g_indices, length, 4); + return {ByteBuffer{g_indices.data() + range.offset, range.size}, range}; +} +std::pair map_uniform(size_t length) { + const auto range = map(g_uniforms, length, g_cachedLimits.minUniformBufferOffsetAlignment); + return {ByteBuffer{g_uniforms.data() + range.offset, range.size}, range}; +} +std::pair copy_uniform(Range source) { + const auto destination = map(g_uniforms, source.size, g_cachedLimits.minUniformBufferOffsetAlignment); + std::memcpy(g_uniforms.data() + destination.offset, g_uniforms.data() + source.offset, source.size); + return {ByteBuffer{g_uniforms.data() + destination.offset, destination.size}, destination}; +} +std::pair map_storage(size_t length) { + const auto range = map(g_storage, length, g_cachedLimits.minStorageBufferOffsetAlignment); + return {ByteBuffer{g_storage.data() + range.offset, range.size}, range}; +} + +BindGroupRef bind_group_ref(const WGPUBindGroupDescriptor& descriptor) { + const auto id = xxh3_hash(descriptor); + const auto it = g_cachedBindGroups.find(id); + if (it == g_cachedBindGroups.end()) { + auto bg = wgpu::BindGroup::Acquire(wgpuDeviceCreateBindGroup(g_device.Get(), &descriptor)); + g_cachedBindGroups.emplace(id, CachedBindGroup{ + .bindGroup = std::move(bg), + .lastUsedFrame = g_frameIndex, + }); + } else { + it->second.lastUsedFrame = g_frameIndex; + } + return id; +} + +wgpu::BindGroup& find_bind_group(BindGroupRef id) { + const auto it = g_cachedBindGroups.find(id); + CHECK(it != g_cachedBindGroups.end(), "get_bind_group: failed to locate {:x}", id); + return it->second.bindGroup; +} + +wgpu::Sampler& sampler_ref(const wgpu::SamplerDescriptor& descriptor) { + const auto id = xxh3_hash(descriptor); + auto it = g_cachedSamplers.find(id); + if (it == g_cachedSamplers.end()) { + it = g_cachedSamplers.try_emplace(id, g_device.CreateSampler(&descriptor)).first; + } + return it->second; +} + +uint32_t align_uniform(uint32_t value) { return AURORA_ALIGN(value, g_cachedLimits.minUniformBufferOffsetAlignment); } + +void insert_debug_marker(std::string label) { +#if defined(AURORA_GFX_DEBUG_GROUPS) + auto idx = g_debugMarkers.size(); + g_debugMarkers.emplace_back(std::move(label)); + push_command(CommandType::DebugMarker, {.debugMarkerIndex = idx}); +#endif +} + +} // namespace aurora::gfx + +void aurora::gfx::push_debug_group(std::string label) { +#if defined(AURORA_GFX_DEBUG_GROUPS) + g_debugGroupStack.push_back(std::move(label)); +#endif +} +void aurora_push_debug_group(const char* label) { +#ifdef AURORA_GFX_DEBUG_GROUPS + aurora::gfx::g_debugGroupStack.emplace_back(label); +#endif +} +void aurora_pop_debug_group() { +#ifdef AURORA_GFX_DEBUG_GROUPS + if (aurora::gfx::g_debugGroupStack.empty()) { + aurora::gfx::Log.error("Debug group stack underflowed!"); + return; + } + + aurora::gfx::g_debugGroupStack.pop_back(); +#endif +} + +const AuroraStats* aurora_get_stats() { return &aurora::gfx::g_stats; } diff --git a/aurora-main/lib/gfx/common.hpp b/aurora-main/lib/gfx/common.hpp new file mode 100644 index 0000000..e0a24a3 --- /dev/null +++ b/aurora-main/lib/gfx/common.hpp @@ -0,0 +1,407 @@ +#pragma once + +#include "../internal.hpp" +#include "../webgpu/gpu.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#define XXH_STATIC_LINKING_ONLY +#include + +namespace aurora { +#if INTPTR_MAX == INT32_MAX +using HashType = XXH32_hash_t; +#else +using HashType = XXH64_hash_t; +#endif +static inline HashType xxh3_hash_s(const void* input, size_t len, HashType seed = 0) { + return static_cast(XXH3_64bits_withSeed(input, len, seed)); +} +template +static inline HashType xxh3_hash(const T& input, HashType seed = 0) { + // Validate that the type has no padding bytes, which can easily cause + // hash mismatches. This also disallows floats, but that's okay for us. + static_assert(std::has_unique_object_representations_v); + return xxh3_hash_s(&input, sizeof(T), seed); +} + +// Folds two 64-bit hashes. Chaining them instead drops XXH3 onto its seeded long path past 240 +// bytes, and that path regenerates a 192-byte secret on every call. +static inline HashType hash_combine(HashType lhs, HashType rhs) { + uint64_t mixed = static_cast(lhs) ^ (static_cast(rhs) + 0x9E3779B97F4A7C15ull + + (static_cast(lhs) << 6) + (static_cast(lhs) >> 2)); + mixed ^= mixed >> 33; + mixed *= 0xFF51AFD7ED558CCDull; + mixed ^= mixed >> 29; + return static_cast(mixed); +} + +// Guest-RAM write tracking hooks (see aurora_set_guest_write_hooks). A digest samples the +// generation before reading the bytes, so a racing write costs an extra digest, never a skipped one. +inline constexpr uint64_t kGuestWriteUntracked = AURORA_GUEST_WRITE_UNTRACKED; +inline AuroraGuestWriteGenerationCallback g_guestWriteGenerationHook = nullptr; +inline AuroraGuestWriteNotifyCallback g_guestWriteNotifyHook = nullptr; + +inline uint64_t guest_write_generation(const void* data, size_t size) noexcept { + if (g_guestWriteGenerationHook == nullptr || data == nullptr || size == 0) { + return kGuestWriteUntracked; + } + return g_guestWriteGenerationHook(data, size); +} + +inline void notify_guest_write(const void* data, size_t size) noexcept { + if (g_guestWriteNotifyHook == nullptr || data == nullptr || size == 0) { + return; + } + g_guestWriteNotifyHook(data, size); +} + +// True when `stored` was taken over the same untouched bytes, so its digest still describes them. +// The untracked sentinel never matches: a source aurora cannot watch is always re-digested. +inline bool guest_write_generation_matches(uint64_t stored, uint64_t current) noexcept { + return current != kGuestWriteUntracked && stored == current; +} + +class Hasher { +public: + explicit Hasher(const XXH64_hash_t seed = 0) { + XXH3_INITSTATE(&state); + XXH3_64bits_reset_withSeed(&state, seed); + } + + void update(const void* data, const size_t size) { XXH3_64bits_update(&state, data, size); } + + template + void update(const T& data) { + static_assert(std::has_unique_object_representations_v); + update(&data, sizeof(T)); + } + + [[nodiscard]] XXH64_hash_t digest() const { return XXH3_64bits_digest(&state); } + +private: + XXH3_state_t state; +}; + +class ByteBuffer { +public: + ByteBuffer() noexcept = default; + explicit ByteBuffer(size_t size) noexcept + : m_data(static_cast(calloc(1, size))), m_length(size), m_capacity(size) {} + explicit ByteBuffer(uint8_t* data, size_t size) noexcept : m_data(data), m_capacity(size), m_owned(false) {} + ~ByteBuffer() noexcept { + if (m_data != nullptr && m_owned) { + free(m_data); + } + } + ByteBuffer(ByteBuffer&& rhs) noexcept + : m_data(rhs.m_data), m_length(rhs.m_length), m_capacity(rhs.m_capacity), m_owned(rhs.m_owned) { + rhs.m_data = nullptr; + rhs.m_length = 0; + rhs.m_capacity = 0; + rhs.m_owned = true; + } + ByteBuffer& operator=(ByteBuffer&& rhs) noexcept { + if (m_data != nullptr && m_owned) { + free(m_data); + } + m_data = rhs.m_data; + m_length = rhs.m_length; + m_capacity = rhs.m_capacity; + m_owned = rhs.m_owned; + rhs.m_data = nullptr; + rhs.m_length = 0; + rhs.m_capacity = 0; + rhs.m_owned = true; + return *this; + } + ByteBuffer(ByteBuffer const&) = delete; + ByteBuffer& operator=(ByteBuffer const&) = delete; + operator ArrayRef() const noexcept { return {m_data, m_length}; } + + [[nodiscard]] uint8_t* data() noexcept { return m_data; } + [[nodiscard]] const uint8_t* data() const noexcept { return m_data; } + [[nodiscard]] size_t size() const noexcept { return m_length; } + [[nodiscard]] bool empty() const noexcept { return m_length == 0; } + + void append(const void* data, size_t size) { + resize(m_length + size, false); + memcpy(m_data + m_length, data, size); + m_length += size; + } + + template + void append(const T& obj) { + append(&obj, sizeof(T)); + } + + void append_zeroes(size_t size) { + resize(m_length + size, true); + m_length += size; + } + + // Extend the buffer without clearing the new bytes. Only for callers that overwrite the whole + // region: the mapped staging buffers are megabytes of write-combine memory. + void append_uninitialized(size_t size) { + resize(m_length + size, false); + m_length += size; + } + + void release() { + if (m_data != nullptr && m_owned) { + free(m_data); + } + m_data = nullptr; + m_length = 0; + m_capacity = 0; + m_owned = true; + } + + void clear() { + m_length = 0; + } + + void reserve_extra(size_t size) { resize(m_length + size, true); } + + ByteBuffer clone() const { + ByteBuffer clone{m_length}; + std::memcpy(clone.data(), m_data, m_length); + return clone; + } + +private: + uint8_t* m_data = nullptr; + size_t m_length = 0; + size_t m_capacity = 0; + bool m_owned = true; + + // `size` is the total capacity needed. When `zeroed` is set, [m_length, size) has to read back as + // zero on every branch; the early return used to leave the previous frame's bytes in the padding. + void resize(size_t size, bool zeroed) { + if (size == 0) { + clear(); + return; + } + const size_t zeroBegin = m_length; + if (m_data == nullptr) { + if (zeroed) { + m_data = static_cast(calloc(1, size)); + } else { + m_data = static_cast(malloc(size)); + } + m_owned = true; + m_capacity = size; + // calloc already cleared the whole allocation. + return; + } + if (size > m_capacity) { + if (!m_owned) { + abort(); + } + // Exponential expansion to avoid O(n^2) time complexity. + size_t capacity = size; + if (capacity < m_capacity * 2) { + capacity = m_capacity * 2; + } + m_data = static_cast(realloc(m_data, capacity)); + m_capacity = capacity; + } + if (zeroed && size > zeroBegin) { + memset(m_data + zeroBegin, 0, size - zeroBegin); + } + } +}; +} // namespace aurora + +namespace aurora::gfx { +inline constexpr bool UseTextureBuffer = false; +inline constexpr uint64_t UniformBufferSize = 25165824; // 24mb +inline constexpr uint64_t VertexBufferSize = 3145728; // 3mb +inline constexpr uint64_t IndexBufferSize = 2097152; // 2mb +inline constexpr uint64_t StorageBufferSize = 8388608; // 8mb +inline constexpr uint64_t TextureUploadSize = 25165824; // 24mb + +extern AuroraStats g_stats; +extern uint32_t g_drawCallCount; +extern uint32_t g_mergedDrawCallCount; +extern wgpu::Buffer g_vertexBuffer; +extern wgpu::Buffer g_uniformBuffer; +extern wgpu::Buffer g_indexBuffer; +extern wgpu::Buffer g_storageBuffer; +extern wgpu::BindGroupLayout g_staticBindGroupLayout; +extern wgpu::BindGroup g_staticBindGroup; +extern wgpu::BindGroupLayout g_uniformBindGroupLayout; +extern wgpu::BindGroup g_uniformBindGroup; + +using BindGroupRef = HashType; +using PipelineRef = HashType; +using SamplerRef = HashType; +using ShaderRef = HashType; +struct Range { + uint32_t offset = 0; + uint32_t size = 0; + + bool operator==(const Range& rhs) const { return memcmp(this, &rhs, sizeof(*this)) == 0; } + bool operator!=(const Range& rhs) const { return !(*this == rhs); } +}; + +struct ClipRect { + int32_t x; + int32_t y; + int32_t width; + int32_t height; + + bool operator==(const ClipRect& rhs) const { return memcmp(this, &rhs, sizeof(*this)) == 0; } + bool operator!=(const ClipRect& rhs) const { return !(*this == rhs); } +}; + +using webgpu::Viewport; + +struct TextureRef; +using TextureHandle = std::shared_ptr; + +enum class ShaderType : uint8_t { + Clear = 0, + GX = 1, +}; + +void initialize(); +void shutdown(); + +bool begin_frame(); +bool resume_frame(); +void abort_frame() noexcept; +void end_frame(const wgpu::CommandEncoder& cmd); +void end_batch(const wgpu::CommandEncoder& cmd); +uint32_t current_frame() noexcept; + +// One frame's recorded render passes, detached from the guest-visible +// recording state. The contents are private to common.cpp. +struct SealedFrameData; + +// Owns the recorded passes of a sealed frame, touched only by the thread that sealed it. +// seal_frame() runs with the producer excluded, and the object is reused to keep its capacity. +class SealedFrame { +public: + SealedFrame(); + ~SealedFrame(); + SealedFrame(SealedFrame&&) noexcept; + SealedFrame& operator=(SealedFrame&&) noexcept; + SealedFrame(const SealedFrame&) = delete; + SealedFrame& operator=(const SealedFrame&) = delete; + + [[nodiscard]] SealedFrameData& data() const noexcept { return *m_data; } + +private: + std::unique_ptr m_data; +}; + +// Detach the recorded passes of the frame that just ended into `out`. Must be +// called with the renderer GPU mutex held; see SealedFrame. +void seal_frame(SealedFrame& out) noexcept; + +// Encode a sealed frame. Never touches the producer-visible recording state, +// so this may run concurrently with the producer's FIFO drains. +void render(SealedFrame& frame, wgpu::CommandEncoder& cmd, int32_t interpolatedFrame = -1, bool finalize = true); + +// Encode the frame that is still being recorded. Only for the synchronous +// EFB-readback split path, which runs on the producer thread. +void render(wgpu::CommandEncoder& cmd, int32_t interpolatedFrame = -1, bool finalize = true); + +// Sweep bind groups that have not been used for a while. Mutates the cache the producer inserts +// into, so it may only run with the producer excluded, inside the seal prologue. +void expire_bind_group_cache() noexcept; +void after_submit() noexcept; +void map_staging_buffer(); +// `persistentCopy` marks resolves whose destination outlives the frame with no re-issue path, so +// the pass waits for its pipelines instead of dropping draws that would be baked in permanently. +void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool clearAlpha, bool clearDepth, + Vec4 clearColorValue, float clearDepthValue, GXTexFmt resolveFormat = GX_TF_RGBA8, + const Vec4* sourceRectPixels = nullptr, bool halfScale = false, + const std::array* copyFilterCoefficients = nullptr, bool forceOpaqueAlpha = false, + float copyFilterRowStride = 1.0f, bool clampTop = false, bool clampBottom = false, + bool persistentCopy = false); + +void begin_offscreen(uint32_t width, uint32_t height); +void end_offscreen(); +bool is_offscreen() noexcept; +uint32_t get_sample_count() noexcept; +void clear_caches() noexcept; + +namespace tex_palette_conv { +struct ConvRequest; +} // namespace tex_palette_conv +void queue_palette_conv(tex_palette_conv::ConvRequest req); + +Range push_verts(const uint8_t* data, size_t length); +template +static Range push_verts(ArrayRef data) { + return push_verts(reinterpret_cast(data.data()), data.size() * sizeof(T)); +} +Range push_indices(const uint8_t* data, size_t length); +template +static Range push_indices(ArrayRef data) { + return push_indices(reinterpret_cast(data.data()), data.size() * sizeof(T)); +} +Range push_uniform(const uint8_t* data, size_t length); +template +static Range push_uniform(const T& data) { + return push_uniform(reinterpret_cast(&data), sizeof(T)); +} +Range push_storage(const uint8_t* data, size_t length); +template +static Range push_storage(ArrayRef data) { + return push_storage(reinterpret_cast(data.data()), data.size() * sizeof(T)); +} +template +static Range push_storage(const T& data) { + return push_storage(reinterpret_cast(&data), sizeof(T)); +} +Range push_texture_data(const uint8_t* data, size_t length, uint32_t bytesPerRow, uint32_t rowsPerImage); +std::pair map_verts(size_t length); +std::pair map_indices(size_t length); +std::pair map_uniform(size_t length); +std::pair copy_uniform(Range source); +std::pair map_storage(size_t length); + +template +const State& get_state(); +template +void push_draw_command(DrawData data); +template +DrawData* get_last_draw_command(); + +template +PipelineRef pipeline_ref(const PipelineConfig& config); +// `currentPipeline` is the caller's per-pass dedupe slot; as a file-static, two concurrent encoders +// skipped each other's SetPipeline. `requireReady` refuses the skip-unready shortcut for bakes. +bool bind_pipeline(PipelineRef ref, const wgpu::RenderPassEncoder& pass, PipelineRef& currentPipeline, + bool requireReady = false); + +BindGroupRef bind_group_ref(const WGPUBindGroupDescriptor& descriptor); +wgpu::BindGroup& find_bind_group(BindGroupRef id); + +wgpu::Sampler& sampler_ref(const wgpu::SamplerDescriptor& descriptor); + +uint32_t align_uniform(uint32_t value); + +Vec2 get_render_target_size() noexcept; +// Same value as get_render_target_size() outside a render pass, but never +// touches the frame worker's render-pass list, so it is safe off-thread. +Vec2 get_frame_buffer_size() noexcept; +void set_viewport(const Viewport& viewport) noexcept; +void set_scissor(const ClipRect& scissor) noexcept; + +void push_debug_group(std::string label); +void insert_debug_marker(std::string label); +} // namespace aurora::gfx diff --git a/aurora-main/lib/gfx/dds_io.cpp b/aurora-main/lib/gfx/dds_io.cpp new file mode 100644 index 0000000..84eecc2 --- /dev/null +++ b/aurora-main/lib/gfx/dds_io.cpp @@ -0,0 +1,256 @@ +#include "dds_io.hpp" + +#include +#include +#include + +#include "texture.hpp" + +namespace aurora::gfx::dds { +struct ParsedDDSLayout { + wgpu::TextureFormat format = wgpu::TextureFormat::Undefined; + size_t dataOffset = 0; +}; + +struct DDSPixelFormat { + uint32_t size; + uint32_t flags; + uint32_t fourCC; + uint32_t rgbBitCount; + uint32_t rBitMask; + uint32_t gBitMask; + uint32_t bBitMask; + uint32_t aBitMask; +}; +static_assert(sizeof(DDSPixelFormat) == 32); + +struct DDSHeader { + uint32_t size; + uint32_t flags; + uint32_t height; + uint32_t width; + uint32_t pitchOrLinearSize; + uint32_t depth; + uint32_t mipMapCount; + uint32_t reserved1[11]; + DDSPixelFormat ddspf; + uint32_t caps; + uint32_t caps2; + uint32_t caps3; + uint32_t caps4; + uint32_t reserved2; +}; +static_assert(sizeof(DDSHeader) == 124); + +struct DDSHeaderDX10 { + uint32_t dxgiFormat; + uint32_t resourceDimension; + uint32_t miscFlag; + uint32_t arraySize; + uint32_t miscFlags2; +}; +static_assert(sizeof(DDSHeaderDX10) == 20); + +constexpr uint32_t kDDSMagic = 0x20534444; // "DDS" + +bool ensure_directory(const std::filesystem::path& dir) noexcept { + std::error_code ec; + std::filesystem::create_directories(dir, ec); + return !ec; +} + +bool write_binary_file(const std::filesystem::path& path, ArrayRef data) noexcept { + if (!ensure_directory(path.parent_path())) { + return false; + } + + std::ofstream out(path, std::ios::binary | std::ios::trunc); + if (!out) { + return false; + } + + out.write(reinterpret_cast(data.data()), data.size()); + return static_cast(out); +} + +std::optional read_binary_file(const std::filesystem::path& path) noexcept { + std::ifstream file(path, std::ios::binary | std::ios::ate); + if (!file) { + return std::nullopt; + } + + const auto size = static_cast(file.tellg()); + if (size == 0) { + return std::nullopt; + } + + ByteBuffer bytes{size}; + file.seekg(0, std::ios::beg); + file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + if (!file) { + return std::nullopt; + } + + return bytes; +} + +bool validate_dds_header(const DDSHeader& header) noexcept { + if (header.size != sizeof(DDSHeader) || header.ddspf.size != sizeof(DDSPixelFormat)) { + return false; + } + if (header.width == 0 || header.height == 0) { + return false; + } + if ((header.caps2 & (0x00000200 | 0x00200000)) != 0) { // Unsupported + return false; + } + return true; +} + +std::optional resolve_dx10_format(uint32_t dxgiFormat) noexcept { + switch (dxgiFormat) { + case 28: + return wgpu::TextureFormat::RGBA8Unorm; + case 87: + return wgpu::TextureFormat::BGRA8Unorm; + case 71: + return wgpu::TextureFormat::BC1RGBAUnorm; + case 77: + return wgpu::TextureFormat::BC3RGBAUnorm; + case 83: + return wgpu::TextureFormat::BC5RGUnorm; + case 98: + return wgpu::TextureFormat::BC7RGBAUnorm; + default: + return std::nullopt; + } +} + +std::optional resolve_dds_layout(ArrayRef bytes, const DDSHeader& header) noexcept { + ParsedDDSLayout out{.dataOffset = sizeof(uint32_t) + sizeof(DDSHeader)}; + + if ((header.ddspf.flags & 0x00000004) != 0) { // DDS has FourCC + switch (header.ddspf.fourCC) { + case 0x31545844: + out.format = wgpu::TextureFormat::BC1RGBAUnorm; + return out; + case 0x35545844: + out.format = wgpu::TextureFormat::BC3RGBAUnorm; + return out; + case 0x32495441: + out.format = wgpu::TextureFormat::BC5RGUnorm; + return out; + case 0x30315844: { + if (bytes.size() < out.dataOffset + sizeof(DDSHeaderDX10)) { + return std::nullopt; + } + const auto* dx10 = reinterpret_cast(bytes.data() + out.dataOffset); + out.dataOffset += sizeof(DDSHeaderDX10); + if (dx10->resourceDimension != 3 || dx10->arraySize != 1) { + return std::nullopt; + } + const auto format = resolve_dx10_format(dx10->dxgiFormat); + if (!format.has_value()) { + return std::nullopt; + } + out.format = *format; + return out; + } + default: + return std::nullopt; + } + } + + if ((header.ddspf.flags & 0x00000040) != 0 && header.ddspf.rgbBitCount == 32) { + if (header.ddspf.rBitMask == 0x000000FF && header.ddspf.gBitMask == 0x0000FF00 && + header.ddspf.bBitMask == 0x00FF0000 && header.ddspf.aBitMask == 0xFF000000) { + out.format = wgpu::TextureFormat::RGBA8Unorm; + return out; + } + if (header.ddspf.rBitMask == 0x00FF0000 && header.ddspf.gBitMask == 0x0000FF00 && + header.ddspf.bBitMask == 0x000000FF && header.ddspf.aBitMask == 0xFF000000) { + out.format = wgpu::TextureFormat::BGRA8Unorm; + return out; + } + } + + return std::nullopt; +} + +std::optional parse_dds_bytes(ArrayRef bytes) noexcept { + if (bytes.size() < sizeof(uint32_t) + sizeof(DDSHeader)) { + return std::nullopt; + } + + const uint32_t magic = *reinterpret_cast(bytes.data()); + if (magic != kDDSMagic) { + return std::nullopt; + } + + const auto* header = reinterpret_cast(bytes.data() + sizeof(uint32_t)); + if (!validate_dds_header(*header)) { + return std::nullopt; + } + + const auto parsedLayout = resolve_dds_layout(bytes, *header); + if (!parsedLayout.has_value()) { + return std::nullopt; + } + + const uint32_t mipCount = 1u; + const auto expectedSize = calc_texture_size(parsedLayout->format, header->width, header->height, mipCount); + if (expectedSize == 0 || parsedLayout->dataOffset + expectedSize > bytes.size()) { + return std::nullopt; + } + + ByteBuffer data{expectedSize}; + std::memcpy(data.data(), bytes.data() + parsedLayout->dataOffset, expectedSize); + return ConvertedTexture{ + .format = parsedLayout->format, + .width = header->width, + .height = header->height, + .mips = mipCount, + .data = std::move(data), + }; +} + +std::optional load_dds_file(const std::filesystem::path& path) noexcept { + const auto bytes = read_binary_file(path); + if (!bytes.has_value()) { + return std::nullopt; + } + return parse_dds_bytes(*bytes); +} + +ByteBuffer encode_rgba8_dds(uint32_t width, uint32_t height, ArrayRef pixels) { + DDSHeader header{}; + header.size = sizeof(DDSHeader); + header.flags = 0x1 | 0x2 | 0x4 | 0x1000 | 0x8; + header.height = height; + header.width = width; + header.pitchOrLinearSize = width * 4; + header.mipMapCount = 1; + header.ddspf = { + .size = sizeof(DDSPixelFormat), + .flags = 0x00000040 | 0x00000001, + .fourCC = 0, + .rgbBitCount = 32, + .rBitMask = 0x000000FF, + .gBitMask = 0x0000FF00, + .bBitMask = 0x00FF0000, + .aBitMask = 0xFF000000, + }; + header.caps = 0x00001000; + + ByteBuffer bytes{sizeof(uint32_t) + sizeof(DDSHeader) + pixels.size()}; + std::memcpy(bytes.data(), &kDDSMagic, sizeof(kDDSMagic)); + std::memcpy(bytes.data() + sizeof(uint32_t), &header, sizeof(header)); + std::memcpy(bytes.data() + sizeof(uint32_t) + sizeof(DDSHeader), pixels.data(), pixels.size()); + return bytes; +} + +bool write_rgba8_dds(const std::filesystem::path& path, uint32_t width, uint32_t height, + ArrayRef pixels) noexcept { + return write_binary_file(path, encode_rgba8_dds(width, height, pixels)); +} +} // namespace aurora::gfx::dds diff --git a/aurora-main/lib/gfx/dds_io.hpp b/aurora-main/lib/gfx/dds_io.hpp new file mode 100644 index 0000000..2f275f2 --- /dev/null +++ b/aurora-main/lib/gfx/dds_io.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "common.hpp" +#include "texture_convert.hpp" + +#include +#include + +namespace aurora::gfx::dds { +std::optional parse_dds_bytes(ArrayRef bytes) noexcept; +std::optional load_dds_file(const std::filesystem::path& path) noexcept; +ByteBuffer encode_rgba8_dds(uint32_t width, uint32_t height, ArrayRef pixels); +bool write_rgba8_dds(const std::filesystem::path& path, uint32_t width, uint32_t height, ArrayRef pixels) noexcept; + +std::optional read_binary_file(const std::filesystem::path& path) noexcept; + +} // namespace aurora::gfx::dds diff --git a/aurora-main/lib/gfx/depth_peek.cpp b/aurora-main/lib/gfx/depth_peek.cpp new file mode 100644 index 0000000..b41ee92 --- /dev/null +++ b/aurora-main/lib/gfx/depth_peek.cpp @@ -0,0 +1,469 @@ +#include "depth_peek.hpp" + +#include "../dolphin/vi/vi_internal.hpp" +#include "../gx/gx.hpp" +#include "../webgpu/gpu.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +namespace aurora::gfx::depth_peek { +namespace { +Module Log("aurora::gfx::depth_peek"); + +using webgpu::g_device; +using webgpu::g_instance; +using webgpu::g_queue; + +using Clock = std::chrono::steady_clock; + +constexpr size_t SlotCount = 3; +constexpr uint32_t WorkgroupSizeX = 8; +constexpr uint32_t WorkgroupSizeY = 8; +constexpr uint32_t DepthPeekSnapshotHz = 30; +constexpr auto SnapshotInterval = std::chrono::nanoseconds{1'000'000'000 / DepthPeekSnapshotHz}; + +struct Params { + uint32_t dstWidth = 0; + uint32_t dstHeight = 0; + uint32_t srcWidth = 0; + uint32_t srcHeight = 0; + float offsetX = 0.f; + float offsetY = 0.f; + float scaleX = 1.f; + float scaleY = 1.f; +}; +static_assert(sizeof(Params) == 32); + +struct LatestSnapshot { + uint32_t width = 0; + uint32_t height = 0; + std::vector data; +}; + +enum class SlotState : uint8_t { + Available, + CopySubmitted, + MapPending, +}; + +struct Slot { + wgpu::Buffer storageBuffer; + wgpu::Buffer readbackBuffer; + wgpu::Buffer paramsBuffer; + uint32_t width = 0; + uint32_t height = 0; + uint64_t byteSize = 0; + SlotState state = SlotState::Available; +}; + +struct PendingMap { + size_t slotIdx = 0; + wgpu::Buffer readbackBuffer; + uint64_t byteSize = 0; +}; + +std::array g_slots; +size_t g_nextSlot = 0; +wgpu::BindGroupLayout g_bindGroupLayout; +wgpu::ComputePipeline g_pipeline; +bool g_snapshotRequested = false; +Clock::time_point g_nextSnapshotTime; +LatestSnapshot g_latest; +std::mutex g_mutex; + +constexpr std::string_view ShaderPreamble = R"( +struct Params { + dstSize: vec2u, + srcSize: vec2u, + offset: vec2f, + scale: vec2f, +}; + +@group(0) @binding(1) var out_z: array; +@group(0) @binding(2) var params: Params; +)"sv; + +constexpr std::string_view ReversedZBody = R"( +fn gx_z24(depth: f32) -> u32 { + return min(u32(clamp(depth, 0.0, 1.0) * 16777216.0), 0x00ffffffu); +} +)"sv; + +constexpr std::string_view ForwardZBody = R"( +fn gx_z24(depth: f32) -> u32 { + return min(u32(clamp(depth, 0.0, 1.0) * 16777215.0 + 0.5), 0x00ffffffu); +} +)"sv; + +constexpr std::string_view ShaderMain = R"( +@group(0) @binding(0) var src: texture_depth_2d; + +fn load_depth(coord: vec2i) -> f32 { + return textureLoad(src, coord, 0); +} + +@compute @workgroup_size(8, 8, 1) +fn cs_main(@builtin(global_invocation_id) id: vec3u) { + if (id.x >= params.dstSize.x || id.y >= params.dstSize.y) { + return; + } + + let dstCenter = vec2f(vec2u(id.xy)) + vec2f(0.5, 0.5); + let srcPixel = clamp(vec2i(floor(params.offset + dstCenter * params.scale)), vec2i(0, 0), + vec2i(params.srcSize) - vec2i(1, 1)); + let depth = load_depth(srcPixel); + out_z[id.y * params.dstSize.x + id.x] = gx_z24(depth); +} +)"sv; + +std::string build_shader_source() { + std::string source; + source.reserve(ShaderPreamble.size() + ReversedZBody.size() + ShaderMain.size()); + source += ShaderPreamble; + source += gx::UseReversedZ ? ReversedZBody : ForwardZBody; + source += ShaderMain; + return source; +} + +wgpu::ComputePipeline create_pipeline(const wgpu::BindGroupLayout& bindGroupLayout, const char* label) { + const auto shaderSource = build_shader_source(); + const wgpu::ShaderSourceWGSL wgslSource{wgpu::ShaderSourceWGSL::Init{ + .code = shaderSource.c_str(), + }}; + const wgpu::ShaderModuleDescriptor moduleDescriptor{ + .nextInChain = &wgslSource, + .label = label, + }; + const auto module = g_device.CreateShaderModule(&moduleDescriptor); + + const wgpu::PipelineLayoutDescriptor layoutDescriptor{ + .bindGroupLayoutCount = 1, + .bindGroupLayouts = &bindGroupLayout, + }; + const auto pipelineLayout = g_device.CreatePipelineLayout(&layoutDescriptor); + const wgpu::ComputePipelineDescriptor pipelineDescriptor{ + .label = label, + .layout = pipelineLayout, + .compute = + wgpu::ComputeState{ + .module = module, + .entryPoint = "cs_main", + }, + }; + return g_device.CreateComputePipeline(&pipelineDescriptor); +} + +wgpu::BindGroupLayout create_bind_group_layout(const char* label) { + constexpr std::array entries{ + wgpu::BindGroupLayoutEntry{ + .binding = 0, + .visibility = wgpu::ShaderStage::Compute, + .texture = + wgpu::TextureBindingLayout{ + .sampleType = wgpu::TextureSampleType::Depth, + .viewDimension = wgpu::TextureViewDimension::e2D, + }, + }, + wgpu::BindGroupLayoutEntry{ + .binding = 1, + .visibility = wgpu::ShaderStage::Compute, + .buffer = + wgpu::BufferBindingLayout{ + .type = wgpu::BufferBindingType::Storage, + }, + }, + wgpu::BindGroupLayoutEntry{ + .binding = 2, + .visibility = wgpu::ShaderStage::Compute, + .buffer = + wgpu::BufferBindingLayout{ + .type = wgpu::BufferBindingType::Uniform, + }, + }, + }; + const wgpu::BindGroupLayoutDescriptor descriptor{ + .label = label, + .entryCount = entries.size(), + .entries = entries.data(), + }; + return g_device.CreateBindGroupLayout(&descriptor); +} + +Params make_params(wgpu::Extent3D sourceSize, Vec2 dstSize) noexcept { + Params params{ + .dstWidth = dstSize.x, + .dstHeight = dstSize.y, + .srcWidth = sourceSize.width, + .srcHeight = sourceSize.height, + }; + + if (gx::g_gxState.viewportPolicy == AURORA_VIEWPORT_NATIVE) { + return params; + } + + const auto logicalSize = vi::configured_fb_size(); + if (logicalSize.x == 0 || logicalSize.y == 0 || sourceSize.width == 0 || sourceSize.height == 0) { + return params; + } + + const bool stretch = gx::g_gxState.viewportPolicy == AURORA_VIEWPORT_STRETCH; + const float scaleX = static_cast(sourceSize.width) / static_cast(logicalSize.x); + const float scaleY = static_cast(sourceSize.height) / static_cast(logicalSize.y); + const float scale = std::min(scaleX, scaleY); + params.scaleX = stretch ? scaleX : scale; + params.scaleY = stretch ? scaleY : scale; + params.offsetX = + stretch ? 0.f : (static_cast(sourceSize.width) - static_cast(logicalSize.x) * scale) * 0.5f; + params.offsetY = + stretch ? 0.f : (static_cast(sourceSize.height) - static_cast(logicalSize.y) * scale) * 0.5f; + return params; +} + +bool ensure_slot(Slot& slot, uint32_t width, uint32_t height) { + const uint64_t byteSize = static_cast(width) * static_cast(height) * sizeof(uint32_t); + if (slot.storageBuffer && slot.width == width && slot.height == height && slot.byteSize == byteSize) { + return true; + } + + slot.storageBuffer = {}; + slot.readbackBuffer = {}; + slot.paramsBuffer = {}; + slot.width = width; + slot.height = height; + slot.byteSize = byteSize; + + if (byteSize == 0 || byteSize > UINT32_MAX) { + return false; + } + + const wgpu::BufferDescriptor storageDescriptor{ + .label = "Depth Peek Storage Buffer", + .usage = wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopySrc, + .size = byteSize, + }; + slot.storageBuffer = g_device.CreateBuffer(&storageDescriptor); + + const wgpu::BufferDescriptor readbackDescriptor{ + .label = "Depth Peek Readback Buffer", + .usage = wgpu::BufferUsage::MapRead | wgpu::BufferUsage::CopyDst, + .size = byteSize, + }; + slot.readbackBuffer = g_device.CreateBuffer(&readbackDescriptor); + + const wgpu::BufferDescriptor paramsDescriptor{ + .label = "Depth Peek Params Buffer", + .usage = wgpu::BufferUsage::Uniform | wgpu::BufferUsage::CopyDst, + .size = sizeof(Params), + }; + slot.paramsBuffer = g_device.CreateBuffer(¶msDescriptor); + return slot.storageBuffer && slot.readbackBuffer && slot.paramsBuffer; +} + +Slot* find_available_slot(uint32_t width, uint32_t height) { + for (size_t i = 0; i < g_slots.size(); ++i) { + const size_t idx = (g_nextSlot + i) % g_slots.size(); + auto& slot = g_slots[idx]; + if (slot.state != SlotState::Available) { + continue; + } + if (!ensure_slot(slot, width, height)) { + continue; + } + g_nextSlot = (idx + 1) % g_slots.size(); + return &slot; + } + return nullptr; +} + +void complete_slot(size_t slotIdx, wgpu::MapAsyncStatus status, wgpu::StringView message) { + std::lock_guard lock{g_mutex}; + auto& slot = g_slots[slotIdx]; + if (status == wgpu::MapAsyncStatus::Success) { + const auto valueCount = static_cast(slot.width) * static_cast(slot.height); + const auto* mapped = + static_cast(slot.readbackBuffer.GetConstMappedRange(0, valueCount * sizeof(uint32_t))); + if (mapped != nullptr) { + g_latest.width = slot.width; + g_latest.height = slot.height; + g_latest.data.assign(mapped, mapped + valueCount); + } + slot.readbackBuffer.Unmap(); + } else if (status != wgpu::MapAsyncStatus::CallbackCancelled && status != wgpu::MapAsyncStatus::Aborted) { + Log.warn("Depth Peek readback mapping failed {}: {}", magic_enum::enum_name(status), message); + } + slot.state = SlotState::Available; +} +} // namespace + +void initialize() { + g_bindGroupLayout = create_bind_group_layout("Depth Peek Bind Group Layout"); + g_pipeline = create_pipeline(g_bindGroupLayout, "Depth Peek Pipeline"); +} + +void shutdown() { + testing::reset(); + g_pipeline = {}; + g_bindGroupLayout = {}; + for (auto& slot : g_slots) { + slot = {}; + } +} + +void request_snapshot() noexcept { + std::lock_guard lock{g_mutex}; + g_snapshotRequested = true; +} + +bool read_latest(uint16_t x, uint16_t y, uint32_t& z) noexcept { + std::lock_guard lock{g_mutex}; + if (x >= g_latest.width || y >= g_latest.height || g_latest.data.empty()) { + return false; + } + z = g_latest.data[static_cast(y) * g_latest.width + x] & 0x00ffffffu; + return true; +} + +void poll() noexcept { + if (g_instance) { + g_instance.ProcessEvents(); + } +} + +void encode_frame_snapshot(const wgpu::CommandEncoder& cmd, const wgpu::TextureView& depthView, + wgpu::Extent3D sourceSize, uint32_t msaaSamples) noexcept { + ZoneScoped; + const auto now = Clock::now(); + { + std::lock_guard lock{g_mutex}; + if (!g_snapshotRequested || now < g_nextSnapshotTime) { + return; + } + g_snapshotRequested = false; + g_nextSnapshotTime = now + SnapshotInterval; + } + + const auto dstSize = vi::configured_fb_size(); + if (!depthView || dstSize.x == 0 || dstSize.y == 0 || sourceSize.width == 0 || sourceSize.height == 0) { + return; + } + if (msaaSamples > 1) { + Log.fatal("Depth Peek from multisampled EFB targets is not supported"); + } + + const Params params = make_params(sourceSize, dstSize); + wgpu::Buffer storageBuffer; + wgpu::Buffer readbackBuffer; + wgpu::Buffer paramsBuffer; + uint64_t byteSize = 0; + { + std::lock_guard lock{g_mutex}; + auto* slot = find_available_slot(dstSize.x, dstSize.y); + if (slot == nullptr) { + return; + } + slot->state = SlotState::CopySubmitted; + storageBuffer = slot->storageBuffer; + readbackBuffer = slot->readbackBuffer; + paramsBuffer = slot->paramsBuffer; + byteSize = slot->byteSize; + } + + g_queue.WriteBuffer(paramsBuffer, 0, ¶ms, sizeof(params)); + + const std::array bindGroupEntries{ + wgpu::BindGroupEntry{ + .binding = 0, + .textureView = depthView, + }, + wgpu::BindGroupEntry{ + .binding = 1, + .buffer = storageBuffer, + .size = byteSize, + }, + wgpu::BindGroupEntry{ + .binding = 2, + .buffer = paramsBuffer, + .size = sizeof(Params), + }, + }; + const wgpu::BindGroupDescriptor bindGroupDescriptor{ + .label = "Depth Peek Bind Group", + .layout = g_bindGroupLayout, + .entryCount = bindGroupEntries.size(), + .entries = bindGroupEntries.data(), + }; + const auto bindGroup = g_device.CreateBindGroup(&bindGroupDescriptor); + + const wgpu::ComputePassDescriptor passDescriptor{ + .label = "Depth Peek Compute Pass", + }; + const auto pass = cmd.BeginComputePass(&passDescriptor); + pass.SetPipeline(g_pipeline); + pass.SetBindGroup(0, bindGroup); + pass.DispatchWorkgroups((dstSize.x + WorkgroupSizeX - 1) / WorkgroupSizeX, + (dstSize.y + WorkgroupSizeY - 1) / WorkgroupSizeY); + pass.End(); + + cmd.CopyBufferToBuffer(storageBuffer, 0, readbackBuffer, 0, byteSize); +} + +void after_submit() noexcept { + std::vector pendingMaps; + { + std::lock_guard lock{g_mutex}; + for (size_t i = 0; i < g_slots.size(); ++i) { + auto& slot = g_slots[i]; + if (slot.state != SlotState::CopySubmitted) { + continue; + } + slot.state = SlotState::MapPending; + pendingMaps.push_back({ + .slotIdx = i, + .readbackBuffer = slot.readbackBuffer, + .byteSize = slot.byteSize, + }); + } + } + + for (const auto& pending : pendingMaps) { + pending.readbackBuffer.MapAsync( + wgpu::MapMode::Read, 0, pending.byteSize, wgpu::CallbackMode::AllowSpontaneous, + [slotIdx = pending.slotIdx](wgpu::MapAsyncStatus status, wgpu::StringView message) { + complete_slot(slotIdx, status, message); + }); + } +} + +namespace testing { +void reset() noexcept { + std::lock_guard lock{g_mutex}; + g_snapshotRequested = false; + g_nextSlot = 0; + g_nextSnapshotTime = {}; + g_latest = {}; + for (auto& slot : g_slots) { + slot.state = SlotState::Available; + } +} + +bool snapshot_requested() noexcept { + std::lock_guard lock{g_mutex}; + return g_snapshotRequested; +} + +void set_latest(uint32_t width, uint32_t height, const std::vector& data) { + std::lock_guard lock{g_mutex}; + g_latest.width = width; + g_latest.height = height; + g_latest.data = data; +} +} // namespace testing + +} // namespace aurora::gfx::depth_peek diff --git a/aurora-main/lib/gfx/depth_peek.hpp b/aurora-main/lib/gfx/depth_peek.hpp new file mode 100644 index 0000000..0f6fec9 --- /dev/null +++ b/aurora-main/lib/gfx/depth_peek.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "common.hpp" + +#include + +namespace aurora::gfx::depth_peek { + +void initialize(); +void shutdown(); + +void request_snapshot() noexcept; +bool read_latest(uint16_t x, uint16_t y, uint32_t& z) noexcept; +void poll() noexcept; + +void encode_frame_snapshot(const wgpu::CommandEncoder& cmd, const wgpu::TextureView& depthView, + wgpu::Extent3D sourceSize, uint32_t msaaSamples) noexcept; +void after_submit() noexcept; + +namespace testing { +void reset() noexcept; +bool snapshot_requested() noexcept; +void set_latest(uint32_t width, uint32_t height, const std::vector& data); +} // namespace testing + +} // namespace aurora::gfx::depth_peek diff --git a/aurora-main/lib/gfx/efb_ram_copy.cpp b/aurora-main/lib/gfx/efb_ram_copy.cpp new file mode 100644 index 0000000..34d507b --- /dev/null +++ b/aurora-main/lib/gfx/efb_ram_copy.cpp @@ -0,0 +1,451 @@ +#include "efb_ram_copy.hpp" + +#include "efb_ram_encoder.hpp" +#include "tex_copy_conv.hpp" +#include "../internal.hpp" +#include "../webgpu/gpu.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +namespace aurora::gfx::efb_ram { +namespace { + +Module Log("aurora::gfx::efb_ram"); + +using webgpu::g_device; +using webgpu::g_instance; + +// A CPU-consumed EFB copy this small is the lens-flare occlusion probe (4x4 Z24X8, 64 bytes). +// Resolving it synchronously stalls the producer and kills interpolation, so read it back async. +constexpr size_t kAsyncReadbackMaxBytes = 256; +// Each destination keeps its readback buffer forever. Only a handful are expected, and the cap +// stops an unexpected pattern of one-shot destinations from leaking GPU buffers. +constexpr size_t kMaxAsyncSlots = 32; + +struct PendingCopy { + void* dest = nullptr; + uint32_t width = 0; + uint32_t height = 0; + GXTexFmt format = GX_TF_RGBA8; + TextureHandle texture; + TextureHandle nativeTexture; + Range nativeBlitUniform; +}; + +struct Download { + PendingCopy copy; + wgpu::Buffer buffer; + uint32_t bytesPerRow = 0; + uint64_t bufferSize = 0; +}; + +enum class AsyncState : uint8_t { + Idle, + CopySubmitted, + MapPending, +}; + +// One readback per destination address, reused for that destination's lifetime: the probe hits +// the same buffer every frame, so pooling avoids a per-frame allocation. +struct AsyncSlot { + wgpu::Buffer buffer; + TextureHandle nativeTexture; + uint64_t bufferSize = 0; + uint32_t bytesPerRow = 0; + // Latched at encode time and read by the map callback. + void* dest = nullptr; + GXTexFmt format = GX_TF_RGBA8; + uint32_t width = 0; + uint32_t height = 0; + uint32_t hostWidth = 0; + uint32_t hostHeight = 0; + HostPixelOrder order = HostPixelOrder::RGBA; + AsyncState state = AsyncState::Idle; +}; + +std::vector g_pending; +std::vector g_downloads; + +// Recorded by the producer, moved to the worker-private sealed list while the +// producer is excluded. +std::vector g_asyncPending; +std::vector g_asyncSealed; + +std::mutex g_asyncMutex; +std::unordered_map g_asyncSlots; +uint32_t g_asyncMapsInFlight = 0; + +uint32_t align_to(uint32_t value, uint32_t alignment) noexcept { return (value + alignment - 1) & ~(alignment - 1); } + +// An internally scaled copy cannot be read back directly, so blit it down to native-resolution +// RGBA8 first. Pushes a uniform, so it must run while the staging buffers are still mapped. +void ensure_native_texture(PendingCopy& pending, TextureHandle* cache = nullptr) noexcept { + if (pending.texture->size.width == pending.width && pending.texture->size.height == pending.height) { + return; + } + if (pending.nativeTexture) { + return; + } + if (cache != nullptr && *cache && (*cache)->size.width == pending.width && + (*cache)->size.height == pending.height) { + pending.nativeTexture = *cache; + } else { + pending.nativeTexture = new_conv_texture(pending.width, pending.height, GX_TF_RGBA8, "GX EFB RAM native readback"); + if (cache != nullptr) { + *cache = pending.nativeTexture; + } + } + const std::array nativeBlitUniform{ + 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 64.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, + }; + pending.nativeBlitUniform = push_uniform(nativeBlitUniform); +} + +void encode_native_blit(const wgpu::CommandEncoder& encoder, const PendingCopy& pending) noexcept { + const tex_copy_conv::ConvRequest blitRequest{ + .fmt = GX_TF_RGBA8, + .srcView = pending.texture->sampleTextureView, + .uniformRange = pending.nativeBlitUniform, + .dst = pending.nativeTexture, + .sampleFilter = pending.format == GX_TF_Z16 || pending.format == GX_TF_Z24X8 + ? tex_copy_conv::SampleFilter::Nearest + : tex_copy_conv::SampleFilter::Linear, + }; + tex_copy_conv::blit(encoder, blitRequest); +} + +HostPixelOrder texture_pixel_order(const TextureHandle& texture) noexcept { + return texture->format == wgpu::TextureFormat::BGRA8Unorm ? HostPixelOrder::BGRA : HostPixelOrder::RGBA; +} + +void complete_async_slot(void* dest, wgpu::MapAsyncStatus status, wgpu::StringView message) noexcept { + std::lock_guard lock{g_asyncMutex}; + if (g_asyncMapsInFlight > 0) { + --g_asyncMapsInFlight; + } + const auto it = g_asyncSlots.find(dest); + if (it == g_asyncSlots.end()) { + return; + } + auto& slot = it->second; + if (status == wgpu::MapAsyncStatus::Success) { + const auto* pixels = static_cast(slot.buffer.GetConstMappedRange(0, slot.bufferSize)); + if (pixels != nullptr) { + // Writes guest RAM from the event-queue thread while the guest may be reading it. The only + // consumer min/maxes depth for a fade factor, so a torn tile just mixes two frames' depths. + const size_t outputSize = encoded_size(slot.format, slot.width, slot.height); + if (!encode(slot.dest, outputSize, slot.format, slot.width, slot.height, pixels, slot.hostWidth, slot.hostHeight, + slot.bytesPerRow, slot.order)) { + Log.error("Failed to encode async EFB RAM copy format=0x{:x} size={}x{}", static_cast(slot.format), + slot.width, slot.height); + } + // Guest RAM written from outside the embedder, so nothing bumps its write generation and a + // texture cached over this range would keep its digest. + notify_guest_write(slot.dest, outputSize); + } + slot.buffer.Unmap(); + } else if (status != wgpu::MapAsyncStatus::CallbackCancelled && status != wgpu::MapAsyncStatus::Aborted) { + Log.warn("Async EFB RAM readback mapping failed {}: {}", magic_enum::enum_name(status), message); + } + slot.state = AsyncState::Idle; +} + +// Runs the completion callbacks for maps whose GPU work has finished. Must not +// be called while g_asyncMutex is held: complete_async_slot takes it. +void drain_async_events() noexcept { + { + std::lock_guard lock{g_asyncMutex}; + if (g_asyncMapsInFlight == 0) { + return; + } + } + if (g_instance) { + g_instance.ProcessEvents(); + } +} + +} // namespace + +void schedule(void* dest, uint32_t width, uint32_t height, GXTexFmt format, TextureHandle texture) noexcept { + if (dest == nullptr || width == 0 || height == 0 || !texture) { + return; + } + if (!supports_format(format)) { + Log.fatal("Unsupported CPU-visible EFB copy format 0x{:x}", static_cast(format)); + } + + PendingCopy request{ + .dest = dest, + .width = width, + .height = height, + .format = format, + .texture = std::move(texture), + }; + + const size_t encodedSize = encoded_size(format, width, height); + // Offscreen copies resolve on a separate pass list that the sealed frame's + // native render does not replay, so they stay on the lazy path. + bool async = !is_offscreen() && encodedSize != 0 && encodedSize <= kAsyncReadbackMaxBytes; + if (async) { + std::lock_guard lock{g_asyncMutex}; + const auto it = g_asyncSlots.find(dest); + if (it == g_asyncSlots.end()) { + if (g_asyncSlots.size() >= kMaxAsyncSlots) { + async = false; + } else { + g_asyncSlots.try_emplace(dest); + // No readback has landed here yet. 0xff decodes to far Z, which the probe reads as unobstructed + // so a new flare fades in; zero-filled RAM would decode as fully occluded. + std::memset(dest, 0xff, encodedSize); + notify_guest_write(dest, encodedSize); + } + } + } + + auto& list = async ? g_asyncPending : g_pending; + if (auto it = std::find_if(list.begin(), list.end(), + [dest](const PendingCopy& pending) { return pending.dest == dest; }); + it != list.end()) { + *it = std::move(request); + } else { + list.push_back(std::move(request)); + } +} + +bool has_pending(void* dest) noexcept { + if (dest == nullptr) return !g_pending.empty() || !g_downloads.empty(); + return std::any_of(g_pending.begin(), g_pending.end(), + [dest](const PendingCopy& pending) { return pending.dest == dest; }) || + std::any_of(g_downloads.begin(), g_downloads.end(), + [dest](const Download& download) { return download.copy.dest == dest; }); +} + +bool prepare_downloads(void* dest) noexcept { + bool found = false; + for (auto& pending : g_pending) { + if (dest != nullptr && pending.dest != dest) continue; + found = true; + ensure_native_texture(pending); + } + return found; +} + +void encode_downloads(const wgpu::CommandEncoder& encoder, void* dest) noexcept { + if (!g_downloads.empty()) { + Log.fatal("Attempted to encode new EFB RAM downloads before completing the previous batch"); + } + + g_downloads.reserve(g_pending.size()); + for (auto it = g_pending.begin(); it != g_pending.end();) { + if (dest != nullptr && it->dest != dest) { + ++it; + continue; + } + PendingCopy pending = std::move(*it); + it = g_pending.erase(it); + if (pending.nativeTexture) { + encode_native_blit(encoder, pending); + } + const auto& texture = pending.nativeTexture ? pending.nativeTexture : pending.texture; + const uint32_t bytesPerRow = align_to(texture->size.width * 4, 256); + const uint64_t bufferSize = static_cast(bytesPerRow) * texture->size.height; + const wgpu::BufferDescriptor descriptor{ + .label = "GX EFB RAM copy readback", + .usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead, + .size = bufferSize, + }; + auto buffer = g_device.CreateBuffer(&descriptor); + const wgpu::TexelCopyTextureInfo source{ + .texture = texture->texture, + .mipLevel = 0, + .origin = {0, 0, 0}, + .aspect = wgpu::TextureAspect::All, + }; + const wgpu::TexelCopyBufferInfo destination{ + .layout = + wgpu::TexelCopyBufferLayout{ + .offset = 0, + .bytesPerRow = bytesPerRow, + .rowsPerImage = texture->size.height, + }, + .buffer = buffer, + }; + encoder.CopyTextureToBuffer(&source, &destination, &texture->size); + g_downloads.push_back({ + .copy = std::move(pending), + .buffer = std::move(buffer), + .bytesPerRow = bytesPerRow, + .bufferSize = bufferSize, + }); + } +} + +bool complete_downloads() noexcept { + bool success = true; + for (auto& download : g_downloads) { + wgpu::MapAsyncStatus mapStatus = wgpu::MapAsyncStatus::CallbackCancelled; + wgpu::StringView mapMessage{}; + const auto future = + download.buffer.MapAsync(wgpu::MapMode::Read, 0, download.bufferSize, wgpu::CallbackMode::WaitAnyOnly, + [&mapStatus, &mapMessage](wgpu::MapAsyncStatus status, wgpu::StringView message) { + mapStatus = status; + mapMessage = message; + }); + const auto waitStatus = g_instance.WaitAny(future, 5000000000); + if (waitStatus != wgpu::WaitStatus::Success || mapStatus != wgpu::MapAsyncStatus::Success) { + Log.error("EFB RAM readback failed wait={} map={} message={}", magic_enum::enum_name(waitStatus), + magic_enum::enum_name(mapStatus), mapMessage); + success = false; + continue; + } + + const auto* pixels = static_cast(download.buffer.GetConstMappedRange(0, download.bufferSize)); + const auto& readbackTexture = download.copy.nativeTexture ? download.copy.nativeTexture : download.copy.texture; + const auto order = texture_pixel_order(readbackTexture); + const size_t outputSize = encoded_size(download.copy.format, download.copy.width, download.copy.height); + if (!encode(download.copy.dest, outputSize, download.copy.format, download.copy.width, download.copy.height, pixels, + readbackTexture->size.width, readbackTexture->size.height, download.bytesPerRow, order)) { + Log.error("Failed to encode EFB RAM copy format=0x{:x} size={}x{}", static_cast(download.copy.format), + download.copy.width, download.copy.height); + success = false; + } + notify_guest_write(download.copy.dest, outputSize); + download.buffer.Unmap(); + } + g_downloads.clear(); + return success; +} + +void cancel() noexcept { + g_pending.clear(); + g_downloads.clear(); + g_asyncPending.clear(); +} + +void seal_async_downloads() noexcept { + { + std::lock_guard lock{g_asyncMutex}; + for (auto& pending : g_asyncPending) { + const auto it = g_asyncSlots.find(pending.dest); + ensure_native_texture(pending, it != g_asyncSlots.end() ? &it->second.nativeTexture : nullptr); + } + } + g_asyncSealed = std::move(g_asyncPending); + g_asyncPending.clear(); +} + +void encode_async_downloads(const wgpu::CommandEncoder& encoder) noexcept { + if (g_asyncSealed.empty()) { + return; + } + // Retire the previous frame's map first so its slot is free here, otherwise a single-slot + // destination could only be sampled every other frame. + drain_async_events(); + std::lock_guard lock{g_asyncMutex}; + for (auto& pending : g_asyncSealed) { + const auto it = g_asyncSlots.find(pending.dest); + if (it == g_asyncSlots.end()) { + continue; + } + auto& slot = it->second; + // Only one readback per destination may be outstanding and the previous map still owns the pooled + // buffer. Dropping this request just leaves the guest on a slightly older probe. + if (slot.state != AsyncState::Idle) { + continue; + } + if (pending.nativeTexture) { + encode_native_blit(encoder, pending); + } + const auto& texture = pending.nativeTexture ? pending.nativeTexture : pending.texture; + const uint32_t bytesPerRow = align_to(texture->size.width * 4, 256); + const uint64_t bufferSize = static_cast(bytesPerRow) * texture->size.height; + if (!slot.buffer || slot.bufferSize != bufferSize) { + const wgpu::BufferDescriptor descriptor{ + .label = "GX EFB RAM async readback", + .usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead, + .size = bufferSize, + }; + slot.buffer = g_device.CreateBuffer(&descriptor); + slot.bufferSize = bufferSize; + } + if (!slot.buffer) { + continue; + } + const wgpu::TexelCopyTextureInfo source{ + .texture = texture->texture, + .mipLevel = 0, + .origin = {0, 0, 0}, + .aspect = wgpu::TextureAspect::All, + }; + const wgpu::TexelCopyBufferInfo destination{ + .layout = + wgpu::TexelCopyBufferLayout{ + .offset = 0, + .bytesPerRow = bytesPerRow, + .rowsPerImage = texture->size.height, + }, + .buffer = slot.buffer, + }; + encoder.CopyTextureToBuffer(&source, &destination, &texture->size); + slot.bytesPerRow = bytesPerRow; + slot.dest = pending.dest; + slot.format = pending.format; + slot.width = pending.width; + slot.height = pending.height; + slot.hostWidth = texture->size.width; + slot.hostHeight = texture->size.height; + slot.order = texture_pixel_order(texture); + slot.state = AsyncState::CopySubmitted; + } + g_asyncSealed.clear(); +} + +void after_submit() noexcept { + struct PendingMap { + void* dest; + wgpu::Buffer buffer; + uint64_t bufferSize; + }; + std::vector pendingMaps; + { + std::lock_guard lock{g_asyncMutex}; + for (auto& [dest, slot] : g_asyncSlots) { + if (slot.state != AsyncState::CopySubmitted) { + continue; + } + slot.state = AsyncState::MapPending; + ++g_asyncMapsInFlight; + pendingMaps.push_back({dest, slot.buffer, slot.bufferSize}); + } + } + + for (const auto& pending : pendingMaps) { + pending.buffer.MapAsync(wgpu::MapMode::Read, 0, pending.bufferSize, wgpu::CallbackMode::AllowSpontaneous, + [dest = pending.dest](wgpu::MapAsyncStatus status, wgpu::StringView message) { + complete_async_slot(dest, status, message); + }); + } + + // Nothing else pumps the WebGPU event queue on this thread, so a completed map would sit + // unharvested with its slot busy. ProcessEvents does not block. + drain_async_events(); +} + +void abort_async() noexcept { g_asyncSealed.clear(); } + +void shutdown() noexcept { + cancel(); + g_asyncSealed.clear(); + std::lock_guard lock{g_asyncMutex}; + g_asyncSlots.clear(); + g_asyncMapsInFlight = 0; +} + +} // namespace aurora::gfx::efb_ram diff --git a/aurora-main/lib/gfx/efb_ram_copy.hpp b/aurora-main/lib/gfx/efb_ram_copy.hpp new file mode 100644 index 0000000..6a20279 --- /dev/null +++ b/aurora-main/lib/gfx/efb_ram_copy.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "common.hpp" +#include "texture.hpp" + +#include + +namespace aurora::gfx::efb_ram { + +void schedule(void* dest, uint32_t width, uint32_t height, GXTexFmt format, TextureHandle texture) noexcept; +bool has_pending(void* dest = nullptr) noexcept; +bool prepare_downloads(void* dest = nullptr) noexcept; +void encode_downloads(const wgpu::CommandEncoder& encoder, void* dest = nullptr) noexcept; +bool complete_downloads() noexcept; +void cancel() noexcept; + +// Frame-latent readbacks for probe-sized CPU-consumed copies: they ride the frame's own encode and +// land in guest RAM a frame later. Per frame, from the worker: seal, encode, then after_submit. +void seal_async_downloads() noexcept; +void encode_async_downloads(const wgpu::CommandEncoder& encoder) noexcept; +void after_submit() noexcept; +// Drops requests sealed for a frame that will never be encoded. +void abort_async() noexcept; +void shutdown() noexcept; + +} // namespace aurora::gfx::efb_ram diff --git a/aurora-main/lib/gfx/efb_ram_encoder.cpp b/aurora-main/lib/gfx/efb_ram_encoder.cpp new file mode 100644 index 0000000..f57486c --- /dev/null +++ b/aurora-main/lib/gfx/efb_ram_encoder.cpp @@ -0,0 +1,211 @@ +#include "efb_ram_encoder.hpp" + +#include +#include +#include + +namespace aurora::gfx::efb_ram { +namespace { + +struct Pixel { + uint8_t r; + uint8_t g; + uint8_t b; + uint8_t a; +}; + +struct BlockInfo { + uint32_t width; + uint32_t height; + uint32_t bytes; +}; + +enum class Layout { + I4, + I8, + IA4, + IA8, + RGB565, + RGB5A3, + RGBA8, +}; + +bool describe(GXTexFmt format, Layout& layout, BlockInfo& block) noexcept { + switch (format) { + case GX_TF_I4: + case GX_CTF_R4: + layout = Layout::I4; + block = {8, 8, 32}; + return true; + case GX_TF_I8: + case GX_CTF_A8: + case GX_CTF_R8: + case GX_CTF_G8: + case GX_CTF_B8: + layout = Layout::I8; + block = {8, 4, 32}; + return true; + case GX_TF_IA4: + case GX_CTF_RA4: + layout = Layout::IA4; + block = {8, 4, 32}; + return true; + case GX_TF_IA8: + case GX_TF_Z16: + case GX_CTF_RA8: + case GX_CTF_RG8: + case GX_CTF_GB8: + layout = Layout::IA8; + block = {4, 4, 32}; + return true; + case GX_TF_RGB565: + layout = Layout::RGB565; + block = {4, 4, 32}; + return true; + case GX_TF_RGB5A3: + layout = Layout::RGB5A3; + block = {4, 4, 32}; + return true; + case GX_TF_RGBA8: + case GX_TF_Z24X8: + layout = Layout::RGBA8; + block = {4, 4, 64}; + return true; + default: + return false; + } +} + +Pixel read_pixel(const uint8_t* pixels, uint32_t hostWidth, uint32_t hostHeight, uint32_t bytesPerRow, + HostPixelOrder order, uint32_t guestX, uint32_t guestY, uint32_t guestWidth, + uint32_t guestHeight) noexcept { + const uint32_t clampedGuestX = std::min(guestX, guestWidth - 1); + const uint32_t clampedGuestY = std::min(guestY, guestHeight - 1); + const uint64_t sampleXNumerator = (static_cast(clampedGuestX) * 2 + 1) * hostWidth; + const uint64_t sampleYNumerator = (static_cast(clampedGuestY) * 2 + 1) * hostHeight; + const uint32_t hostX = + std::min(static_cast(sampleXNumerator / (guestWidth * 2ull)), hostWidth - 1); + const uint32_t hostY = + std::min(static_cast(sampleYNumerator / (guestHeight * 2ull)), hostHeight - 1); + const auto* src = pixels + static_cast(hostY) * bytesPerRow + static_cast(hostX) * 4; + if (order == HostPixelOrder::BGRA) { + return {src[2], src[1], src[0], src[3]}; + } + return {src[0], src[1], src[2], src[3]}; +} + +void write_be16(uint8_t*& dst, uint16_t value) noexcept { + *dst++ = static_cast(value >> 8); + *dst++ = static_cast(value); +} + +} // namespace + +bool supports_format(GXTexFmt format) noexcept { + Layout layout{}; + BlockInfo block{}; + return describe(format, layout, block); +} + +size_t encoded_size(GXTexFmt format, uint32_t width, uint32_t height) noexcept { + Layout layout{}; + BlockInfo block{}; + if (width == 0 || height == 0 || !describe(format, layout, block)) { + return 0; + } + const uint64_t blocksX = (static_cast(width) + block.width - 1) / block.width; + const uint64_t blocksY = (static_cast(height) + block.height - 1) / block.height; + const uint64_t size = blocksX * blocksY * block.bytes; + return size <= SIZE_MAX ? static_cast(size) : 0; +} + +bool encode(void* dstValue, size_t dstSize, GXTexFmt format, uint32_t guestWidth, uint32_t guestHeight, + const uint8_t* hostPixels, uint32_t hostWidth, uint32_t hostHeight, uint32_t hostBytesPerRow, + HostPixelOrder order) noexcept { + Layout layout{}; + BlockInfo block{}; + const size_t required = encoded_size(format, guestWidth, guestHeight); + if (dstValue == nullptr || hostPixels == nullptr || required == 0 || dstSize < required || hostWidth == 0 || + hostHeight == 0 || hostBytesPerRow < hostWidth * 4 || !describe(format, layout, block)) { + return false; + } + + auto* dst = static_cast(dstValue); + std::memset(dst, 0, required); + const uint32_t blocksX = (guestWidth + block.width - 1) / block.width; + const uint32_t blocksY = (guestHeight + block.height - 1) / block.height; + + for (uint32_t blockY = 0; blockY < blocksY; ++blockY) { + for (uint32_t blockX = 0; blockX < blocksX; ++blockX) { + uint8_t* blockDst = dst + (static_cast(blockY) * blocksX + blockX) * block.bytes; + if (layout == Layout::RGBA8) { + uint8_t* ar = blockDst; + uint8_t* gb = blockDst + 32; + for (uint32_t y = 0; y < 4; ++y) { + for (uint32_t x = 0; x < 4; ++x) { + const Pixel pixel = read_pixel(hostPixels, hostWidth, hostHeight, hostBytesPerRow, order, blockX * 4 + x, + blockY * 4 + y, guestWidth, guestHeight); + *ar++ = pixel.a; + *ar++ = pixel.r; + *gb++ = pixel.g; + *gb++ = pixel.b; + } + } + continue; + } + + uint8_t* out = blockDst; + for (uint32_t y = 0; y < block.height; ++y) { + if (layout == Layout::I4) { + for (uint32_t x = 0; x < block.width; x += 2) { + const Pixel first = + read_pixel(hostPixels, hostWidth, hostHeight, hostBytesPerRow, order, blockX * block.width + x, + blockY * block.height + y, guestWidth, guestHeight); + const Pixel second = + read_pixel(hostPixels, hostWidth, hostHeight, hostBytesPerRow, order, blockX * block.width + x + 1, + blockY * block.height + y, guestWidth, guestHeight); + *out++ = static_cast((first.r & 0xf0) | (second.r >> 4)); + } + continue; + } + + for (uint32_t x = 0; x < block.width; ++x) { + const Pixel pixel = read_pixel(hostPixels, hostWidth, hostHeight, hostBytesPerRow, order, + blockX * block.width + x, blockY * block.height + y, guestWidth, guestHeight); + switch (layout) { + case Layout::I8: + *out++ = pixel.r; + break; + case Layout::IA4: + *out++ = static_cast((pixel.a & 0xf0) | (pixel.r >> 4)); + break; + case Layout::IA8: + *out++ = pixel.a; + *out++ = pixel.r; + break; + case Layout::RGB565: { + const uint16_t packed = + static_cast(((pixel.r >> 3) << 11) | ((pixel.g >> 2) << 5) | (pixel.b >> 3)); + write_be16(out, packed); + break; + } + case Layout::RGB5A3: { + const uint16_t packed = + pixel.a >= 224 + ? static_cast(0x8000 | ((pixel.r >> 3) << 10) | ((pixel.g >> 3) << 5) | (pixel.b >> 3)) + : static_cast(((pixel.a >> 5) << 12) | ((pixel.r >> 4) << 8) | ((pixel.g >> 4) << 4) | + (pixel.b >> 4)); + write_be16(out, packed); + break; + } + default: + return false; + } + } + } + } + } + return true; +} + +} // namespace aurora::gfx::efb_ram diff --git a/aurora-main/lib/gfx/efb_ram_encoder.hpp b/aurora-main/lib/gfx/efb_ram_encoder.hpp new file mode 100644 index 0000000..85982b4 --- /dev/null +++ b/aurora-main/lib/gfx/efb_ram_encoder.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include +#include + +namespace aurora::gfx::efb_ram { + +enum class HostPixelOrder : uint8_t { + RGBA, + BGRA, +}; + +bool supports_format(GXTexFmt format) noexcept; +size_t encoded_size(GXTexFmt format, uint32_t width, uint32_t height) noexcept; + +// Encodes a decoded host texture back into the tiled layout a GX EFB copy writes. Host dimensions +// may be internally scaled; guest dimensions are always the native copy dimensions. +bool encode(void* dst, size_t dstSize, GXTexFmt format, uint32_t guestWidth, uint32_t guestHeight, + const uint8_t* hostPixels, uint32_t hostWidth, uint32_t hostHeight, uint32_t hostBytesPerRow, + HostPixelOrder order) noexcept; + +} // namespace aurora::gfx::efb_ram diff --git a/aurora-main/lib/gfx/pipeline_cache.cpp b/aurora-main/lib/gfx/pipeline_cache.cpp new file mode 100644 index 0000000..83358ff --- /dev/null +++ b/aurora-main/lib/gfx/pipeline_cache.cpp @@ -0,0 +1,1300 @@ +#include "pipeline_cache.hpp" + +#include "clear.hpp" +#include "../gx/pipeline.hpp" +#include "../sqlite_utils.hpp" +#include "../webgpu/gpu.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace aurora::gfx { +static Module Log("aurora::gfx::pipeline_cache"); + +constexpr int PipelineCacheSchema = 1; +constexpr const char* InitialPipelineCacheName = "initial_pipeline_cache.db"; +constexpr const char* SdlVfsName = "aurora_pipeline_cache_sdl_vfs"; + +struct CachedPipeline { + wgpu::RenderPipeline pipeline; + uint32_t firstFrameUsed = UINT32_MAX; +}; + +struct PendingPipeline { + PipelineRef hash; + uint32_t firstFrameUsed = UINT32_MAX; + NewPipelineCallback create; +}; + +struct PipelineCacheWrite { + ShaderType type; + PipelineRef hash; + uint32_t configVersion; + ByteBuffer config; + uint32_t firstFrameUsed = UINT32_MAX; +}; + +struct SdlVfsSqliteFile { + sqlite3_file base; + SDL_IOStream* io = nullptr; +}; + +static std::mutex g_pipelineMutex; +static bool g_hasPipelineThread = false; +static bool g_pipelineFrameActive = false; +static std::atomic_bool g_skipUnreadyGxPipelines = false; +static size_t g_pipelinesPerFrame = 0; +// Keep first-use compilation bounded. The render command stream remains ordered; +// it waits for a queued pipeline only when the corresponding draw is consumed. +constexpr size_t MaxQueuedPipelineBuilds = 256; +// First-use compilation works best as a short parallel burst. Leave two logical processors for the +// render and game threads, and cap large hosts to limit driver submissions and memory use. +constexpr size_t ReservedLogicalProcessors = 2; +constexpr size_t MaxPipelineWorkers = 22; +// Cached clear and GX pipelines are prewarmed using the full worker pool. +constexpr size_t MaxBackgroundPipelineWorkers = MaxPipelineWorkers; +// For synchronous pipeline fallback (OpenGL) +#ifdef NDEBUG +constexpr size_t BuildPipelinesPerFrame = 5; +#else +constexpr size_t BuildPipelinesPerFrame = 1; +#endif +static std::vector g_pipelineThreads; +static bool g_pipelineThreadEnd = false; +static size_t g_activeBackgroundPipelineWorkers = 0; +static std::condition_variable g_pipelineCv; +static absl::flat_hash_map g_pipelines; +static std::deque g_priorityPipelines; +static std::deque g_backgroundPipelines; +static absl::flat_hash_set g_pendingPipelines; + +static sqlite3* g_pipelineCacheDb = nullptr; +static sqlite3_stmt* g_pipelineCacheLoadStmt = nullptr; +static sqlite3_stmt* g_pipelineCacheUpsertStmt = nullptr; +static bool g_pipelineCacheBroken = false; +static std::thread g_pipelineCacheWriterThread; +static std::condition_variable g_pipelineCacheWriterCv; +static std::mutex g_pipelineCacheWriterMutex; +static std::deque g_pipelineCacheWriteQueue; +static absl::flat_hash_set g_pipelineCachePendingWrites; +static bool g_pipelineCacheWriterStop = false; +static int g_sdlVfsRegisterResult = SQLITE_ERROR; + +static SdlVfsSqliteFile* sdl_vfs_file(sqlite3_file* file) { + return reinterpret_cast(file); +} + +static sqlite3_vfs* default_vfs(sqlite3_vfs* vfs) { + return static_cast(vfs->pAppData); +} + +static int sdl_vfs_close(sqlite3_file* file) { + auto* vfsFile = sdl_vfs_file(file); + if (vfsFile->io != nullptr) { + SDL_CloseIO(vfsFile->io); + vfsFile->io = nullptr; + } + return SQLITE_OK; +} + +static int sdl_vfs_read(sqlite3_file* file, void* buffer, int amount, sqlite3_int64 offset) { + auto* vfsFile = sdl_vfs_file(file); + if (vfsFile->io == nullptr || offset < 0 || amount < 0) { + return SQLITE_IOERR_READ; + } + if (SDL_SeekIO(vfsFile->io, offset, SDL_IO_SEEK_SET) < 0) { + return SQLITE_IOERR_SEEK; + } + + auto* dst = static_cast(buffer); + int total = 0; + while (total < amount) { + const size_t read = SDL_ReadIO(vfsFile->io, dst + total, static_cast(amount - total)); + if (read == 0) { + if (SDL_GetIOStatus(vfsFile->io) == SDL_IO_STATUS_EOF) { + std::memset(dst + total, 0, static_cast(amount - total)); + return SQLITE_IOERR_SHORT_READ; + } + return SQLITE_IOERR_READ; + } + if (read > static_cast(std::numeric_limits::max() - total)) { + return SQLITE_IOERR_READ; + } + total += static_cast(read); + } + return SQLITE_OK; +} + +static int sdl_vfs_write(sqlite3_file*, const void*, int, sqlite3_int64) { return SQLITE_READONLY; } + +static int sdl_vfs_truncate(sqlite3_file*, sqlite3_int64) { return SQLITE_READONLY; } + +static int sdl_vfs_sync(sqlite3_file*, int) { return SQLITE_OK; } + +static int sdl_vfs_file_size(sqlite3_file* file, sqlite3_int64* size) { + auto* vfsFile = sdl_vfs_file(file); + if (vfsFile->io == nullptr || size == nullptr) { + return SQLITE_IOERR_FSTAT; + } + + const auto ioSize = SDL_GetIOSize(vfsFile->io); + if (ioSize < 0) { + return SQLITE_IOERR_FSTAT; + } + *size = static_cast(ioSize); + return SQLITE_OK; +} + +static int sdl_vfs_lock(sqlite3_file*, int) { return SQLITE_OK; } + +static int sdl_vfs_unlock(sqlite3_file*, int) { return SQLITE_OK; } + +static int sdl_vfs_check_reserved_lock(sqlite3_file*, int* reserved) { + if (reserved != nullptr) { + *reserved = 0; + } + return SQLITE_OK; +} + +static int sdl_vfs_file_control(sqlite3_file*, int op, void* arg) { + switch (op) { + case SQLITE_FCNTL_LOCKSTATE: + *static_cast(arg) = SQLITE_LOCK_NONE; + return SQLITE_OK; + case SQLITE_FCNTL_HAS_MOVED: + *static_cast(arg) = 0; + return SQLITE_OK; + case SQLITE_FCNTL_SIZE_HINT: + return SQLITE_OK; + default: + return SQLITE_NOTFOUND; + } +} + +static int sdl_vfs_sector_size(sqlite3_file*) { return 4096; } + +static int sdl_vfs_device_characteristics(sqlite3_file*) { + return SQLITE_IOCAP_IMMUTABLE | SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN; +} + +static constexpr sqlite3_io_methods SdlVfsIoMethods{ + .iVersion = 1, + .xClose = sdl_vfs_close, + .xRead = sdl_vfs_read, + .xWrite = sdl_vfs_write, + .xTruncate = sdl_vfs_truncate, + .xSync = sdl_vfs_sync, + .xFileSize = sdl_vfs_file_size, + .xLock = sdl_vfs_lock, + .xUnlock = sdl_vfs_unlock, + .xCheckReservedLock = sdl_vfs_check_reserved_lock, + .xFileControl = sdl_vfs_file_control, + .xSectorSize = sdl_vfs_sector_size, + .xDeviceCharacteristics = sdl_vfs_device_characteristics, +}; + +static int sdl_vfs_open(sqlite3_vfs*, sqlite3_filename name, sqlite3_file* file, int flags, int* outFlags) { + auto* vfsFile = sdl_vfs_file(file); + vfsFile->base.pMethods = nullptr; + vfsFile->io = nullptr; + + if (name == nullptr || (flags & SQLITE_OPEN_READWRITE) != 0 || (flags & SQLITE_OPEN_READONLY) == 0) { + return SQLITE_CANTOPEN; + } + + vfsFile->io = SDL_IOFromFile(name, "rb"); + if (vfsFile->io == nullptr) { + return SQLITE_CANTOPEN; + } + + vfsFile->base.pMethods = &SdlVfsIoMethods; + if (outFlags != nullptr) { + *outFlags = SQLITE_OPEN_READONLY; + } + return SQLITE_OK; +} + +static int sdl_vfs_delete(sqlite3_vfs*, const char*, int) { return SQLITE_READONLY; } + +static int sdl_vfs_access(sqlite3_vfs*, const char* name, int flags, int* result) { + if (result == nullptr) { + return SQLITE_IOERR_ACCESS; + } + if (name == nullptr || flags == SQLITE_ACCESS_READWRITE) { + *result = 0; + return SQLITE_OK; + } + + auto* io = SDL_IOFromFile(name, "rb"); + *result = io != nullptr ? 1 : 0; + if (io != nullptr) { + SDL_CloseIO(io); + } + return SQLITE_OK; +} + +static int sdl_vfs_full_pathname(sqlite3_vfs*, const char* name, int outSize, char* out) { + if (name == nullptr || out == nullptr || outSize <= 0) { + return SQLITE_CANTOPEN; + } + sqlite3_snprintf(outSize, out, "%s", name); + return SQLITE_OK; +} + +static void* sdl_vfs_dl_open(sqlite3_vfs*, const char*) { return nullptr; } + +static void sdl_vfs_dl_error(sqlite3_vfs*, int bytes, char* message) { + if (message != nullptr && bytes > 0) { + sqlite3_snprintf(bytes, message, "%s", "Dynamic loading is unsupported"); + } +} + +static void (*sdl_vfs_dl_sym(sqlite3_vfs*, void*, const char*))(void) { return nullptr; } + +static void sdl_vfs_dl_close(sqlite3_vfs*, void*) {} + +static int sdl_vfs_randomness(sqlite3_vfs* vfs, int bytes, char* out) { + if (auto* base = default_vfs(vfs); base != nullptr && base->xRandomness != nullptr) { + return base->xRandomness(base, bytes, out); + } + if (out != nullptr && bytes > 0) { + std::memset(out, 0, static_cast(bytes)); + } + return bytes; +} + +static int sdl_vfs_sleep(sqlite3_vfs* vfs, int microseconds) { + if (auto* base = default_vfs(vfs); base != nullptr && base->xSleep != nullptr) { + return base->xSleep(base, microseconds); + } + return microseconds; +} + +static int sdl_vfs_current_time(sqlite3_vfs* vfs, double* time) { + if (auto* base = default_vfs(vfs); base != nullptr && base->xCurrentTime != nullptr) { + return base->xCurrentTime(base, time); + } + if (time != nullptr) { + *time = 2440587.5; + } + return SQLITE_OK; +} + +static int sdl_vfs_get_last_error(sqlite3_vfs*, int, char*) { return SQLITE_OK; } + +static bool register_sdl_vfs() { + static std::once_flag registerOnce; + std::call_once(registerOnce, [] { + auto* baseVfs = sqlite3_vfs_find(nullptr); + static sqlite3_vfs sdlVfs{ + .iVersion = 1, + .szOsFile = static_cast(sizeof(SdlVfsSqliteFile)), + .mxPathname = 4096, + .pNext = nullptr, + .zName = SdlVfsName, + .pAppData = baseVfs, + .xOpen = sdl_vfs_open, + .xDelete = sdl_vfs_delete, + .xAccess = sdl_vfs_access, + .xFullPathname = sdl_vfs_full_pathname, + .xDlOpen = sdl_vfs_dl_open, + .xDlError = sdl_vfs_dl_error, + .xDlSym = sdl_vfs_dl_sym, + .xDlClose = sdl_vfs_dl_close, + .xRandomness = sdl_vfs_randomness, + .xSleep = sdl_vfs_sleep, + .xCurrentTime = sdl_vfs_current_time, + .xGetLastError = sdl_vfs_get_last_error, + }; + g_sdlVfsRegisterResult = sqlite3_vfs_register(&sdlVfs, 0); + }); + return g_sdlVfsRegisterResult == SQLITE_OK; +} + +#if defined(__cpp_lib_atomic_ref) +static std::atomic_ref queuedPipelines{g_stats.queuedPipelines}; +static std::atomic_ref createdPipelines{g_stats.createdPipelines}; +#else +struct AtomicStatRef { + uint32_t& ref; + void operator++() { __atomic_fetch_add(&ref, 1, __ATOMIC_RELAXED); } + void operator--() { __atomic_fetch_sub(&ref, 1, __ATOMIC_RELAXED); } + void operator++(int) { __atomic_fetch_add(&ref, 1, __ATOMIC_RELAXED); } + void operator--(int) { __atomic_fetch_sub(&ref, 1, __ATOMIC_RELAXED); } + void operator=(uint32_t val) { __atomic_store_n(&ref, val, __ATOMIC_RELAXED); } + uint32_t load() const { return __atomic_load_n(&ref, __ATOMIC_RELAXED); } +}; +static AtomicStatRef queuedPipelines{g_stats.queuedPipelines}; +static AtomicStatRef createdPipelines{g_stats.createdPipelines}; +#endif + +template +static PipelineCacheWrite make_pipeline_cache_write(ShaderType type, PipelineRef hash, const PipelineConfig& config, + uint32_t firstFrameUsed) { + static_assert(std::has_unique_object_representations_v); + + PipelineCacheWrite write{ + .type = type, + .hash = hash, + .configVersion = config.version, + .config = ByteBuffer(sizeof(config)), + .firstFrameUsed = firstFrameUsed, + }; + std::memcpy(write.config.data(), &config, sizeof(config)); + return write; +} + +static void enqueue_pipeline_cache_write(PipelineCacheWrite write) { + if (g_pipelineCacheBroken || g_pipelineCacheDb == nullptr) { + return; + } + + { + std::lock_guard lock{g_pipelineCacheWriterMutex}; + if (!g_pipelineCachePendingWrites.insert(write.hash).second) { + return; + } + g_pipelineCacheWriteQueue.emplace_back(std::move(write)); + } + g_pipelineCacheWriterCv.notify_one(); +} + +template +static auto find_pending_pipeline(Queue& queue, PipelineRef hash) { + return std::find_if(queue.begin(), queue.end(), [=](const PendingPipeline& pending) { return pending.hash == hash; }); +} + +static PendingPipeline* touch_pending_pipeline(PipelineRef hash, bool prioritize) { + auto priorityIt = find_pending_pipeline(g_priorityPipelines, hash); + if (priorityIt != g_priorityPipelines.end()) { + return &*priorityIt; + } + + auto backgroundIt = find_pending_pipeline(g_backgroundPipelines, hash); + if (backgroundIt == g_backgroundPipelines.end()) { + return nullptr; + } + + if (!prioritize) { + return &*backgroundIt; + } + + g_priorityPipelines.emplace_back(std::move(*backgroundIt)); + g_backgroundPipelines.erase(backgroundIt); + return &g_priorityPipelines.back(); +} + +// A persistent resolve is the last chance to complete its producing draw, so promote its queued +// build ahead of first-use work. One already taken by a worker is compiling and has no entry. +static bool promote_pending_pipeline_for_wait(PipelineRef hash) { + auto priorityIt = find_pending_pipeline(g_priorityPipelines, hash); + if (priorityIt != g_priorityPipelines.end()) { + if (priorityIt != g_priorityPipelines.begin()) { + PendingPipeline pending = std::move(*priorityIt); + g_priorityPipelines.erase(priorityIt); + g_priorityPipelines.emplace_front(std::move(pending)); + } + return true; + } + + auto backgroundIt = find_pending_pipeline(g_backgroundPipelines, hash); + if (backgroundIt == g_backgroundPipelines.end()) { + return false; + } + PendingPipeline pending = std::move(*backgroundIt); + g_backgroundPipelines.erase(backgroundIt); + g_priorityPipelines.emplace_front(std::move(pending)); + return true; +} + +template +static bool remove_pending_pipeline(Queue& queue, PipelineRef hash) { + const auto it = find_pending_pipeline(queue, hash); + if (it == queue.end()) { + return false; + } + queue.erase(it); + return true; +} + +template +static PipelineRef find_pipeline_impl(ShaderType type, const PipelineConfig& config, NewPipelineCallback&& cb, + bool persist, std::optional firstFrameUsedOverride) { + ZoneScoped; + + const PipelineRef hash = xxh3_hash(config, static_cast(type)); + const uint32_t firstFrameUsed = firstFrameUsedOverride.value_or(current_frame()); + const bool cachePreload = firstFrameUsedOverride.has_value(); + bool notifyWorker = false; + bool syncCreate = false; + bool removedPending = false; + bool notifyWaiters = false; + std::optional cacheWrite; + { + std::scoped_lock guard{g_pipelineMutex}; + const bool deferGxPipeline = g_hasPipelineThread && g_pipelineFrameActive && type == ShaderType::GX; + const bool skipUnreadyGxPipeline = deferGxPipeline && g_skipUnreadyGxPipelines.load(std::memory_order_relaxed); + auto pipelineIt = g_pipelines.find(hash); + if (pipelineIt != g_pipelines.end()) { + if (persist && firstFrameUsed < pipelineIt->second.firstFrameUsed) { + pipelineIt->second.firstFrameUsed = firstFrameUsed; + cacheWrite = make_pipeline_cache_write(type, hash, config, firstFrameUsed); + } + } else if (g_pendingPipelines.contains(hash)) { + auto* pending = touch_pending_pipeline(hash, g_pipelineFrameActive); + if (pending != nullptr && firstFrameUsed < pending->firstFrameUsed) { + pending->firstFrameUsed = firstFrameUsed; + if (persist) { + cacheWrite = make_pipeline_cache_write(type, hash, config, firstFrameUsed); + } + } + if (g_pipelineFrameActive && !deferGxPipeline) { + syncCreate = true; + } + } else { + if (g_pipelineFrameActive && !deferGxPipeline) { + syncCreate = true; + } else { + if (!cachePreload && deferGxPipeline && g_pendingPipelines.size() >= MaxQueuedPipelineBuilds && + !g_backgroundPipelines.empty()) { + g_pendingPipelines.erase(g_backgroundPipelines.back().hash); + g_backgroundPipelines.pop_back(); + --queuedPipelines; + } + + // Keep the pipeline queued past the cap rather than compiling synchronously: the recorded draw + // references this hash forever, and dropping it baked one-shot EFB copies incomplete. + if (!cachePreload && g_pendingPipelines.size() >= MaxQueuedPipelineBuilds && !skipUnreadyGxPipeline) { + syncCreate = true; + } else { + auto& targetQueue = deferGxPipeline ? g_priorityPipelines : g_backgroundPipelines; + targetQueue.emplace_back(PendingPipeline{ + .hash = hash, + .firstFrameUsed = firstFrameUsed, + .create = std::move(cb), + }); + g_pendingPipelines.insert(hash); + ++queuedPipelines; + if (persist) { + cacheWrite = make_pipeline_cache_write(type, hash, config, firstFrameUsed); + } + notifyWorker = true; + } + } + } + } + + if (syncCreate) { + const auto pipeline = cb(); + { + std::scoped_lock guard{g_pipelineMutex}; + removedPending = remove_pending_pipeline(g_priorityPipelines, hash); + removedPending = remove_pending_pipeline(g_backgroundPipelines, hash) || removedPending; + if (removedPending) { + g_pendingPipelines.erase(hash); + } + auto [it, inserted] = g_pipelines.try_emplace(hash, CachedPipeline{ + .pipeline = pipeline, + .firstFrameUsed = firstFrameUsed, + }); + if (!inserted && persist && firstFrameUsed < it->second.firstFrameUsed) { + it->second.firstFrameUsed = firstFrameUsed; + } + if (inserted) { + ++createdPipelines; + } + if (persist) { + cacheWrite = make_pipeline_cache_write(type, hash, config, firstFrameUsed); + } + notifyWaiters = true; + } + } + + if (cacheWrite) { + enqueue_pipeline_cache_write(std::move(*cacheWrite)); + } + + if (notifyWorker) { + g_pipelineCv.notify_one(); + } + if (notifyWaiters) { + g_pipelineCv.notify_all(); + } + if (removedPending) { + --queuedPipelines; + } + + return hash; +} + +static void pipeline_cache_abort() { + g_pipelineCacheBroken = true; + if (g_pipelineCacheLoadStmt != nullptr) { + sqlite3_finalize(g_pipelineCacheLoadStmt); + g_pipelineCacheLoadStmt = nullptr; + } + if (g_pipelineCacheUpsertStmt != nullptr) { + sqlite3_finalize(g_pipelineCacheUpsertStmt); + g_pipelineCacheUpsertStmt = nullptr; + } + if (g_pipelineCacheDb != nullptr) { + sqlite3_close(g_pipelineCacheDb); + g_pipelineCacheDb = nullptr; + } +} + +static bool write_pipeline_cache_record(const PipelineCacheWrite& write); + +static std::string pipeline_cache_seed_path() { + if (g_config.resourcesPath == nullptr || g_config.resourcesPath[0] == '\0') { + return InitialPipelineCacheName; + } + + std::string path{g_config.resourcesPath}; + if (path.back() != '/' && path.back() != '\\') { + path += '/'; + } + path += InitialPipelineCacheName; + return path; +} + +static sqlite3* open_pipeline_cache_seed_db(const std::string& path) { + if (!register_sdl_vfs()) { + Log.warn("Failed to register SDL pipeline cache seed VFS"); + return nullptr; + } + + sqlite3* seedDb = nullptr; + const auto ret = + sqlite3_open_v2(path.c_str(), &seedDb, SQLITE_OPEN_READONLY | SQLITE_OPEN_PRIVATECACHE, SdlVfsName); + if (ret != SQLITE_OK) { + if (seedDb != nullptr) { + sqlite3_close(seedDb); + } + Log.info("No bundled initial pipeline cache found at '{}'", path); + return nullptr; + } + + bool schemaMatch = false; + const auto schemaQuery = fmt::format("SELECT 1 FROM aurora_schema WHERE value = {}", PipelineCacheSchema); + const auto schemaRet = + sqlite::exec(seedDb, schemaQuery.c_str(), [&schemaMatch](int, char**, char**) { schemaMatch = true; }); + if (schemaRet != SQLITE_OK) { + Log.warn("Failed to read bundled pipeline cache schema from '{}': {}", path, sqlite3_errmsg(seedDb)); + sqlite3_close(seedDb); + return nullptr; + } + if (!schemaMatch) { + Log.warn("Bundled pipeline cache '{}' does not use schema version {}", path, PipelineCacheSchema); + sqlite3_close(seedDb); + return nullptr; + } + + return seedDb; +} + +static void seed_pipeline_cache() { + if (g_pipelineCacheBroken || g_pipelineCacheDb == nullptr || g_pipelineCacheUpsertStmt == nullptr) { + return; + } + + const auto seedPath = pipeline_cache_seed_path(); + sqlite3* seedDb = open_pipeline_cache_seed_db(seedPath); + if (seedDb == nullptr) { + return; + } + + sqlite3_stmt* seedStmt = nullptr; + auto closeSeed = [&] { + if (seedStmt != nullptr) { + sqlite3_finalize(seedStmt); + seedStmt = nullptr; + } + sqlite3_close(seedDb); + }; + + auto ret = sqlite3_prepare_v3(seedDb, + "SELECT type, hash, config_version, config_size, config, first_frame_used " + "FROM pipeline_cache", + -1, 0, &seedStmt, nullptr); + if (ret != SQLITE_OK) { + Log.warn("Failed to read bundled pipeline cache rows from '{}': {}", seedPath, sqlite3_errmsg(seedDb)); + closeSeed(); + return; + } + + bool writeFailed = false; + bool readFailed = false; + uint32_t mergedRows = 0; + uint32_t skippedRows = 0; + { + sqlite::Transaction tx(g_pipelineCacheDb, Log, true); + if (!tx) { + Log.error("Failed to begin pipeline cache seed transaction"); + closeSeed(); + pipeline_cache_abort(); + return; + } + + while ((ret = sqlite3_step(seedStmt)) == SQLITE_ROW) { + const auto typeValue = sqlite3_column_int(seedStmt, 0); + const auto hashValue = sqlite3_column_int64(seedStmt, 1); + const auto configVersionValue = sqlite3_column_int(seedStmt, 2); + const auto configSizeValue = sqlite3_column_int(seedStmt, 3); + const auto* configBlob = static_cast(sqlite3_column_blob(seedStmt, 4)); + const auto configBlobSize = sqlite3_column_bytes(seedStmt, 4); + const auto firstFrameUsedValue = sqlite3_column_int64(seedStmt, 5); + constexpr auto MaxShaderTypeValue = std::numeric_limits>::max(); + if (typeValue < 0 || typeValue > MaxShaderTypeValue || configVersionValue < 0 || configSizeValue < 0 || + configSizeValue != configBlobSize || (configBlobSize > 0 && configBlob == nullptr) || + firstFrameUsedValue < 0 || firstFrameUsedValue > std::numeric_limits::max()) { + ++skippedRows; + continue; + } + + PipelineCacheWrite write{ + .type = static_cast(typeValue), + .hash = static_cast(hashValue), + .configVersion = static_cast(configVersionValue), + .config = ByteBuffer(static_cast(configBlobSize)), + .firstFrameUsed = static_cast(firstFrameUsedValue), + }; + if (configBlobSize > 0) { + std::memcpy(write.config.data(), configBlob, static_cast(configBlobSize)); + } + + if (!write_pipeline_cache_record(write)) { + writeFailed = true; + break; + } + ++mergedRows; + } + + if (!writeFailed && ret != SQLITE_DONE) { + Log.warn("Failed while reading bundled pipeline cache rows from '{}': {}", seedPath, sqlite3_errmsg(seedDb)); + readFailed = true; + } + + if (!writeFailed && !readFailed) { + tx.commit(); + } + } + + closeSeed(); + + if (writeFailed) { + pipeline_cache_abort(); + return; + } + + if (!readFailed) { + Log.info("Seeded pipeline cache from '{}' ({} rows merged, {} rows skipped)", seedPath, mergedRows, + skippedRows); + } +} + +static bool prepare_pipeline_cache_db() { + if (g_pipelineCacheBroken) { + return false; + } + if (g_pipelineCacheDb != nullptr) { + return true; + } + + const auto path = (std::filesystem::path{g_config.pipelineCachePath} / "pipeline_cache.db").string(); + auto ret = sqlite3_open(path.c_str(), &g_pipelineCacheDb); + if (ret != SQLITE_OK) { + Log.error("Failed to open pipeline cache database: {}", sqlite3_errmsg(g_pipelineCacheDb)); + pipeline_cache_abort(); + return false; + } + + ret = sqlite::exec(g_pipelineCacheDb, "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;"); + if (ret != SQLITE_OK) { + Log.error("Failed to set pipeline cache pragmas: {}", sqlite3_errmsg(g_pipelineCacheDb)); + pipeline_cache_abort(); + return false; + } + + bool schemaFailed = false; + { + sqlite::Transaction tx(g_pipelineCacheDb, Log, true); + if (!tx) { + Log.error("Failed to begin pipeline cache schema transaction"); + schemaFailed = true; + } else { + ret = sqlite::exec(g_pipelineCacheDb, "CREATE TABLE IF NOT EXISTS aurora_schema(value INTEGER);"); + if (ret != SQLITE_OK) { + Log.error("Failed to create pipeline cache schema table: {}", sqlite3_errmsg(g_pipelineCacheDb)); + schemaFailed = true; + } + } + + bool schemaMatch = false; + if (!schemaFailed) { + const auto schemaQuery = fmt::format("SELECT 1 FROM aurora_schema WHERE value = {}", PipelineCacheSchema); + ret = sqlite::exec(g_pipelineCacheDb, schemaQuery.c_str(), + [&schemaMatch](int, char**, char**) { schemaMatch = true; }); + if (ret != SQLITE_OK) { + Log.error("Failed to read pipeline cache schema version: {}", sqlite3_errmsg(g_pipelineCacheDb)); + schemaFailed = true; + } + } + + if (!schemaFailed && !schemaMatch) { + const auto schemaSql = fmt::format( + R"(DROP TABLE IF EXISTS pipeline_cache; +CREATE TABLE pipeline_cache ( + type INTEGER NOT NULL, + hash INTEGER NOT NULL, + config_version INTEGER NOT NULL, + config_size INTEGER NOT NULL, + config BLOB NOT NULL, + first_frame_used INTEGER NOT NULL, + PRIMARY KEY (type, hash) +); +CREATE INDEX pipeline_cache_load_order_idx + ON pipeline_cache(type, config_version, first_frame_used); +DELETE FROM aurora_schema; +INSERT INTO aurora_schema VALUES ({});)", + PipelineCacheSchema); + ret = sqlite::exec(g_pipelineCacheDb, schemaSql.c_str()); + if (ret != SQLITE_OK) { + Log.error("Failed to initialize pipeline cache schema: {}", sqlite3_errmsg(g_pipelineCacheDb)); + schemaFailed = true; + } + } + + if (!schemaFailed) { + tx.commit(); + } + } + + if (schemaFailed) { + pipeline_cache_abort(); + return false; + } + + ret = sqlite3_prepare_v3(g_pipelineCacheDb, + "SELECT hash, config, first_frame_used FROM pipeline_cache " + "WHERE type = ? AND config_version = ? " + "ORDER BY first_frame_used ASC, rowid ASC", + -1, SQLITE_PREPARE_PERSISTENT, &g_pipelineCacheLoadStmt, nullptr); + if (ret != SQLITE_OK) { + Log.error("Failed to prepare pipeline cache load statement: {}", sqlite3_errmsg(g_pipelineCacheDb)); + pipeline_cache_abort(); + return false; + } + + ret = sqlite3_prepare_v3( + g_pipelineCacheDb, + "INSERT INTO pipeline_cache (type, hash, config_version, config_size, config, first_frame_used) " + "VALUES (?, ?, ?, ?, ?, ?) " + "ON CONFLICT(type, hash) DO UPDATE SET " + "config_version = excluded.config_version, " + "config_size = excluded.config_size, " + "config = excluded.config, " + "first_frame_used = MIN(pipeline_cache.first_frame_used, excluded.first_frame_used)", + -1, SQLITE_PREPARE_PERSISTENT, &g_pipelineCacheUpsertStmt, nullptr); + if (ret != SQLITE_OK) { + Log.error("Failed to prepare pipeline cache upsert statement: {}", sqlite3_errmsg(g_pipelineCacheDb)); + pipeline_cache_abort(); + return false; + } + + seed_pipeline_cache(); + if (g_pipelineCacheBroken) { + return false; + } + + return true; +} + +static void prune_old_pipeline_cache_versions() { + if (!prepare_pipeline_cache_db()) { + return; + } + + const auto clearDelete = fmt::format("DELETE FROM pipeline_cache WHERE type = {} AND config_version < {}", + underlying(ShaderType::Clear), clear::ClearPipelineConfigVersion); + auto ret = sqlite::exec(g_pipelineCacheDb, clearDelete.c_str()); + if (ret != SQLITE_OK) { + Log.error("Failed to prune clear pipeline cache rows: {}", sqlite3_errmsg(g_pipelineCacheDb)); + pipeline_cache_abort(); + return; + } + + const auto gxDelete = fmt::format("DELETE FROM pipeline_cache WHERE type = {} AND config_version < {}", + underlying(ShaderType::GX), gx::GXPipelineConfigVersion); + ret = sqlite::exec(g_pipelineCacheDb, gxDelete.c_str()); + if (ret != SQLITE_OK) { + Log.error("Failed to prune GX pipeline cache rows: {}", sqlite3_errmsg(g_pipelineCacheDb)); + pipeline_cache_abort(); + } +} + +static bool write_pipeline_cache_record(const PipelineCacheWrite& write) { + const auto fail = [&]() { + sqlite3_reset(g_pipelineCacheUpsertStmt); + sqlite3_clear_bindings(g_pipelineCacheUpsertStmt); + return false; + }; + + auto ret = sqlite3_bind_int(g_pipelineCacheUpsertStmt, 1, underlying(write.type)); + if (ret != SQLITE_OK) { + Log.error("Failed to bind pipeline cache type: {}", sqlite3_errmsg(g_pipelineCacheDb)); + return fail(); + } + ret = sqlite3_bind_int64(g_pipelineCacheUpsertStmt, 2, static_cast(write.hash)); + if (ret != SQLITE_OK) { + Log.error("Failed to bind pipeline cache hash: {}", sqlite3_errmsg(g_pipelineCacheDb)); + return fail(); + } + ret = sqlite3_bind_int(g_pipelineCacheUpsertStmt, 3, static_cast(write.configVersion)); + if (ret != SQLITE_OK) { + Log.error("Failed to bind pipeline cache config version: {}", sqlite3_errmsg(g_pipelineCacheDb)); + return fail(); + } + ret = sqlite3_bind_int(g_pipelineCacheUpsertStmt, 4, static_cast(write.config.size())); + if (ret != SQLITE_OK) { + Log.error("Failed to bind pipeline cache config size: {}", sqlite3_errmsg(g_pipelineCacheDb)); + return fail(); + } + ret = sqlite3_bind_blob64(g_pipelineCacheUpsertStmt, 5, write.config.data(), write.config.size(), SQLITE_TRANSIENT); + if (ret != SQLITE_OK) { + Log.error("Failed to bind pipeline cache config blob: {}", sqlite3_errmsg(g_pipelineCacheDb)); + return fail(); + } + ret = sqlite3_bind_int64(g_pipelineCacheUpsertStmt, 6, static_cast(write.firstFrameUsed)); + if (ret != SQLITE_OK) { + Log.error("Failed to bind pipeline cache first-frame-used: {}", sqlite3_errmsg(g_pipelineCacheDb)); + return fail(); + } + + ret = sqlite3_step(g_pipelineCacheUpsertStmt); + if (ret != SQLITE_DONE) { + Log.error("Failed to upsert pipeline cache row: {}", sqlite3_errmsg(g_pipelineCacheDb)); + return fail(); + } + + sqlite3_reset(g_pipelineCacheUpsertStmt); + sqlite3_clear_bindings(g_pipelineCacheUpsertStmt); + return true; +} + +static void pipeline_cache_writer() { +#ifdef TRACY_ENABLE + tracy::SetThreadName("Pipeline cache writer thread"); +#endif + + while (true) { + std::deque batch; + { + std::unique_lock lock{g_pipelineCacheWriterMutex}; + g_pipelineCacheWriterCv.wait(lock, + [] { return g_pipelineCacheWriterStop || !g_pipelineCacheWriteQueue.empty(); }); + if (g_pipelineCacheWriterStop && g_pipelineCacheWriteQueue.empty()) { + return; + } + batch.swap(g_pipelineCacheWriteQueue); + g_pipelineCachePendingWrites.clear(); + } + + bool writeFailed = false; + { + sqlite::Transaction tx(g_pipelineCacheDb, Log, true); + if (!tx) { + Log.error("Failed to begin pipeline cache write transaction"); + writeFailed = true; + } else { + for (const auto& write : batch) { + if (!write_pipeline_cache_record(write)) { + writeFailed = true; + break; + } + } + + if (!writeFailed) { + tx.commit(); + } + } + } + + if (writeFailed) { + pipeline_cache_abort(); + return; + } + } +} + +// Boot prewarm telemetry: how long the cached-recipe rebuild takes and how well the +// Dawn blob cache served it. Logged once, when the launch queue first drains. +static std::atomic_bool g_prewarmActive{false}; +static std::chrono::steady_clock::time_point g_prewarmStart{}; +static uint32_t g_prewarmCount = 0; + +static void note_pipeline_queue_drained() { + // Every drained burst (boot prewarm, later first-use compiles) is a natural point to + // persist the monolithic Vulkan pipeline cache; Dawn skips the write when it is clean. + webgpu::serialize_pipeline_caches(); + if (!g_prewarmActive.exchange(false, std::memory_order_acq_rel)) { + return; + } + const auto elapsed = + std::chrono::duration_cast(std::chrono::steady_clock::now() - g_prewarmStart); + const auto stats = webgpu::blob_cache_stats(); + Log.info("Pipeline prewarm finished: {} pipelines in {:.1f} s (Dawn blob cache: {}/{} hits, {} stores, {:.1f} MiB " + "loaded)", + g_prewarmCount, elapsed.count() / 1000.0, stats.hits, stats.lookups, stats.stores, + static_cast(stats.hitBytes) / (1024.0 * 1024.0)); +} + +static void compile_pending_pipeline(PendingPipeline pending) { + auto result = pending.create(); + { + std::lock_guard lock{g_pipelineMutex}; + const auto [_, inserted] = g_pipelines.try_emplace(pending.hash, CachedPipeline{ + .pipeline = std::move(result), + .firstFrameUsed = pending.firstFrameUsed, + }); + g_pendingPipelines.erase(pending.hash); + if (inserted) { + ++createdPipelines; + } + } + g_pipelineCv.notify_all(); + --queuedPipelines; + if (queuedPipelines.load() == 0) { + note_pipeline_queue_drained(); + } +} + +static void pipeline_worker() { +#ifdef TRACY_ENABLE + tracy::SetThreadName("Pipeline compilation thread"); +#endif + + while (true) { + PendingPipeline pending; + bool background = false; + { + std::unique_lock lock{g_pipelineMutex}; + g_pipelineCv.wait(lock, [] { + return !g_priorityPipelines.empty() || + (!g_backgroundPipelines.empty() && + g_activeBackgroundPipelineWorkers < MaxBackgroundPipelineWorkers) || + g_pipelineThreadEnd; + }); + if (g_pipelineThreadEnd) { + return; + } + background = g_priorityPipelines.empty(); + auto& source = background ? g_backgroundPipelines : g_priorityPipelines; + pending = std::move(source.front()); + source.pop_front(); + if (background) { + ++g_activeBackgroundPipelineWorkers; + } + } + compile_pending_pipeline(std::move(pending)); + if (background) { + { + std::lock_guard lock{g_pipelineMutex}; + --g_activeBackgroundPipelineWorkers; + } + g_pipelineCv.notify_all(); + } + } +} + +static void build_synchronous_pipelines_for_frame() { + while (g_pipelinesPerFrame < BuildPipelinesPerFrame) { + PendingPipeline pending; + { + std::lock_guard lock{g_pipelineMutex}; + if (g_priorityPipelines.empty() && g_backgroundPipelines.empty()) { + return; + } + auto& source = !g_priorityPipelines.empty() ? g_priorityPipelines : g_backgroundPipelines; + pending = std::move(source.front()); + source.pop_front(); + } + compile_pending_pipeline(std::move(pending)); + ++g_pipelinesPerFrame; + } +} + +static size_t pipeline_worker_count() { + const size_t logicalProcessors = std::thread::hardware_concurrency(); + if (logicalProcessors == 0) { + return 1; + } + const size_t availableWorkers = + logicalProcessors > ReservedLogicalProcessors ? logicalProcessors - ReservedLogicalProcessors : 1; + return std::clamp(availableWorkers, size_t{1}, MaxPipelineWorkers); +} + +template +static void load_pipeline_cache_entries(ShaderType type, uint32_t configVersion, CreateFn&& create) { + if (!prepare_pipeline_cache_db()) { + return; + } + + auto ret = sqlite3_bind_int(g_pipelineCacheLoadStmt, 1, underlying(type)); + if (ret != SQLITE_OK) { + Log.error("Failed to bind pipeline cache load type: {}", sqlite3_errmsg(g_pipelineCacheDb)); + pipeline_cache_abort(); + return; + } + ret = sqlite3_bind_int(g_pipelineCacheLoadStmt, 2, static_cast(configVersion)); + if (ret != SQLITE_OK) { + Log.error("Failed to bind pipeline cache load config version: {}", sqlite3_errmsg(g_pipelineCacheDb)); + pipeline_cache_abort(); + return; + } + + while ((ret = sqlite3_step(g_pipelineCacheLoadStmt)) == SQLITE_ROW) { + const auto storedHash = static_cast(sqlite3_column_int64(g_pipelineCacheLoadStmt, 0)); + const auto* configBlob = static_cast(sqlite3_column_blob(g_pipelineCacheLoadStmt, 1)); + const auto configSize = sqlite3_column_bytes(g_pipelineCacheLoadStmt, 1); + const auto firstFrameUsed = static_cast(sqlite3_column_int64(g_pipelineCacheLoadStmt, 2)); + if (configSize != static_cast(sizeof(PipelineConfig)) || (configSize != 0 && configBlob == nullptr)) { + continue; + } + if (xxh3_hash_s(configBlob, static_cast(configSize), static_cast(type)) != storedHash) { + Log.warn("Skipping cached pipeline configuration with mismatched content hash"); + continue; + } + PipelineConfig config; + std::memcpy(&config, configBlob, sizeof(config)); + if (config.version != configVersion) { + continue; + } + if constexpr (std::is_same_v) { + if (!gx::valid_pipeline_config(config)) { + Log.warn("Skipping invalid cached GX pipeline configuration"); + continue; + } + } + + find_pipeline_impl(type, config, [=] { return create(config); }, false, firstFrameUsed); + } + + if (ret != SQLITE_DONE) { + Log.error("Failed to read pipeline cache rows: {}", sqlite3_errmsg(g_pipelineCacheDb)); + pipeline_cache_abort(); + } + + sqlite3_reset(g_pipelineCacheLoadStmt); + sqlite3_clear_bindings(g_pipelineCacheLoadStmt); +} + +static void load_pipeline_cache() { + if (!prepare_pipeline_cache_db()) { + return; + } + prune_old_pipeline_cache_versions(); + if (g_pipelineCacheBroken) { + return; + } + load_pipeline_cache_entries(ShaderType::Clear, clear::ClearPipelineConfigVersion, + clear::create_pipeline); + load_pipeline_cache_entries(ShaderType::GX, gx::GXPipelineConfigVersion, + gx::create_pipeline); + const auto queued = queuedPipelines.load(); + if (queued > 0) { + g_prewarmCount = queued; + g_prewarmStart = std::chrono::steady_clock::now(); + g_prewarmActive.store(true, std::memory_order_release); + } +} + +static void start_pipeline_cache_writer() { + if (!prepare_pipeline_cache_db()) { + return; + } + + g_pipelineCacheWriterStop = false; + g_pipelineCacheWriterThread = std::thread(pipeline_cache_writer); +} + +static void stop_pipeline_cache_writer() { + if (g_pipelineCacheWriterThread.joinable()) { + { + std::lock_guard lock{g_pipelineCacheWriterMutex}; + g_pipelineCacheWriterStop = true; + } + g_pipelineCacheWriterCv.notify_one(); + g_pipelineCacheWriterThread.join(); + } else { + g_pipelineCacheWriterStop = false; + } + + g_pipelineCacheWriteQueue.clear(); + g_pipelineCachePendingWrites.clear(); +} + +template <> +PipelineRef find_pipeline(ShaderType type, const clear::PipelineConfig& config, NewPipelineCallback&& cb) { + return find_pipeline_impl(type, config, std::move(cb), true, std::nullopt); +} + +template <> +PipelineRef find_pipeline(ShaderType type, const gx::PipelineConfig& config, NewPipelineCallback&& cb) { + return find_pipeline_impl(type, config, std::move(cb), true, std::nullopt); +} + +void initialize_pipeline_cache() { + g_pipelineCacheBroken = false; + g_pipelineCacheWriterStop = false; + g_pipelineFrameActive = false; + g_pipelineThreadEnd = false; + g_activeBackgroundPipelineWorkers = 0; + + if (webgpu::g_backendType == wgpu::BackendType::OpenGL || webgpu::g_backendType == wgpu::BackendType::OpenGLES || + webgpu::g_backendType == wgpu::BackendType::WebGPU) { + g_hasPipelineThread = false; + } else { + g_hasPipelineThread = true; + const size_t workerCount = pipeline_worker_count(); + g_pipelineThreads.reserve(workerCount); + for (size_t i = 0; i < workerCount; ++i) { + g_pipelineThreads.emplace_back(pipeline_worker); + } + Log.info("Enabled {} priority pipeline compilation workers ({} background prewarm)", + workerCount, MaxBackgroundPipelineWorkers); + } + + load_pipeline_cache(); + if (!g_pipelineCacheBroken) { + start_pipeline_cache_writer(); + } +} + +void shutdown_pipeline_cache() { + if (g_hasPipelineThread) { + { + std::lock_guard lock{g_pipelineMutex}; + g_pipelineThreadEnd = true; + } + g_pipelineCv.notify_all(); + for (auto& thread : g_pipelineThreads) { + thread.join(); + } + } + g_pipelineThreads.clear(); + g_hasPipelineThread = false; + g_activeBackgroundPipelineWorkers = 0; + + stop_pipeline_cache_writer(); + pipeline_cache_abort(); + g_pipelineCacheBroken = false; + g_pipelineFrameActive = false; + g_pipelinesPerFrame = 0; + g_pipelines.clear(); + g_priorityPipelines.clear(); + g_backgroundPipelines.clear(); + g_pendingPipelines.clear(); + + queuedPipelines = 0; + createdPipelines = 0; +} + +void begin_pipeline_frame() { + g_pipelineFrameActive = true; + if (!g_hasPipelineThread) { + g_pipelinesPerFrame = 0; + } +} + +void end_pipeline_frame() { + g_pipelineFrameActive = false; + if (!g_hasPipelineThread) { + build_synchronous_pipelines_for_frame(); + } +} + +void set_skip_unready_pipelines(bool enabled) noexcept { + g_skipUnreadyGxPipelines.store(enabled, std::memory_order_relaxed); +} + +bool skip_unready_pipelines() noexcept { return g_skipUnreadyGxPipelines.load(std::memory_order_relaxed); } + +uint32_t queued_pipeline_count() noexcept { +#if defined(__cpp_lib_atomic_ref) + return queuedPipelines.load(std::memory_order_relaxed); +#else + return queuedPipelines.load(); +#endif +} + +bool try_pipeline(PipelineRef ref, wgpu::RenderPipeline& pipeline) { + std::scoped_lock lock{g_pipelineMutex}; + const auto it = g_pipelines.find(ref); + if (it == g_pipelines.end()) { + return false; + } + pipeline = it->second.pipeline; + return true; +} + +static bool wait_pipeline_impl(PipelineRef ref, wgpu::RenderPipeline& pipeline, bool fatalIfMissing, + bool persistentPass) { + std::unique_lock lock{g_pipelineMutex}; + if (!g_pipelines.contains(ref) && g_pendingPipelines.contains(ref)) { + ZoneScopedN("wait_pipeline"); + const auto finished = [ref] { + return g_pipelines.contains(ref) || !g_pendingPipelines.contains(ref) || g_pipelineThreadEnd; + }; + if (persistentPass) { + if (promote_pending_pipeline_for_wait(ref)) { + g_pipelineCv.notify_all(); + } + // A persistent resolve is the last chance to produce its texture, so timing out here corrupts it + // permanently. Only a pass marked requireReadyPipelines takes this path. + } + g_pipelineCv.wait(lock, finished); + } + const auto it = g_pipelines.find(ref); + if (it == g_pipelines.end()) { + lock.unlock(); + if (fatalIfMissing) { + Log.fatal("Pipeline 0x{:x} is unavailable at its ordered draw boundary", static_cast(ref)); + } + Log.error("Pipeline 0x{:x} is unavailable for a persistent resolve pass; dropping its draw", + static_cast(ref)); + return false; + } + pipeline = it->second.pipeline; + return true; +} + +bool wait_pipeline(PipelineRef ref, wgpu::RenderPipeline& pipeline) { + return wait_pipeline_impl(ref, pipeline, true, false); +} + +bool wait_pipeline_for_persistent_pass(PipelineRef ref, wgpu::RenderPipeline& pipeline) { + return wait_pipeline_impl(ref, pipeline, false, true); +} + +} // namespace aurora::gfx + +void aurora_set_skip_unready_pipelines(const bool enabled) { aurora::gfx::set_skip_unready_pipelines(enabled); } + +bool aurora_get_skip_unready_pipelines() { return aurora::gfx::skip_unready_pipelines(); } + +uint32_t aurora_get_queued_pipeline_count() { return aurora::gfx::queued_pipeline_count(); } diff --git a/aurora-main/lib/gfx/pipeline_cache.hpp b/aurora-main/lib/gfx/pipeline_cache.hpp new file mode 100644 index 0000000..737e531 --- /dev/null +++ b/aurora-main/lib/gfx/pipeline_cache.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "common.hpp" + +#include + +namespace aurora::gfx::clear { +struct PipelineConfig; +} // namespace aurora::gfx::clear + +namespace aurora::gx { +struct PipelineConfig; +} // namespace aurora::gx + +namespace aurora::gfx { + +using NewPipelineCallback = std::function; + +void initialize_pipeline_cache(); +void shutdown_pipeline_cache(); +void begin_pipeline_frame(); +void end_pipeline_frame(); +void set_skip_unready_pipelines(bool enabled) noexcept; +bool skip_unready_pipelines() noexcept; +uint32_t queued_pipeline_count() noexcept; + +template +PipelineRef find_pipeline(ShaderType type, const Config& config, NewPipelineCallback&& cb); + +bool wait_pipeline(PipelineRef ref, wgpu::RenderPipeline& pipeline); +bool try_pipeline(PipelineRef ref, wgpu::RenderPipeline& pipeline); +// Waits indefinitely for a draw about to be committed to a persistent texture with no re-issue +// path. Separate from wait_pipeline so skip-unready mode never becomes blocking for other passes. +bool wait_pipeline_for_persistent_pass(PipelineRef ref, wgpu::RenderPipeline& pipeline); + +} // namespace aurora::gfx diff --git a/aurora-main/lib/gfx/png_io.cpp b/aurora-main/lib/gfx/png_io.cpp new file mode 100644 index 0000000..ea3ac62 --- /dev/null +++ b/aurora-main/lib/gfx/png_io.cpp @@ -0,0 +1,102 @@ +#include "png_io.hpp" + +#include + +#include "dds_io.hpp" +#include "png.h" +#include "../fs_helper.hpp" + +static aurora::Module Log("aurora::gfx::png"); + +namespace aurora::gfx::png { + +struct PngStructs { + png_structp pStruct; + png_infop pInfo; + std::ifstream file; + + ~PngStructs() { + png_destroy_read_struct(&pStruct, &pInfo, nullptr); + } +}; + +static void readPngData(png_structp png, png_bytep data, const size_t length) { + auto structs = static_cast(png_get_io_ptr(png)); + structs->file.read(reinterpret_cast(data), static_cast(length)); + + if (structs->file.eof() || structs->file.fail()) { + png_error(png, "file read failed!"); + } +} + +std::optional +load_png_file(const std::filesystem::path& path) noexcept { + PngStructs structs{}; + + structs.file = std::move(std::ifstream(path, std::ifstream::in | std::ifstream::binary)); + if (!structs.file) { + Log.error("failed to open file: {}", fs_path_to_string(path)); + return std::nullopt; + } + + structs.pStruct = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (!structs.pStruct) { + Log.error("png_create_read_struct failed"); + return std::nullopt; + } + + structs.pInfo = png_create_info_struct(structs.pStruct); + if (!structs.pInfo) { + Log.error("png_create_info_struct failed"); + return std::nullopt; + } + + // I'm scared of putting any locals after that setjmp. + std::vector rowPointers; + ByteBuffer imageData{}; + png_uint_32 width, height; + int bit_depth, color_type, interlace_type, compression_type, filter_type; + size_t rowBytes; + int i; + + if (setjmp(png_jmpbuf(structs.pStruct))) { + Log.error("libpng encountered an error"); + return std::nullopt; + } + + png_set_read_fn(structs.pStruct, &structs, readPngData); + png_read_info(structs.pStruct, structs.pInfo); + + if (!png_get_IHDR(structs.pStruct, structs.pInfo, &width, &height, &bit_depth, &color_type, &interlace_type, &compression_type, &filter_type)) { + Log.error("libpng unable to read IHDR"); + return std::nullopt; + } + + // Always read as RGBA8. + png_set_gray_to_rgb(structs.pStruct); + png_set_filler(structs.pStruct, 0xFF, PNG_FILLER_AFTER); + png_set_expand(structs.pStruct); + png_set_strip_16(structs.pStruct); + + png_read_update_info(structs.pStruct, structs.pInfo); + rowBytes = png_get_rowbytes(structs.pStruct, structs.pInfo); + rowPointers.resize(height); + + imageData.append_zeroes(rowBytes * height); + + for (i = 0; i < height; i++) { + rowPointers[i] = imageData.data() + i * rowBytes; + } + + png_read_image(structs.pStruct, rowPointers.data()); + png_read_end(structs.pStruct, nullptr); + + return ConvertedTexture{ + .format = wgpu::TextureFormat::RGBA8Unorm, + .width = width, + .height = height, + .mips = 1, + .data = std::move(imageData) + }; +} +} \ No newline at end of file diff --git a/aurora-main/lib/gfx/png_io.hpp b/aurora-main/lib/gfx/png_io.hpp new file mode 100644 index 0000000..a0f32a7 --- /dev/null +++ b/aurora-main/lib/gfx/png_io.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include +#include + +#include "texture_convert.hpp" + +namespace aurora::gfx::png { +std::optional load_png_file(const std::filesystem::path& path) noexcept; +} diff --git a/aurora-main/lib/gfx/tex_copy_conv.cpp b/aurora-main/lib/gfx/tex_copy_conv.cpp new file mode 100644 index 0000000..c5e8171 --- /dev/null +++ b/aurora-main/lib/gfx/tex_copy_conv.cpp @@ -0,0 +1,607 @@ +#include "tex_copy_conv.hpp" +#include "tex_copy_format_contract.hpp" + +#include "../internal.hpp" +#include "../gx/gx.hpp" +#include "../webgpu/gpu.hpp" +#include "texture.hpp" +#include "../gx/gx_fmt.hpp" + +#include + +#include "texture_convert.hpp" + +using namespace std::string_literals; + +namespace aurora::gfx::tex_copy_conv { +static Module Log("aurora::gfx::tex_copy_conv"); + +using webgpu::g_device; + +static constexpr std::string_view ShaderPreamble = R"( +@group(0) @binding(0) var src_samp: sampler; +@group(0) @binding(1) var src: texture_2d; + +struct UVTransform { + offset: vec2f, + scale: vec2f, + copy_filter: vec4f, + flags: vec4f, +}; +@group(0) @binding(2) var uv_xf: UVTransform; + +struct VertexOutput { + @builtin(position) pos: vec4f, + @location(0) uv: vec2f, +}; + +var positions: array = array( + vec2f(-1.0, 1.0), + vec2f(-1.0, -3.0), + vec2f(3.0, 1.0), +); +var uvs: array = array( + vec2f(0.0, 0.0), + vec2f(0.0, 2.0), + vec2f(2.0, 0.0), +); + +@vertex fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput { + var out: VertexOutput; + out.pos = vec4f(positions[vi], 0.0, 1.0); + out.uv = uvs[vi] * uv_xf.scale + uv_xf.offset; + return out; +} + +fn intensity(rgb: vec3f) -> f32 { + // ITU-R BT.601 luma coefficients + return dot(rgb, vec3f(0.257, 0.504, 0.098)) + 16.0 / 255.0; +} + +fn quantize4(v: f32) -> f32 { + return floor(v * 16.0) / 15.0; +} + +fn apply_opaque_alpha(c: vec4f) -> vec4f { + if (uv_xf.flags.x != 0.0) { + return vec4f(c.rgb, 1.0); + } + return c; +} + +fn clamp_copy_uv(uv: vec2f) -> vec2f { + return vec2f(uv.x, clamp(uv.y, uv_xf.flags.z, uv_xf.flags.w)); +} + +fn sample_copy(uv: vec2f) -> vec4f { + let current = textureSample(src, src_samp, clamp_copy_uv(uv)); + if (uv_xf.copy_filter.w == 0.0) { + return apply_opaque_alpha(current); + } + + let tex_size = vec2f(textureDimensions(src)); + let pixel_size = vec2f(1.0, 1.0) / tex_size; + let row_stride = max(uv_xf.flags.y, 1.0); + let prev = textureSample(src, src_samp, + clamp_copy_uv(uv - vec2f(0.0, pixel_size.y * row_stride))); + let next = textureSample(src, src_samp, + clamp_copy_uv(uv + vec2f(0.0, pixel_size.y * row_stride))); + let prev_rgb = floor(prev.rgb * 255.0 + vec3f(0.5)); + let current_rgb = floor(current.rgb * 255.0 + vec3f(0.5)); + let next_rgb = floor(next.rgb * 255.0 + vec3f(0.5)); + let filtered_rgb = min( + floor((prev_rgb * uv_xf.copy_filter.x + + current_rgb * uv_xf.copy_filter.y + + next_rgb * uv_xf.copy_filter.z) / 64.0), + vec3f(255.0)); + let filtered = vec4f(filtered_rgb / 255.0, current.a); + return apply_opaque_alpha(filtered); +} +)"sv; + +static const std::string DepthShaderPreamble = R"( +@group(0) @binding(0) var src: texture_depth_2d; + +struct UVTransform { + offset: vec2f, + scale: vec2f, + copy_filter: vec4f, + flags: vec4f, +}; +@group(0) @binding(1) var uv_xf: UVTransform; + +struct VertexOutput { + @builtin(position) pos: vec4f, + @location(0) uv: vec2f, +}; + +var positions: array = array( + vec2f(-1.0, 1.0), + vec2f(-1.0, -3.0), + vec2f(3.0, 1.0), +); +var uvs: array = array( + vec2f(0.0, 0.0), + vec2f(0.0, 2.0), + vec2f(2.0, 0.0), +); + +@vertex fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput { + var out: VertexOutput; + out.pos = vec4f(positions[vi], 0.0, 1.0); + out.uv = uvs[vi] * uv_xf.scale + uv_xf.offset; + return out; +} +)"s + (gx::UseReversedZ ? R"( +fn gx_z24_at_coord(unclamped_coord: vec2i) -> u32 { + let tex_size = vec2i(textureDimensions(src)); + let coord = clamp(unclamped_coord, vec2i(0), tex_size - vec2i(1)); + let depth = textureLoad(src, coord, 0); + return min(u32(clamp(depth, 0.0, 1.0) * 16777216.0), 0x00ffffffu); +} +)"s + : R"( +fn gx_z24_at_coord(unclamped_coord: vec2i) -> u32 { + let tex_size = vec2i(textureDimensions(src)); + let coord = clamp(unclamped_coord, vec2i(0), tex_size - vec2i(1)); + let depth = textureLoad(src, coord, 0); + return min(u32(clamp(depth, 0.0, 1.0) * 16777215.0 + 0.5), 0x00ffffffu); +} +)"s) + R"( +fn gx_depth_bytes(z24: u32) -> vec3u { + return vec3u((z24 >> 16u) & 0xffu, (z24 >> 8u) & 0xffu, z24 & 0xffu); +} + +fn clamp_copy_coord(coord: vec2i, tex_size: vec2i) -> vec2i { + let top = i32(floor(uv_xf.flags.z * f32(tex_size.y))); + let bottom = i32(floor(uv_xf.flags.w * f32(tex_size.y))); + return vec2i(coord.x, clamp(coord.y, top, bottom)); +} + +fn sample_depth_copy(uv: vec2f) -> vec4u { + let tex_size = vec2i(textureDimensions(src)); + let current_coord = clamp_copy_coord(vec2i(floor(uv * vec2f(tex_size))), tex_size); + let current = gx_depth_bytes(gx_z24_at_coord(current_coord)); + if (uv_xf.copy_filter.w == 0.0) { + return vec4u(current, 255u); + } + + // GX applies its vertical copy filter to depth copies too, filtering the high/middle/low Z bytes + // independently before the destination format picks bytes. Alpha is unfiltered and 255. + let row_stride = max(i32(round(uv_xf.flags.y)), 1); + let prev = gx_depth_bytes(gx_z24_at_coord( + clamp_copy_coord(current_coord - vec2i(0, row_stride), tex_size))); + let next = gx_depth_bytes(gx_z24_at_coord( + clamp_copy_coord(current_coord + vec2i(0, row_stride), tex_size))); + let coefficients = vec3u(uv_xf.copy_filter.xyz); + let combined = prev * coefficients.x + current * coefficients.y + next * coefficients.z; + var filtered = combined >> vec3u(6u); + + // The copy-filter accumulator wraps to nine bits when coefficients can + // produce values at or above 512, before saturating to an eight-bit byte. + if (coefficients.x + coefficients.y + coefficients.z >= 128u) { + filtered = filtered & vec3u(0x1ffu); + } + filtered = min(filtered, vec3u(255u)); + return vec4u(filtered, 255u); +} +)"s; + +// Passthrough blit (for scaling) +static constexpr std::string_view FragPassthrough = R"( +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + return sample_copy(in.uv); +} +)"sv; + +// GX_TF_I4: 4-bit intensity -> R8Unorm (quantized) +static constexpr std::string_view FragI4 = R"( +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + let rgb = sample_copy(in.uv).rgb; + let i = quantize4(intensity(rgb)); + return vec4f(i, i, i, i); +} +)"sv; + +// GX_TF_I8: 8-bit intensity -> R8Unorm +static constexpr std::string_view FragI8 = R"( +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + let rgb = sample_copy(in.uv).rgb; + let i = intensity(rgb); + return vec4f(i, i, i, i); +} +)"sv; + +// GX_TF_IA4: 4-bit intensity + 4-bit alpha -> RG8Unorm +static constexpr std::string_view FragIA4 = R"( +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + let c = sample_copy(in.uv); + let i = quantize4(intensity(c.rgb)); + let a = quantize4(c.a); + return vec4f(i, i, i, a); +} +)"sv; + +// GX_TF_IA8: 8-bit intensity + 8-bit alpha -> RG8Unorm +static constexpr std::string_view FragIA8 = R"( +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + let c = sample_copy(in.uv); + let i = intensity(c.rgb); + return vec4f(i, i, i, c.a); +} +)"sv; + +// GX_TF_RGB565: Blit alpha to 1.0 +static constexpr std::string_view FragRGB565 = R"( +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + let c = sample_copy(in.uv); + return vec4f(c.rgb, 1.0); +} +)"sv; + +// GX_CTF_R4: 4-bit red -> R8Unorm +static constexpr std::string_view FragR4 = R"( +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + let r = quantize4(sample_copy(in.uv).r); + return vec4f(r, r, r, r); +} +)"sv; + +// GX_CTF_RA4: 4-bit red + 4-bit alpha -> RG8Unorm +static constexpr std::string_view FragRA4 = R"( +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + let c = sample_copy(in.uv); + let r = quantize4(c.r); + return vec4f(r, r, r, quantize4(c.a)); +} +)"sv; + +// GX_CTF_RA8: 8-bit red + 8-bit alpha -> RG8Unorm +static constexpr std::string_view FragRA8 = R"( +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + let c = sample_copy(in.uv); + return vec4f(c.r, c.r, c.r, c.a); +} +)"sv; + +// GX_CTF_A8: 8-bit alpha -> R8Unorm +static constexpr std::string_view FragA8 = R"( +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + let a = sample_copy(in.uv).a; + return vec4f(a, a, a, a); +} +)"sv; + +// GX_CTF_R8: 8-bit red -> R8Unorm +static constexpr std::string_view FragR8 = R"( +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + let r = sample_copy(in.uv).r; + return vec4f(r, r, r, r); +} +)"sv; + +// GX_CTF_G8: 8-bit green -> R8Unorm +static constexpr std::string_view FragG8 = R"( +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + let g = sample_copy(in.uv).g; + return vec4f(g, g, g, g); +} +)"sv; + +// GX_CTF_B8: 8-bit blue -> R8Unorm +static constexpr std::string_view FragB8 = R"( +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + let b = sample_copy(in.uv).b; + return vec4f(b, b, b, b); +} +)"sv; + +// GX_CTF_RG8: 8-bit red + 8-bit green -> RG8Unorm +static constexpr std::string_view FragRG8 = R"( +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + let c = sample_copy(in.uv); + return vec4f(c.r, c.r, c.r, c.g); +} +)"sv; + +// GX_CTF_GB8: 8-bit green + 8-bit blue -> RG8Unorm +static constexpr std::string_view FragGB8 = R"( +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + let c = sample_copy(in.uv); + return vec4f(c.g, c.g, c.g, c.b); +} +)"sv; + +// GX_TF_Z8 stores the high-Z byte in I8 storage, which samples as intensity +// replicated across all four channels. +static constexpr std::string_view FragZ8 = R"( +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + let depth_bytes = sample_depth_copy(in.uv); + let z8 = f32(depth_bytes.r) / 255.0; + return vec4f(z8, z8, z8, z8); +} +)"sv; + +// GX_TF_Z16 is the depth RA8 copy path: depth samples have opaque alpha and their high-Z byte in +// red, so the encoded [A, R] pair samples through IA8 as high-Z intensity with opaque alpha. +static constexpr std::string_view FragZ16 = detail::Z16FragmentShader; + +// GX_TF_Z24X8 uses the RGBA8 storage shape for copies, with the 24-bit depth +// value available to z-texture sampling through RGB. +static constexpr std::string_view FragZ24X8 = R"( +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + let depth_bytes = sample_depth_copy(in.uv); + let r = f32(depth_bytes.r) / 255.0; + let g = f32(depth_bytes.g) / 255.0; + let b = f32(depth_bytes.b) / 255.0; + return vec4f(r, g, b, 1.0); +} +)"sv; + +struct ConvPipeline { + GXTexFmt fmt; + std::string_view fragShader; + wgpu::TextureFormat outputFormat; + const char* label; +}; + +static constexpr std::array ConvPipelines{ + ConvPipeline{GX_TF_I4, FragI4, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv I4"}, + ConvPipeline{GX_TF_I8, FragI8, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv I8"}, + ConvPipeline{GX_TF_IA4, FragIA4, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv IA4"}, + ConvPipeline{GX_TF_IA8, FragIA8, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv IA8"}, + ConvPipeline{GX_TF_RGB565, FragRGB565, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv RGB565"}, + ConvPipeline{GX_CTF_R4, FragR4, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv R4"}, + ConvPipeline{GX_CTF_RA4, FragRA4, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv RA4"}, + ConvPipeline{GX_CTF_RA8, FragRA8, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv RA8"}, + ConvPipeline{GX_CTF_A8, FragA8, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv A8"}, + ConvPipeline{GX_CTF_R8, FragR8, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv R8"}, + ConvPipeline{GX_CTF_G8, FragG8, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv G8"}, + ConvPipeline{GX_CTF_B8, FragB8, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv B8"}, + ConvPipeline{GX_CTF_RG8, FragRG8, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv RG8"}, + ConvPipeline{GX_CTF_GB8, FragGB8, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv GB8"}, +}; + +static constexpr std::array DepthConvPipelines{ + ConvPipeline{GX_TF_Z8, FragZ8, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv Z8"}, + ConvPipeline{GX_TF_Z16, FragZ16, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv Z16"}, + ConvPipeline{GX_TF_Z24X8, FragZ24X8, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv Z24X8"}, +}; + +static wgpu::BindGroupLayout g_bindGroupLayout; +static wgpu::BindGroupLayout g_depthBindGroupLayout; +static wgpu::Sampler g_nearestSampler; +static wgpu::Sampler g_linearSampler; +static absl::flat_hash_map g_pipelines; +static wgpu::RenderPipeline g_blitPipeline; + +static wgpu::RenderPipeline create_pipeline(const ConvPipeline& conv, const std::string_view shaderPreamble, + const wgpu::BindGroupLayout& bindGroupLayout) { + std::string shaderSource; + shaderSource.reserve(shaderPreamble.size() + conv.fragShader.size()); + shaderSource += shaderPreamble; + shaderSource += conv.fragShader; + + const wgpu::ShaderSourceWGSL wgslSource{wgpu::ShaderSourceWGSL::Init{ + .code = shaderSource.c_str(), + }}; + const wgpu::ShaderModuleDescriptor moduleDescriptor{ + .nextInChain = &wgslSource, + .label = conv.label, + }; + const auto module = g_device.CreateShaderModule(&moduleDescriptor); + + const std::array colorTargets{wgpu::ColorTargetState{ + .format = conv.outputFormat, + }}; + const wgpu::FragmentState fragmentState{ + .module = module, + .entryPoint = "fs_main", + .targetCount = colorTargets.size(), + .targets = colorTargets.data(), + }; + + const wgpu::PipelineLayoutDescriptor layoutDescriptor{ + .bindGroupLayoutCount = 1, + .bindGroupLayouts = &bindGroupLayout, + }; + const auto pipelineLayout = g_device.CreatePipelineLayout(&layoutDescriptor); + + const wgpu::RenderPipelineDescriptor pipelineDescriptor{ + .label = conv.label, + .layout = pipelineLayout, + .vertex = + wgpu::VertexState{ + .module = module, + .entryPoint = "vs_main", + }, + .primitive = + wgpu::PrimitiveState{ + .topology = wgpu::PrimitiveTopology::TriangleList, + }, + .fragment = &fragmentState, + }; + return g_device.CreateRenderPipeline(&pipelineDescriptor); +} + +bool needs_conversion(const GXTexFmt fmt) { return g_pipelines.contains(fmt); } + +void initialize() { + static constexpr std::array bindGroupLayoutEntries{ + wgpu::BindGroupLayoutEntry{ + .binding = 0, + .visibility = wgpu::ShaderStage::Fragment, + .sampler = + wgpu::SamplerBindingLayout{ + .type = wgpu::SamplerBindingType::Filtering, + }, + }, + wgpu::BindGroupLayoutEntry{ + .binding = 1, + .visibility = wgpu::ShaderStage::Fragment, + .texture = + wgpu::TextureBindingLayout{ + .sampleType = wgpu::TextureSampleType::Float, + .viewDimension = wgpu::TextureViewDimension::e2D, + }, + }, + wgpu::BindGroupLayoutEntry{ + .binding = 2, + .visibility = wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment, + .buffer = + wgpu::BufferBindingLayout{ + .type = wgpu::BufferBindingType::Uniform, + }, + }, + }; + static constexpr wgpu::BindGroupLayoutDescriptor bindGroupLayoutDescriptor{ + .label = "TexCopyConv Bind Group Layout", + .entryCount = bindGroupLayoutEntries.size(), + .entries = bindGroupLayoutEntries.data(), + }; + g_bindGroupLayout = g_device.CreateBindGroupLayout(&bindGroupLayoutDescriptor); + + static constexpr std::array depthBindGroupLayoutEntries{ + wgpu::BindGroupLayoutEntry{ + .binding = 0, + .visibility = wgpu::ShaderStage::Fragment, + .texture = + wgpu::TextureBindingLayout{ + .sampleType = wgpu::TextureSampleType::Depth, + .viewDimension = wgpu::TextureViewDimension::e2D, + }, + }, + wgpu::BindGroupLayoutEntry{ + .binding = 1, + .visibility = wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment, + .buffer = + wgpu::BufferBindingLayout{ + .type = wgpu::BufferBindingType::Uniform, + }, + }, + }; + static constexpr wgpu::BindGroupLayoutDescriptor depthBindGroupLayoutDescriptor{ + .label = "TexCopyConv Depth Bind Group Layout", + .entryCount = depthBindGroupLayoutEntries.size(), + .entries = depthBindGroupLayoutEntries.data(), + }; + g_depthBindGroupLayout = g_device.CreateBindGroupLayout(&depthBindGroupLayoutDescriptor); + + g_blitPipeline = create_pipeline( + {GX_TF_RGBA8, FragPassthrough, webgpu::g_graphicsConfig.surfaceConfiguration.format, "TexCopyConv Blit"}, + ShaderPreamble, g_bindGroupLayout); + for (const auto& conv : ConvPipelines) { + g_pipelines[conv.fmt] = create_pipeline(conv, ShaderPreamble, g_bindGroupLayout); + if (conv.outputFormat != to_wgpu(conv.fmt)) { + Log.fatal("Output format mismatch for {}", conv.fmt); + } + } + for (const auto& conv : DepthConvPipelines) { + g_pipelines[conv.fmt] = create_pipeline(conv, DepthShaderPreamble, g_depthBindGroupLayout); + if (conv.outputFormat != to_wgpu(conv.fmt)) { + Log.fatal("Output format mismatch for {}", conv.fmt); + } + } + + static constexpr wgpu::SamplerDescriptor nearestSamplerDescriptor{ + .label = "TexCopyConv Nearest Sampler", + .magFilter = wgpu::FilterMode::Nearest, + .minFilter = wgpu::FilterMode::Nearest, + }; + g_nearestSampler = g_device.CreateSampler(&nearestSamplerDescriptor); + + static constexpr wgpu::SamplerDescriptor linearSamplerDescriptor{ + .label = "TexCopyConv Linear Sampler", + .magFilter = wgpu::FilterMode::Linear, + .minFilter = wgpu::FilterMode::Linear, + }; + g_linearSampler = g_device.CreateSampler(&linearSamplerDescriptor); +} + +void shutdown() { + g_pipelines.clear(); + g_blitPipeline = {}; + g_bindGroupLayout = {}; + g_depthBindGroupLayout = {}; + g_nearestSampler = {}; + g_linearSampler = {}; +} + +static void execute(const wgpu::CommandEncoder& cmd, const ConvRequest& req, const wgpu::RenderPipeline& pipeline) { + wgpu::BindGroup bindGroup; + if (gx::is_depth_format(req.fmt)) { + const std::array bindGroupEntries{ + wgpu::BindGroupEntry{ + .binding = 0, + .textureView = req.srcView, + }, + wgpu::BindGroupEntry{ + .binding = 1, + .buffer = g_uniformBuffer, + .offset = req.uniformRange.offset, + .size = req.uniformRange.size, + }, + }; + const wgpu::BindGroupDescriptor bindGroupDescriptor{ + .layout = g_depthBindGroupLayout, + .entryCount = bindGroupEntries.size(), + .entries = bindGroupEntries.data(), + }; + bindGroup = g_device.CreateBindGroup(&bindGroupDescriptor); + } else { + const auto& sampler = req.sampleFilter == SampleFilter::Linear ? g_linearSampler : g_nearestSampler; + const std::array bindGroupEntries{ + wgpu::BindGroupEntry{ + .binding = 0, + .sampler = sampler, + }, + wgpu::BindGroupEntry{ + .binding = 1, + .textureView = req.srcView, + }, + wgpu::BindGroupEntry{ + .binding = 2, + .buffer = g_uniformBuffer, + .offset = req.uniformRange.offset, + .size = req.uniformRange.size, + }, + }; + const wgpu::BindGroupDescriptor bindGroupDescriptor{ + .layout = g_bindGroupLayout, + .entryCount = bindGroupEntries.size(), + .entries = bindGroupEntries.data(), + }; + bindGroup = g_device.CreateBindGroup(&bindGroupDescriptor); + } + + const std::array colorAttachments{ + wgpu::RenderPassColorAttachment{ + .view = req.dst->attachmentTextureView, + .loadOp = wgpu::LoadOp::Clear, + .storeOp = wgpu::StoreOp::Store, + .clearValue = {0.0, 0.0, 0.0, 0.0}, + }, + }; + const wgpu::RenderPassDescriptor renderPassDescriptor{ + .label = "TexCopyConv Pass", + .colorAttachmentCount = colorAttachments.size(), + .colorAttachments = colorAttachments.data(), + }; + const auto pass = cmd.BeginRenderPass(&renderPassDescriptor); + pass.SetPipeline(pipeline); + pass.SetBindGroup(0, bindGroup); + pass.Draw(3); + pass.End(); +} + +void run(const wgpu::CommandEncoder& cmd, const ConvRequest& req) { + const auto it = g_pipelines.find(req.fmt); + if (it == g_pipelines.end()) { + Log.fatal("No copy conversion pipeline for format {}", static_cast(req.fmt)); + } + execute(cmd, req, it->second); +} + +void blit(const wgpu::CommandEncoder& cmd, const ConvRequest& req) { execute(cmd, req, g_blitPipeline); } + +} // namespace aurora::gfx::tex_copy_conv diff --git a/aurora-main/lib/gfx/tex_copy_conv.hpp b/aurora-main/lib/gfx/tex_copy_conv.hpp new file mode 100644 index 0000000..29965be --- /dev/null +++ b/aurora-main/lib/gfx/tex_copy_conv.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "common.hpp" + +#include + +namespace aurora::gfx::tex_copy_conv { + +enum class SampleFilter : uint8_t { + Nearest, + Linear, +}; + +struct ConvRequest { + GXTexFmt fmt; + wgpu::TextureView srcView; // View of resolved EFB / offscreen color/depth + Range uniformRange; // UV transform uniform (offset + scale) + TextureHandle dst; // Destination texture + SampleFilter sampleFilter = SampleFilter::Nearest; + bool forceOpaqueAlpha = false; +}; + +bool needs_conversion(GXTexFmt fmt); + +void initialize(); +void shutdown(); +void run(const wgpu::CommandEncoder& cmd, const ConvRequest& req); +void blit(const wgpu::CommandEncoder& cmd, const ConvRequest& req); + +} // namespace aurora::gfx::tex_copy_conv diff --git a/aurora-main/lib/gfx/tex_copy_format_contract.hpp b/aurora-main/lib/gfx/tex_copy_format_contract.hpp new file mode 100644 index 0000000..be10c9a --- /dev/null +++ b/aurora-main/lib/gfx/tex_copy_format_contract.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +namespace aurora::gfx::tex_copy_conv::detail { + +// GX_TF_Z16 selects the depth RA8 copy format: the encoder writes alpha (255) then the high-Z +// byte, so sampling as IA8 gives high-Z as intensity. Middle and low depth bytes are not stored. +constexpr std::array z16_ra8_as_ia8_rgba(std::uint8_t high) noexcept { + return {high, high, high, 0xff}; +} + +inline constexpr std::string_view Z16FragmentShader = R"( +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + let depth_bytes = sample_depth_copy(in.uv); + let i = f32(depth_bytes.r) / 255.0; + return vec4f(i, i, i, 1.0); +} +)"; + +} // namespace aurora::gfx::tex_copy_conv::detail diff --git a/aurora-main/lib/gfx/tex_palette_conv.cpp b/aurora-main/lib/gfx/tex_palette_conv.cpp new file mode 100644 index 0000000..842cca3 --- /dev/null +++ b/aurora-main/lib/gfx/tex_palette_conv.cpp @@ -0,0 +1,260 @@ +#include "tex_palette_conv.hpp" + +#include "../internal.hpp" +#include "../webgpu/gpu.hpp" +#include "texture.hpp" + +#include + +namespace aurora::gfx::tex_palette_conv { +static Module Log("aurora::gfx::tex_palette_conv"); + +using webgpu::g_device; + +static constexpr std::string_view ShaderPreambleVtx = R"( +struct VertexOutput { + @builtin(position) pos: vec4f, + @location(0) uv: vec2f, +}; + +var positions: array = array( + vec2f(-1.0, 1.0), + vec2f(-1.0, -3.0), + vec2f(3.0, 1.0), +); +var uvs: array = array( + vec2f(0.0, 0.0), + vec2f(0.0, 2.0), + vec2f(2.0, 0.0), +); + +@vertex fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput { + var out: VertexOutput; + out.pos = vec4f(positions[vi], 0.0, 1.0); + out.uv = uvs[vi]; + return out; +} + +fn intensity(rgb: vec3f) -> f32 { + // ITU-R BT.601 luma coefficients + return dot(rgb, vec3f(0.257, 0.504, 0.098)) + 16.0 / 255.0; +} +)"sv; + +// Direct: R16Sint index texture + TLUT -> RGBA8 +static constexpr std::string_view ShaderDirect = R"( +@group(0) @binding(0) var src_samp: sampler; +@group(0) @binding(1) var src: texture_2d; +@group(0) @binding(2) var tlut: texture_2d; + +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + let texSize = vec2f(textureDimensions(src)); + let coord = vec2i(floor(in.uv * texSize)); + let idx = textureLoad(src, coord, 0).r; + return textureLoad(tlut, vec2i(idx, 0), 0); +} +)"sv; + +// FromFloat8: f32 texture (R8Unorm) -> 8-bit index -> TLUT -> RGBA8 +static constexpr std::string_view ShaderFromFloat8 = R"( +@group(0) @binding(0) var src_samp: sampler; +@group(0) @binding(1) var src: texture_2d; +@group(0) @binding(2) var tlut: texture_2d; + +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + let texSize = vec2f(textureDimensions(src)); + let coord = vec2i(floor(in.uv * texSize)); + let r = textureLoad(src, coord, 0).r; + return textureLoad(tlut, vec2i(i32(r * 255.0), 0), 0); +} +)"sv; + +// FromFloat4: f32 texture (R8Unorm) -> 4-bit index -> TLUT -> RGBA8 +static constexpr std::string_view ShaderFromFloat4 = R"( +@group(0) @binding(0) var src_samp: sampler; +@group(0) @binding(1) var src: texture_2d; +@group(0) @binding(2) var tlut: texture_2d; + +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + let texSize = vec2f(textureDimensions(src)); + let coord = vec2i(floor(in.uv * texSize)); + let r = textureLoad(src, coord, 0).r; + return textureLoad(tlut, vec2i(i32(r * 15.0), 0), 0); +} +)"sv; + +struct PipelineInfo { + wgpu::RenderPipeline pipeline; + wgpu::BindGroupLayout bindGroupLayout; +}; + +static PipelineInfo g_directPipeline; +static PipelineInfo g_fromFloat8Pipeline; +static PipelineInfo g_fromFloat4Pipeline; +static wgpu::Sampler g_sampler; + +static PipelineInfo create_pipeline(std::string_view fragBindingsAndShader, wgpu::TextureSampleType srcSampleType, + const char* label) { + std::string shaderSource; + shaderSource.reserve(ShaderPreambleVtx.size() + fragBindingsAndShader.size()); + shaderSource += ShaderPreambleVtx; + shaderSource += fragBindingsAndShader; + + wgpu::ShaderSourceWGSL wgslSource{}; + wgslSource.code = shaderSource.c_str(); + const wgpu::ShaderModuleDescriptor moduleDescriptor{ + .nextInChain = &wgslSource, + .label = label, + }; + auto module = g_device.CreateShaderModule(&moduleDescriptor); + + const std::array bindGroupLayoutEntries{ + wgpu::BindGroupLayoutEntry{ + .binding = 0, + .visibility = wgpu::ShaderStage::Fragment, + .sampler = + wgpu::SamplerBindingLayout{ + .type = wgpu::SamplerBindingType::NonFiltering, + }, + }, + wgpu::BindGroupLayoutEntry{ + .binding = 1, + .visibility = wgpu::ShaderStage::Fragment, + .texture = + wgpu::TextureBindingLayout{ + .sampleType = srcSampleType, + .viewDimension = wgpu::TextureViewDimension::e2D, + }, + }, + wgpu::BindGroupLayoutEntry{ + .binding = 2, + .visibility = wgpu::ShaderStage::Fragment, + .texture = + wgpu::TextureBindingLayout{ + .sampleType = wgpu::TextureSampleType::Float, + .viewDimension = wgpu::TextureViewDimension::e2D, + }, + }, + }; + const auto bindGroupLayoutLabel = fmt::format("{} Bind Group Layout", label); + const wgpu::BindGroupLayoutDescriptor bindGroupLayoutDescriptor{ + .label = bindGroupLayoutLabel.c_str(), + .entryCount = bindGroupLayoutEntries.size(), + .entries = bindGroupLayoutEntries.data(), + }; + auto bindGroupLayout = g_device.CreateBindGroupLayout(&bindGroupLayoutDescriptor); + + const wgpu::PipelineLayoutDescriptor layoutDescriptor{ + .bindGroupLayoutCount = 1, + .bindGroupLayouts = &bindGroupLayout, + }; + auto pipelineLayout = g_device.CreatePipelineLayout(&layoutDescriptor); + + constexpr std::array colorTargets{wgpu::ColorTargetState{ + .format = wgpu::TextureFormat::RGBA8Unorm, + }}; + const wgpu::FragmentState fragmentState{ + .module = module, + .entryPoint = "fs_main", + .targetCount = colorTargets.size(), + .targets = colorTargets.data(), + }; + const wgpu::RenderPipelineDescriptor pipelineDescriptor{ + .label = label, + .layout = pipelineLayout, + .vertex = + wgpu::VertexState{ + .module = module, + .entryPoint = "vs_main", + }, + .primitive = + wgpu::PrimitiveState{ + .topology = wgpu::PrimitiveTopology::TriangleList, + }, + .fragment = &fragmentState, + }; + + return PipelineInfo{ + .pipeline = g_device.CreateRenderPipeline(&pipelineDescriptor), + .bindGroupLayout = std::move(bindGroupLayout), + }; +} + +static const PipelineInfo& pipeline_for_variant(Variant variant) { + switch (variant) { + case Variant::Direct: + return g_directPipeline; + case Variant::FromFloat8: + return g_fromFloat8Pipeline; + case Variant::FromFloat4: + return g_fromFloat4Pipeline; + } + FATAL("invalid palette conv variant {}", static_cast(variant)); +} + +void initialize() { + g_directPipeline = create_pipeline(ShaderDirect, wgpu::TextureSampleType::Sint, "TexPaletteConv Direct"); + g_fromFloat8Pipeline = + create_pipeline(ShaderFromFloat8, wgpu::TextureSampleType::UnfilterableFloat, "TexPaletteConv FromFloat8"); + g_fromFloat4Pipeline = + create_pipeline(ShaderFromFloat4, wgpu::TextureSampleType::UnfilterableFloat, "TexPaletteConv FromFloat4"); + constexpr wgpu::SamplerDescriptor samplerDesc{ + .label = "TexPaletteConv Sampler", + .magFilter = wgpu::FilterMode::Nearest, + .minFilter = wgpu::FilterMode::Nearest, + }; + g_sampler = g_device.CreateSampler(&samplerDesc); +} + +void shutdown() { + g_directPipeline = {}; + g_fromFloat8Pipeline = {}; + g_fromFloat4Pipeline = {}; + g_sampler = {}; +} + +void run(const wgpu::CommandEncoder& cmd, const ConvRequest& req) { + const auto& [pipeline, bindGroupLayout] = pipeline_for_variant(req.variant); + + const std::array bindGroupEntries{ + wgpu::BindGroupEntry{ + .binding = 0, + .sampler = g_sampler, + }, + wgpu::BindGroupEntry{ + .binding = 1, + .textureView = req.src->sampleTextureView, + }, + wgpu::BindGroupEntry{ + .binding = 2, + .textureView = req.tlut->sampleTextureView, + }, + }; + const wgpu::BindGroupDescriptor bindGroupDescriptor{ + .layout = bindGroupLayout, + .entryCount = bindGroupEntries.size(), + .entries = bindGroupEntries.data(), + }; + const auto bindGroup = g_device.CreateBindGroup(&bindGroupDescriptor); + + const std::array colorAttachments{ + wgpu::RenderPassColorAttachment{ + .view = req.dst->attachmentTextureView, + .loadOp = wgpu::LoadOp::Clear, + .storeOp = wgpu::StoreOp::Store, + .clearValue = {0.0, 0.0, 0.0, 0.0}, + }, + }; + const wgpu::RenderPassDescriptor renderPassDescriptor{ + .label = "TexPaletteConv Pass", + .colorAttachmentCount = colorAttachments.size(), + .colorAttachments = colorAttachments.data(), + }; + const auto pass = cmd.BeginRenderPass(&renderPassDescriptor); + pass.SetPipeline(pipeline); + pass.SetBindGroup(0, bindGroup); + pass.Draw(3); + pass.End(); +} + +} // namespace aurora::gfx::tex_palette_conv diff --git a/aurora-main/lib/gfx/tex_palette_conv.hpp b/aurora-main/lib/gfx/tex_palette_conv.hpp new file mode 100644 index 0000000..fca8a38 --- /dev/null +++ b/aurora-main/lib/gfx/tex_palette_conv.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "common.hpp" + +namespace aurora::gfx::tex_palette_conv { + +enum class Variant : uint8_t { + Direct, // R16Sint index texture -> TLUT -> RGBA8 + FromFloat8, // f32 texture, R channel * 255 -> 8-bit index -> TLUT -> RGBA8 + FromFloat4, // f32 texture, R channel * 15 -> 4-bit index -> TLUT -> RGBA8 +}; + +struct ConvRequest { + Variant variant; + TextureHandle src; + TextureHandle dst; + TextureHandle tlut; +}; + +void initialize(); +void shutdown(); +void run(const wgpu::CommandEncoder& cmd, const ConvRequest& req); + +} // namespace aurora::gfx::tex_palette_conv diff --git a/aurora-main/lib/gfx/texture.cpp b/aurora-main/lib/gfx/texture.cpp new file mode 100644 index 0000000..f465f36 --- /dev/null +++ b/aurora-main/lib/gfx/texture.cpp @@ -0,0 +1,303 @@ +#include "common.hpp" + +#include "../internal.hpp" +#include "../webgpu/gpu.hpp" +#include "aurora/aurora.h" +#include "texture.hpp" +#include "texture_convert.hpp" +#include "../gx/gx_fmt.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace aurora::gfx { +using webgpu::g_device; +using webgpu::g_queue; + +namespace { +Module Log("aurora::gfx"); + +constexpr u32 div_ceil(u32 value, u32 divisor) noexcept { return (value + divisor - 1) / divisor; } + +uint32_t clamp_mip_count(uint32_t width, uint32_t height, uint32_t mips, const char* label) noexcept { + const uint32_t maxMips = max_texture_mip_count(width, height); + const uint32_t safeMips = std::clamp(mips, 1u, maxMips); + if (safeMips != mips) { + Log.warn("{}: clamping mip count {} to {} for {}x{} texture", label, mips, safeMips, width, height); + } + return safeMips; +} + +wgpu::Extent3D physical_size(wgpu::Extent3D size, TextureFormatInfo info) { + const uint32_t width = ((size.width + info.blockWidth - 1) / info.blockWidth) * info.blockWidth; + const uint32_t height = ((size.height + info.blockHeight - 1) / info.blockHeight) * info.blockHeight; + return {.width = width, .height = height, .depthOrArrayLayers = size.depthOrArrayLayers}; +} +} // namespace + +TextureFormatInfo format_info(wgpu::TextureFormat format) noexcept { + switch (format) { + DEFAULT_FATAL("unimplemented texture format {}", magic_enum::enum_name(format)); + case wgpu::TextureFormat::R8Unorm: + return {1, 1, 1, false}; + case wgpu::TextureFormat::RG8Unorm: + case wgpu::TextureFormat::R16Sint: + return {1, 1, 2, false}; + case wgpu::TextureFormat::RGBA8Unorm: + case wgpu::TextureFormat::BGRA8Unorm: + case wgpu::TextureFormat::R32Float: + return {1, 1, 4, false}; + case wgpu::TextureFormat::BC1RGBAUnorm: + return {4, 4, 8, true}; + case wgpu::TextureFormat::BC3RGBAUnorm: + case wgpu::TextureFormat::BC5RGUnorm: + case wgpu::TextureFormat::BC7RGBAUnorm: + return {4, 4, 16, true}; + } +} + +uint64_t calc_texture_size(wgpu::TextureFormat format, u32 width, u32 height, u32 mips) noexcept { + const auto info = format_info(format); + uint64_t total = 0; + for (uint32_t mip = 0; mip < mips; ++mip) { + const uint32_t mipWidth = std::max(width >> mip, 1u); + const uint32_t mipHeight = std::max(height >> mip, 1u); + const uint64_t widthBlocks = div_ceil(mipWidth, info.blockWidth); + const uint64_t heightBlocks = div_ceil(mipHeight, info.blockHeight); + const uint64_t mipBytes = widthBlocks * heightBlocks * info.blockSize; + total += mipBytes; + } + return total; +} + +TextureHandle new_static_texture_2d(uint32_t width, uint32_t height, uint32_t mips, u32 format, ArrayRef data, + bool tlut, const char* label) noexcept { + ZoneScoped; + + mips = clamp_mip_count(width, height, mips, label); + auto handle = new_dynamic_texture_2d(width, height, mips, format, label); + auto& ref = *handle; + + ConvertedTexture converted; + if (ref.gxFormat != InvalidTextureFormat) { + if (tlut) { + CHECK(ref.size.height == 1, "new_static_texture_2d[{}]: expected tlut height 1, got {}", label, ref.size.height); + CHECK(ref.mipCount == 1, "new_static_texture_2d[{}]: expected tlut mipCount 1, got {}", label, ref.mipCount); + converted = convert_tlut(ref.gxFormat, ref.size.width, data); + } else { + converted = convert_texture(ref.gxFormat, ref.size.width, ref.size.height, ref.mipCount, data); + } + if (!converted.data.empty()) { + data = converted.data; + ref.hasArbitraryMips = converted.hasArbitraryMips; + } + } + + uint32_t offset = 0; + for (uint32_t mip = 0; mip < mips; ++mip) { + const wgpu::Extent3D mipSize{ + .width = std::max(ref.size.width >> mip, 1u), + .height = std::max(ref.size.height >> mip, 1u), + .depthOrArrayLayers = ref.size.depthOrArrayLayers, + }; + const auto info = format_info(ref.format); + const auto physicalSize = physical_size(mipSize, info); + const uint32_t widthBlocks = physicalSize.width / info.blockWidth; + const uint32_t heightBlocks = physicalSize.height / info.blockHeight; + const uint32_t bytesPerRow = widthBlocks * info.blockSize; + const uint32_t dataSize = bytesPerRow * heightBlocks * mipSize.depthOrArrayLayers; + if (offset > data.size() || dataSize > data.size() - offset) { + Log.warn("new_static_texture_2d[{}]: stopping at mip {}: expected at least {} bytes, got {}", label, mip, + static_cast(offset) + dataSize, data.size()); + return handle; + } + const wgpu::TexelCopyTextureInfo dstView{ + .texture = ref.texture, + .mipLevel = mip, + }; + if constexpr (UseTextureBuffer) { + const auto range = push_texture_data(data.data() + offset, dataSize, bytesPerRow, heightBlocks); + const wgpu::TexelCopyBufferLayout dataLayout{ + .offset = range.offset, + .bytesPerRow = bytesPerRow, + .rowsPerImage = heightBlocks, + }; + g_textureUploads.emplace_back(dataLayout, std::move(dstView), physicalSize); + } else { + const wgpu::TexelCopyBufferLayout dataLayout{ + .bytesPerRow = bytesPerRow, + .rowsPerImage = heightBlocks, + }; + g_queue.WriteTexture(&dstView, data.data() + offset, dataSize, &dataLayout, &physicalSize); + } + offset += dataSize; + } + if (data.size() != UINT32_MAX && offset < data.size()) { + Log.warn("new_static_texture_2d[{}]: texture used {} bytes, but given {} bytes", label, offset, data.size()); + } + return handle; +} + +TextureHandle new_dynamic_texture_2d(uint32_t width, uint32_t height, uint32_t mips, u32 gxFormat, + const char* label) noexcept { + ZoneScopedS(3); + mips = clamp_mip_count(width, height, mips, label); + const auto wgpuFormat = to_wgpu(gxFormat); + const wgpu::Extent3D size{ + .width = width, + .height = height, + .depthOrArrayLayers = 1, + }; + const wgpu::TextureDescriptor textureDescriptor{ + .label = label, + .usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopyDst, + .dimension = wgpu::TextureDimension::e2D, + .size = size, + .format = wgpuFormat, + .mipLevelCount = mips, + .sampleCount = 1, + }; + auto texture = g_device.CreateTexture(&textureDescriptor); + const auto viewLabel = fmt::format("{} view", label); + wgpu::TextureViewDescriptor textureViewDescriptor{ + .label = viewLabel.c_str(), + .format = wgpuFormat, + .dimension = wgpu::TextureViewDimension::e2D, + .mipLevelCount = mips, + }; + auto textureView = texture.CreateView(&textureViewDescriptor); + return std::make_shared(std::move(texture), std::move(textureView), wgpu::TextureView{}, size, wgpuFormat, + mips, gxFormat); +} + +TextureHandle new_render_texture(uint32_t width, uint32_t height, u32 gxFormat, const char* label) noexcept { + ZoneScoped; + + const auto wgpuFormat = webgpu::g_graphicsConfig.surfaceConfiguration.format; + const wgpu::Extent3D size{ + .width = width, + .height = height, + .depthOrArrayLayers = 1, + }; + const wgpu::TextureDescriptor textureDescriptor{ + .label = label, + .usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::CopyDst | + wgpu::TextureUsage::RenderAttachment, + .dimension = wgpu::TextureDimension::e2D, + .size = size, + .format = wgpuFormat, + .mipLevelCount = 1, + .sampleCount = 1, + }; + auto texture = g_device.CreateTexture(&textureDescriptor); + + // Create texture view for color attachments + const auto viewLabel = fmt::format("{} view", label); + wgpu::TextureViewDescriptor textureViewDescriptor{ + .label = viewLabel.c_str(), + .format = wgpuFormat, + .dimension = wgpu::TextureViewDimension::e2D, + }; + auto attachmentTextureView = texture.CreateView(&textureViewDescriptor); + wgpu::TextureView sampleTextureView = attachmentTextureView; + return std::make_shared(std::move(texture), std::move(sampleTextureView), + std::move(attachmentTextureView), size, wgpuFormat, 1, gxFormat); +} + +TextureHandle new_conv_texture(uint32_t width, uint32_t height, u32 gxFormat, const char* label) noexcept { + ZoneScoped; + + const auto wgpuFormat = to_wgpu(gxFormat); + const wgpu::Extent3D size{ + .width = width, + .height = height, + .depthOrArrayLayers = 1, + }; + const wgpu::TextureDescriptor textureDescriptor{ + .label = label, + .usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc | + wgpu::TextureUsage::RenderAttachment, + .dimension = wgpu::TextureDimension::e2D, + .size = size, + .format = wgpuFormat, + .mipLevelCount = 1, + .sampleCount = 1, + }; + auto texture = g_device.CreateTexture(&textureDescriptor); + + // Create texture view for color attachments + const auto viewLabel = fmt::format("{} view", label); + wgpu::TextureViewDescriptor textureViewDescriptor{ + .label = viewLabel.c_str(), + .format = wgpuFormat, + .dimension = wgpu::TextureViewDimension::e2D, + }; + auto attachmentTextureView = texture.CreateView(&textureViewDescriptor); + wgpu::TextureView sampleTextureView = attachmentTextureView; + return std::make_shared(std::move(texture), std::move(sampleTextureView), + std::move(attachmentTextureView), size, wgpuFormat, 1, gxFormat); +} + +void write_texture(TextureRef& ref, ArrayRef data) noexcept { + ZoneScoped; + + ConvertedTexture converted; + if (ref.gxFormat != InvalidTextureFormat) { + converted = convert_texture(ref.gxFormat, ref.size.width, ref.size.height, ref.mipCount, data); + ref.hasArbitraryMips = converted.hasArbitraryMips; + if (!converted.data.empty()) { + data = converted.data; + } + } + + uint32_t offset = 0; + for (uint32_t mip = 0; mip < ref.mipCount; ++mip) { + const wgpu::Extent3D mipSize{ + .width = std::max(ref.size.width >> mip, 1u), + .height = std::max(ref.size.height >> mip, 1u), + .depthOrArrayLayers = ref.size.depthOrArrayLayers, + }; + const auto info = format_info(ref.format); + const auto physicalSize = physical_size(mipSize, info); + const uint32_t widthBlocks = physicalSize.width / info.blockWidth; + const uint32_t heightBlocks = physicalSize.height / info.blockHeight; + const uint32_t bytesPerRow = widthBlocks * info.blockSize; + const uint32_t dataSize = bytesPerRow * heightBlocks * mipSize.depthOrArrayLayers; + if (offset > data.size() || dataSize > data.size() - offset) { + Log.warn("write_texture: stopping at mip {}: expected at least {} bytes, got {}", mip, + static_cast(offset) + dataSize, data.size()); + return; + } + const wgpu::TexelCopyTextureInfo dstView{ + .texture = ref.texture, + .mipLevel = mip, + }; + if constexpr (UseTextureBuffer) { + const auto range = push_texture_data(data.data() + offset, dataSize, bytesPerRow, heightBlocks); + const wgpu::TexelCopyBufferLayout dataLayout{ + .offset = range.offset, + .bytesPerRow = bytesPerRow, + .rowsPerImage = heightBlocks, + }; + g_textureUploads.emplace_back(dataLayout, std::move(dstView), physicalSize); + } else { + const wgpu::TexelCopyBufferLayout dataLayout{ + .bytesPerRow = bytesPerRow, + .rowsPerImage = heightBlocks, + }; + g_queue.WriteTexture(&dstView, data.data() + offset, dataSize, &dataLayout, &physicalSize); + } + offset += dataSize; + } + if (data.size() != UINT32_MAX && offset < data.size()) { + Log.warn("write_texture: texture used {} bytes, but given {} bytes", offset, data.size()); + } +} +} // namespace aurora::gfx diff --git a/aurora-main/lib/gfx/texture.hpp b/aurora-main/lib/gfx/texture.hpp new file mode 100644 index 0000000..4c1d4cc --- /dev/null +++ b/aurora-main/lib/gfx/texture.hpp @@ -0,0 +1,147 @@ +#pragma once +#include + +#include + +#include "common.hpp" + +namespace aurora::gfx { +constexpr uint32_t max_texture_mip_count(uint32_t width, uint32_t height) noexcept { + uint32_t dimension = std::max(width, height); + uint32_t count = 1; + while (dimension > 1) { + dimension >>= 1; + ++count; + } + return count; +} + +struct TextureUpload { + wgpu::TexelCopyBufferLayout layout; + wgpu::TexelCopyTextureInfo tex; + wgpu::Extent3D size; + + TextureUpload(wgpu::TexelCopyBufferLayout layout, wgpu::TexelCopyTextureInfo tex, wgpu::Extent3D size) noexcept + : layout(layout), tex(std::move(tex)), size(size) {} +}; +extern std::vector g_textureUploads; + +struct TextureFormatInfo { + uint8_t blockWidth; + uint8_t blockHeight; + uint8_t blockSize; + bool compressed; +}; +TextureFormatInfo format_info(wgpu::TextureFormat format) noexcept; +uint64_t calc_texture_size(wgpu::TextureFormat format, uint32_t width, uint32_t height, uint32_t mips) noexcept; + +constexpr u32 InvalidTextureFormat = -1; +struct TextureRef { + wgpu::Texture texture; + wgpu::TextureView sampleTextureView; + wgpu::TextureView attachmentTextureView; + wgpu::Extent3D size; + wgpu::TextureFormat format; + uint32_t mipCount; + u32 gxFormat; + bool hasArbitraryMips = false; + bool isReplacement = false; + + TextureRef(wgpu::Texture texture, wgpu::TextureView sampleTextureView, wgpu::TextureView attachmentTextureView, + wgpu::Extent3D size, wgpu::TextureFormat format, uint32_t mipCount, u32 gxFormat) + : texture(std::move(texture)) + , sampleTextureView(std::move(sampleTextureView)) + , attachmentTextureView(std::move(attachmentTextureView)) + , size(size) + , format(format) + , mipCount(mipCount) + , gxFormat(gxFormat) {} +}; + +TextureHandle new_static_texture_2d(uint32_t width, uint32_t height, uint32_t mips, u32 gxFormat, + ArrayRef data, bool tlut, const char* label) noexcept; +TextureHandle new_dynamic_texture_2d(uint32_t width, uint32_t height, uint32_t mips, u32 gxFormat, + const char* label) noexcept; +TextureHandle new_render_texture(uint32_t width, uint32_t height, u32 gxFormat, const char* label) noexcept; +TextureHandle new_conv_texture(uint32_t width, uint32_t height, u32 gxFormat, const char* label) noexcept; +void write_texture(TextureRef& ref, ArrayRef data) noexcept; +}; // namespace aurora::gfx + +struct GXTexObj_ { + u32 mode0 = 0; + u32 mode1 = 0; + u32 image0 = UINT32_MAX; + u32 image3 = 0; + const void* userData = nullptr; + const void* data = nullptr; + u32 mWidth = 0; + u32 mHeight = 0; + u32 mFormat = aurora::gfx::InvalidTextureFormat; + GXTlut tlut = GX_TLUT0; + u32 texObjId = 0; + u32 texDataVersion = 0; + u8 flags = 0; + + static constexpr u32 get_bits(u32 reg, u32 size, u32 shift) noexcept { return (reg >> shift) & ((1u << size) - 1); } + + u32 width() const noexcept { return mWidth != 0 ? mWidth : get_bits(image0, 10, 0) + 1 & 0x3FF; } + u32 height() const noexcept { return mHeight != 0 ? mHeight : get_bits(image0, 10, 10) + 1 & 0x3FF; } + u32 raw_format() const noexcept { return get_bits(image0, 4, 20); } + u32 format() const noexcept { return mFormat != aurora::gfx::InvalidTextureFormat ? mFormat : raw_format(); } + GXTexWrapMode wrap_s() const noexcept { return static_cast(get_bits(mode0, 2, 0)); } + GXTexWrapMode wrap_t() const noexcept { return static_cast(get_bits(mode0, 2, 2)); } + GXTexFilter min_filter() const noexcept { + constexpr GXTexFilter kHwToGxFilter[8] = { + GX_NEAR, GX_NEAR_MIP_NEAR, GX_NEAR_MIP_LIN, GX_NEAR, GX_LINEAR, GX_LIN_MIP_NEAR, GX_LIN_MIP_LIN, GX_NEAR, + }; + return kHwToGxFilter[get_bits(mode0, 3, 5)]; + } + GXTexFilter mag_filter() const noexcept { return get_bits(mode0, 1, 4) != 0 ? GX_LINEAR : GX_NEAR; } + GXBool has_mips() const noexcept { return (flags & 1u) != 0 ? GX_TRUE : GX_FALSE; } + u32 mip_count() const noexcept { + if (!has_mips()) { + return 1; + } + const u32 requested = std::max(static_cast(max_lod()) + 1, 1u); + return std::min(requested, aurora::gfx::max_texture_mip_count(width(), height())); + } + GXBool do_edge_lod() const noexcept { return get_bits(mode0, 1, 8) == 0 ? GX_TRUE : GX_FALSE; } + float lod_bias() const noexcept { return static_cast(static_cast(get_bits(mode0, 8, 9))) / 32.0f; } + GXAnisotropy max_aniso() const noexcept { return static_cast(get_bits(mode0, 2, 19)); } + GXBool bias_clamp() const noexcept { return get_bits(mode0, 1, 21) != 0 ? GX_TRUE : GX_FALSE; } + float min_lod() const noexcept { return static_cast(get_bits(mode1, 8, 0)) / 16.0f; } + float max_lod() const noexcept { return static_cast(get_bits(mode1, 8, 8)) / 16.0f; } + + // Custom flag for texture caching + bool no_cache() const noexcept { return (flags & 0x80) != 0; } + void set_no_cache(bool value) noexcept { flags = value ? flags | 0x80 : flags & ~0x80; } +}; +static_assert(sizeof(GXTexObj_) <= sizeof(GXTexObj), "GXTexObj too small!"); +struct GXTlutObj_ { + u32 tlut = 0; + u32 loadTlut0 = 0; + u16 numEntries = 0; + const void* data = nullptr; + GXTlutFmt format = GX_TL_IA8; + u32 tlutObjId = 0; + u32 tlutDataVersion = 0; + u8 flags = 0; + + // Custom flag for texture caching + bool no_cache() const noexcept { return (flags & 0x80) != 0; } + void set_no_cache(bool value) noexcept { flags = value ? flags | 0x80 : flags & ~0x80; } +}; +static_assert(sizeof(GXTlutObj_) <= sizeof(GXTlutObj), "GXTlutObj too small!"); + +namespace aurora::gfx { +struct TextureBind { + TextureHandle ref; + GXTexObj_ texObj; + + TextureBind() noexcept = default; + TextureBind(const GXTexObj_& obj, TextureHandle handle) noexcept : ref(std::move(handle)), texObj(obj) {} + void reset() noexcept { ref.reset(); } + [[nodiscard]] wgpu::SamplerDescriptor get_descriptor() const noexcept; + operator bool() const noexcept { return ref.operator bool(); } +}; +} // namespace aurora::gfx diff --git a/aurora-main/lib/gfx/texture_convert.cpp b/aurora-main/lib/gfx/texture_convert.cpp new file mode 100644 index 0000000..f61bbeb --- /dev/null +++ b/aurora-main/lib/gfx/texture_convert.cpp @@ -0,0 +1,726 @@ +#include "texture_convert.hpp" + +#include "../internal.hpp" +#include "../gx/gx_fmt.hpp" + +#include +#include +#include +#include +#include +#include + +namespace aurora::gfx { +static Module Log("aurora::gfx"); + +struct RGBA8 { + uint8_t r; + uint8_t g; + uint8_t b; + uint8_t a; +}; + +namespace { +constexpr float kArbMipThreshold = 15.0f; + +size_t calc_size_rgba8(uint32_t width, uint32_t height) { + return static_cast(width) * static_cast(height) * sizeof(RGBA8); +} + +size_t calc_offset_rgba8(uint32_t x, uint32_t y, uint32_t width) { + return (static_cast(y) * static_cast(width) + static_cast(x)) * sizeof(RGBA8); +} + +/** + * Downscales RGBA8 data using a simple box filter. + */ +ByteBuffer downscale(const uint8_t* src, uint32_t srcWidth, uint32_t srcHeight, uint32_t dstWidth, uint32_t dstHeight) { + ByteBuffer dst{calc_size_rgba8(dstWidth, dstHeight)}; + auto* dstPixels = dst.data(); + for (uint32_t y = 0; y < dstHeight; ++y) { + const uint32_t srcY0 = std::min(y * 2, srcHeight - 1); + const uint32_t srcY1 = std::min(srcY0 + 1, srcHeight - 1); + for (uint32_t x = 0; x < dstWidth; ++x) { + const uint32_t srcX0 = std::min(x * 2, srcWidth - 1); + const uint32_t srcX1 = std::min(srcX0 + 1, srcWidth - 1); + const size_t sampleOffsets[4] = { + calc_offset_rgba8(srcX0, srcY0, srcWidth), + calc_offset_rgba8(srcX1, srcY0, srcWidth), + calc_offset_rgba8(srcX0, srcY1, srcWidth), + calc_offset_rgba8(srcX1, srcY1, srcWidth), + }; + uint8_t* out = dstPixels + calc_offset_rgba8(x, y, dstWidth); + for (size_t channel = 0; channel < 4; ++channel) { + uint32_t sum = 0; + for (const size_t offset : sampleOffsets) { + sum += src[offset + channel]; + } + out[channel] = static_cast((sum + 2) / 4); + } + } + } + return dst; +} + +float avg_diff(const uint8_t* lhs, const uint8_t* rhs, uint32_t width, uint32_t height) { + double diffSum = 0.0; + const size_t byteCount = calc_size_rgba8(width, height); + for (size_t i = 0; i < byteCount; ++i) { + const int diff = static_cast(lhs[i]) - static_cast(rhs[i]); + diffSum += static_cast(diff * diff); + } + const double sampleCount = static_cast(width) * static_cast(height) * 4.0; + return static_cast(std::sqrt(diffSum / sampleCount) / 2.56); +} + +/** + * Attempts to detect whether the mipmaps of a texture are manually-authored, which is sometimes used for mipmap-based + * distance effects. This is achieved by downscaling the mipmaps using a box filter and comparing the average pixel + * difference between the original and downscaled mipmaps. + */ +bool arb_mip_check(uint32_t width, uint32_t height, uint32_t mips, ArrayRef data) { + if (mips < 2) { + return false; + } + + const uint32_t maxMips = max_texture_mip_count(width, height); + if (mips > maxMips) { + Log.warn("arb_mip_check: mip count {} exceeds the {} levels supported by {}x{}", mips, maxMips, width, height); + return false; + } + + struct MipLevel { + const uint8_t* data; + uint32_t width; + uint32_t height; + }; + std::vector levels; + levels.reserve(mips); + + size_t offset = 0; + for (uint32_t mip = 0; mip < mips; ++mip) { + const size_t mipSize = calc_size_rgba8(width, height); + if (offset > data.size() || mipSize > data.size() - offset) { + Log.warn("arb_mip_check: expected at least {} bytes, got {}", offset + mipSize, data.size()); + return false; + } + levels.push_back({data.data() + offset, width, height}); + offset += mipSize; + width = std::max(width >> 1, 1u); + height = std::max(height >> 1, 1u); + } + + ByteBuffer downscaled; + float totalDiff = 0.0f; + for (uint32_t mip = 0; mip + 1 < mips; ++mip) { + const uint8_t* src = mip == 0 ? levels[mip].data : downscaled.data(); + downscaled = downscale(src, levels[mip].width, levels[mip].height, levels[mip + 1].width, + levels[mip + 1].height); + totalDiff += avg_diff(levels[mip + 1].data, downscaled.data(), levels[mip + 1].width, + levels[mip + 1].height); + } + return (totalDiff / static_cast(mips - 1)) > kArbMipThreshold; +} +} // namespace + +// http://www.mindcontrol.org/~hplus/graphics/expand-bits.html +template +constexpr uint8_t ExpandTo8(uint8_t n) { + if constexpr (v == 3) { + return (n << (8 - 3)) | (n << (8 - 6)) | (n >> (9 - 8)); + } else { + return (n << (8 - v)) | (n >> ((v * 2) - 8)); + } +} + +constexpr uint8_t S3TCBlend(uint32_t a, uint32_t b) { + return static_cast((((a << 1) + a) + ((b << 2) + b)) >> 3); +} + +constexpr uint8_t HalfBlend(uint8_t a, uint8_t b) { + return static_cast((static_cast(a) + static_cast(b)) >> 1); +} + +static size_t ComputeMippedTexelCount(uint32_t w, uint32_t h, uint32_t mips) { + size_t ret = w * h; + for (uint32_t i = mips; i > 1; --i) { + if (w > 1) { + w /= 2; + } + if (h > 1) { + h /= 2; + } + ret += w * h; + } + return ret; +} + +static std::optional RequiredTextureDataSize(u32 format, uint32_t width, uint32_t height, uint32_t mips) { + if (width == 0 || height == 0 || mips == 0) { + return std::nullopt; + } + + uint32_t blockWidth = 1; + uint32_t blockHeight = 1; + uint32_t blockSize = 0; + switch (format) { + DEFAULT_FATAL("RequiredTextureDataSize: unknown texture format {}", format); + case GX_TF_R8_PC: + blockSize = 1; + break; + case GX_TF_RGBA8_PC: + blockSize = 4; + break; + case GX_TF_I4: + case GX_TF_C4: + case GX_TF_CMPR: + blockWidth = 8; + blockHeight = 8; + blockSize = 32; + break; + case GX_TF_I8: + case GX_TF_IA4: + case GX_TF_C8: + case GX_TF_Z8: + blockWidth = 8; + blockHeight = 4; + blockSize = 32; + break; + case GX_TF_IA8: + case GX_TF_RGB565: + case GX_TF_RGB5A3: + case GX_TF_C14X2: + case GX_TF_Z16: + blockWidth = 4; + blockHeight = 4; + blockSize = 32; + break; + case GX_TF_RGBA8: + case GX_TF_Z24X8: + blockWidth = 4; + blockHeight = 4; + blockSize = 64; + break; + } + + size_t total = 0; + for (uint32_t mip = 0; mip < mips; ++mip) { + const size_t widthBlocks = (static_cast(width) + blockWidth - 1) / blockWidth; + const size_t heightBlocks = (static_cast(height) + blockHeight - 1) / blockHeight; + if (widthBlocks > std::numeric_limits::max() / heightBlocks || + widthBlocks * heightBlocks > (std::numeric_limits::max() - total) / blockSize) { + return std::nullopt; + } + total += widthBlocks * heightBlocks * blockSize; + width = std::max(width >> 1, 1u); + height = std::max(height >> 1, 1u); + } + return total; +} + +template +concept TextureDecoder = requires(T) { + typename T::Source; + typename T::Target; + { T::Frac } -> std::convertible_to; + { T::BlockWidth } -> std::convertible_to; + { T::BlockHeight } -> std::convertible_to; + { T::decode_texel(std::declval(), std::declval(), 0u) }; +}; + +template +static ByteBuffer DecodeTiled(uint32_t width, uint32_t height, uint32_t mips, ArrayRef data) { + const size_t texelCount = ComputeMippedTexelCount(width, height, mips); + ByteBuffer buf{texelCount * sizeof(typename T::Target)}; + + uint32_t w = width; + uint32_t h = height; + auto* targetMip = reinterpret_cast(buf.data()); + const auto* in = reinterpret_cast(data.data()); + for (uint32_t mip = 0; mip < mips; ++mip) { + const uint32_t bwidth = (w + (T::BlockWidth - 1)) / T::BlockWidth; + const uint32_t bheight = (h + (T::BlockHeight - 1)) / T::BlockHeight; + for (uint32_t by = 0; by < bheight; ++by) { + const uint32_t baseY = by * T::BlockHeight; + const uint32_t numRows = std::min(h - baseY, T::BlockHeight); + for (uint32_t bx = 0; bx < bwidth; ++bx) { + const uint32_t baseX = bx * T::BlockWidth; + for (uint32_t y = 0; y < numRows; ++y) { + auto* target = targetMip + (baseY + y) * w + baseX; + const auto n = std::min(w - baseX, T::BlockWidth); + for (uint32_t x = 0; x < n; ++x) { + T::decode_texel(target, in, x); + } + in += T::BlockWidth / T::Frac; + } + const uint32_t extraY = T::BlockHeight - numRows; + in += T::BlockWidth * extraY / T::Frac; + } + } + targetMip += w * h; + if (w > 1) { + w /= 2; + } + if (h > 1) { + h /= 2; + } + } + + return buf; +} + +template +static ByteBuffer DecodeLinear(uint32_t width, ArrayRef data) { + ByteBuffer buf{width * sizeof(typename T::Target)}; + auto* target = reinterpret_cast(buf.data()); + const auto* in = reinterpret_cast(data.data()); + for (uint32_t x = 0; x < width; ++x) { + T::decode_texel(target, in, x); + } + return buf; +} + +struct TextureDecoderI4 { + using Source = uint8_t; + using Target = RGBA8; + + static constexpr uint32_t Frac = 2; + static constexpr uint32_t BlockWidth = 8; + static constexpr uint32_t BlockHeight = 8; + + static void decode_texel(Target* target, const Source* in, const uint32_t x) { + const uint8_t intensity = ExpandTo8<4>(in[x / 2] >> (x & 1 ? 0 : 4) & 0xf); + target[x].r = intensity; + target[x].g = intensity; + target[x].b = intensity; + target[x].a = intensity; + } +}; + +struct TextureDecoderI8 { + using Source = uint8_t; + using Target = RGBA8; + + static constexpr uint32_t Frac = 1; + static constexpr uint32_t BlockWidth = 8; + static constexpr uint32_t BlockHeight = 4; + + static void decode_texel(Target* target, const Source* in, const uint32_t x) { + const uint8_t intensity = in[x]; + target[x].r = intensity; + target[x].g = intensity; + target[x].b = intensity; + target[x].a = intensity; + } +}; + +struct TextureDecoderIA4 { + using Source = uint8_t; + using Target = RGBA8; + + static constexpr uint32_t Frac = 1; + static constexpr uint32_t BlockWidth = 8; + static constexpr uint32_t BlockHeight = 4; + + static void decode_texel(Target* target, const Source* in, const uint32_t x) { + const uint8_t intensity = ExpandTo8<4>(in[x] & 0xf); + target[x].r = intensity; + target[x].g = intensity; + target[x].b = intensity; + target[x].a = ExpandTo8<4>(in[x] >> 4); + } +}; + +struct TextureDecoderIA8 { + using Source = uint16_t; + using Target = RGBA8; + + static constexpr uint32_t Frac = 1; + static constexpr uint32_t BlockWidth = 4; + static constexpr uint32_t BlockHeight = 4; + + static void decode_texel(Target* target, const Source* in, const uint32_t x) { + const uint8_t intensity = in[x] >> 8; + target[x].r = intensity; + target[x].g = intensity; + target[x].b = intensity; + target[x].a = in[x] & 0xff; + } +}; + +struct TextureDecoderC4 { + using Source = uint8_t; + using Target = uint16_t; + + static constexpr uint32_t Frac = 2; + static constexpr uint32_t BlockWidth = 8; + static constexpr uint32_t BlockHeight = 8; + + static void decode_texel(Target* target, const Source* in, const uint32_t x) { + target[x] = in[x / 2] >> (x & 1 ? 0 : 4) & 0xf; + } +}; + +struct TextureDecoderC8 { + using Source = uint8_t; + using Target = uint16_t; + + static constexpr uint32_t Frac = 1; + static constexpr uint32_t BlockWidth = 8; + static constexpr uint32_t BlockHeight = 4; + + static void decode_texel(Target* target, const Source* in, const uint32_t x) { target[x] = in[x]; } +}; + +struct TextureDecoderC14X2 { + using Source = uint16_t; + using Target = uint16_t; + + static constexpr uint32_t Frac = 1; + static constexpr uint32_t BlockWidth = 4; + static constexpr uint32_t BlockHeight = 4; + + static void decode_texel(Target* target, const Source* in, const uint32_t x) { target[x] = bswap(in[x]) & 0x3fff; } +}; + +struct TextureDecoderRGB565 { + using Source = uint16_t; + using Target = RGBA8; + + static constexpr uint32_t Frac = 1; + static constexpr uint32_t BlockWidth = 4; + static constexpr uint32_t BlockHeight = 4; + + static void decode_texel(Target* target, const Source* in, const uint32_t x) { + const auto texel = bswap(in[x]); + target[x].r = ExpandTo8<5>(texel >> 11 & 0x1f); + target[x].g = ExpandTo8<6>(texel >> 5 & 0x3f); + target[x].b = ExpandTo8<5>(texel & 0x1f); + target[x].a = 0xff; + } +}; + +struct TextureDecoderRGB5A3 { + using Source = uint16_t; + using Target = RGBA8; + + static constexpr uint32_t Frac = 1; + static constexpr uint32_t BlockWidth = 4; + static constexpr uint32_t BlockHeight = 4; + + static void decode_texel(Target* target, const Source* in, const uint32_t x) { + const auto texel = bswap(in[x]); + if ((texel & 0x8000) != 0) { + target[x].r = ExpandTo8<5>(texel >> 10 & 0x1f); + target[x].g = ExpandTo8<5>(texel >> 5 & 0x1f); + target[x].b = ExpandTo8<5>(texel & 0x1f); + target[x].a = 0xff; + } else { + target[x].r = ExpandTo8<4>(texel >> 8 & 0xf); + target[x].g = ExpandTo8<4>(texel >> 4 & 0xf); + target[x].b = ExpandTo8<4>(texel & 0xf); + target[x].a = ExpandTo8<3>(texel >> 12 & 0x7); + } + } +}; + +static ByteBuffer BuildRGBA8FromGCN(uint32_t width, uint32_t height, uint32_t mips, ArrayRef data) { + const size_t texelCount = ComputeMippedTexelCount(width, height, mips); + ByteBuffer buf{sizeof(RGBA8) * texelCount}; + + uint32_t w = width; + uint32_t h = height; + auto* targetMip = reinterpret_cast(buf.data()); + const uint8_t* in = data.data(); + for (uint32_t mip = 0; mip < mips; ++mip) { + const uint32_t bwidth = (w + 3) / 4; + const uint32_t bheight = (h + 3) / 4; + for (uint32_t by = 0; by < bheight; ++by) { + const uint32_t baseY = by * 4; + for (uint32_t bx = 0; bx < bwidth; ++bx) { + const uint32_t baseX = bx * 4; + for (uint32_t c = 0; c < 2; ++c) { + for (uint32_t y = 0; y < 4; ++y) { + for (size_t x = 0; x < 4; ++x) { + if (baseX + x >= w || baseY + y >= h) { + continue; + } + RGBA8* target = targetMip + (baseY + y) * w + baseX; + if (c != 0) { + target[x].g = in[x * 2]; + target[x].b = in[x * 2 + 1]; + } else { + target[x].a = in[x * 2]; + target[x].r = in[x * 2 + 1]; + } + } + in += 8; + } + } + } + } + targetMip += w * h; + if (w > 1) { + w /= 2; + } + if (h > 1) { + h /= 2; + } + } + + return buf; +} + +static ByteBuffer BuildRGBA8FromCMPR(uint32_t width, uint32_t height, uint32_t mips, ArrayRef data) { + const size_t texelCount = ComputeMippedTexelCount(width, height, mips); + ByteBuffer buf{sizeof(RGBA8) * texelCount}; + + uint32_t h = height; + uint32_t w = width; + uint8_t* dst = buf.data(); + const uint8_t* src = data.data(); + for (uint32_t mip = 0; mip < mips; ++mip) { + for (uint32_t yy = 0; yy < h; yy += 8) { + for (uint32_t xx = 0; xx < w; xx += 8) { + for (uint32_t yb = 0; yb < 8; yb += 4) { + for (uint32_t xb = 0; xb < 8; xb += 4) { + // CMPR difference: Big-endian color1/2 + const uint16_t color1 = bswap(*reinterpret_cast(src)); + const uint16_t color2 = bswap(*reinterpret_cast(src + 2)); + src += 4; + + // Fill in first two colors in color table. + std::array color_table{}; + + color_table[0] = ExpandTo8<5>(static_cast((color1 >> 11) & 0x1F)); + color_table[1] = ExpandTo8<6>(static_cast((color1 >> 5) & 0x3F)); + color_table[2] = ExpandTo8<5>(static_cast(color1 & 0x1F)); + color_table[3] = 0xFF; + + color_table[4] = ExpandTo8<5>(static_cast((color2 >> 11) & 0x1F)); + color_table[5] = ExpandTo8<6>(static_cast((color2 >> 5) & 0x3F)); + color_table[6] = ExpandTo8<5>(static_cast(color2 & 0x1F)); + color_table[7] = 0xFF; + if (color1 > color2) { + // Predict gradients. + color_table[8] = S3TCBlend(color_table[4], color_table[0]); + color_table[9] = S3TCBlend(color_table[5], color_table[1]); + color_table[10] = S3TCBlend(color_table[6], color_table[2]); + color_table[11] = 0xFF; + + color_table[12] = S3TCBlend(color_table[0], color_table[4]); + color_table[13] = S3TCBlend(color_table[1], color_table[5]); + color_table[14] = S3TCBlend(color_table[2], color_table[6]); + color_table[15] = 0xFF; + } else { + color_table[8] = HalfBlend(color_table[0], color_table[4]); + color_table[9] = HalfBlend(color_table[1], color_table[5]); + color_table[10] = HalfBlend(color_table[2], color_table[6]); + color_table[11] = 0xFF; + + // CMPR difference: GX fills with an alpha 0 midway point here. + color_table[12] = color_table[8]; + color_table[13] = color_table[9]; + color_table[14] = color_table[10]; + color_table[15] = 0; + } + + for (uint32_t y = 0; y < 4; ++y) { + uint8_t bits = src[y]; + for (uint32_t x = 0; x < 4; ++x) { + if (xx + xb + x >= w || yy + yb + y >= h) { + continue; + } + uint8_t* dstOffs = dst + ((yy + yb + y) * w + (xx + xb + x)) * 4; + const uint8_t* colorTableOffs = &color_table[static_cast((bits >> 6) & 3) * 4]; + memcpy(dstOffs, colorTableOffs, 4); + bits <<= 2; + } + } + src += 4; + } + } + } + } + dst += w * h * 4; + if (w > 1) { + w /= 2; + } + if (h > 1) { + h /= 2; + } + } + + return buf; +} + +ConvertedTexture convert_texture(u32 format, uint32_t width, uint32_t height, uint32_t mips, ArrayRef data) { + const uint32_t maxMips = max_texture_mip_count(width, height); + const uint32_t safeMips = std::clamp(mips, 1u, maxMips); + if (safeMips != mips) { + Log.warn("convert_texture: clamping mip count {} to {} for {}x{} texture", mips, safeMips, width, height); + mips = safeMips; + } + + const auto requiredDataSize = RequiredTextureDataSize(format, width, height, mips); + if (!requiredDataSize) { + Log.warn("convert_texture: invalid {}x{} texture with {} mip levels", width, height, mips); + return {}; + } + if (*requiredDataSize > data.size()) { + Log.warn("convert_texture: invalid data size {} for {}x{} texture with {} mip levels (expected at least {} bytes)", + data.size(), width, height, mips, *requiredDataSize); + return {}; + } + + ByteBuffer converted; + switch (format) { + DEFAULT_FATAL("convert_texture: unknown texture format {}", format); + case GX_TF_R8_PC: + converted = DecodeLinear(static_cast(ComputeMippedTexelCount(width, height, mips)), + data); + break; + case GX_TF_RGBA8_PC: + return {}; // No conversion + case GX_TF_I4: + converted = DecodeTiled(width, height, mips, data); + break; + case GX_TF_I8: + case GX_TF_Z8: + converted = DecodeTiled(width, height, mips, data); + break; + case GX_TF_IA4: + converted = DecodeTiled(width, height, mips, data); + break; + case GX_TF_IA8: + case GX_TF_Z16: + converted = DecodeTiled(width, height, mips, data); + break; + case GX_TF_C4: + converted = DecodeTiled(width, height, mips, data); + break; + case GX_TF_C8: + converted = DecodeTiled(width, height, mips, data); + break; + case GX_TF_C14X2: + converted = DecodeTiled(width, height, mips, data); + break; + case GX_TF_RGB565: + converted = DecodeTiled(width, height, mips, data); + break; + case GX_TF_RGB5A3: + converted = DecodeTiled(width, height, mips, data); + break; + case GX_TF_RGBA8: + case GX_TF_Z24X8: + converted = BuildRGBA8FromGCN(width, height, mips, data); + break; + case GX_TF_CMPR: + converted = BuildRGBA8FromCMPR(width, height, mips, data); + break; + } + const auto wgpuFormat = to_wgpu(format); + bool hasArbitraryMips = false; + if (wgpuFormat == wgpu::TextureFormat::RGBA8Unorm && mips > 1) { + hasArbitraryMips = arb_mip_check(width, height, mips, converted); + } + return { + .format = wgpuFormat, + .width = width, + .height = height, + .mips = mips, + .data = std::move(converted), + .hasArbitraryMips = hasArbitraryMips, + }; +} + +ConvertedTexture convert_tlut(u32 format, uint32_t width, ArrayRef data) { + const size_t requiredDataSize = static_cast(width) * sizeof(uint16_t); + if (requiredDataSize > data.size()) { + Log.warn("convert_tlut: invalid data size {} for {} entries (expected at least {} bytes)", data.size(), width, + requiredDataSize); + return {}; + } + + ByteBuffer converted; + switch (format) { + DEFAULT_FATAL("convert_tlut: unsupported tlut format {}", format); + case GX_TF_IA8: // GX_TL_IA8 + converted = DecodeLinear(width, data); + break; + case GX_TF_RGB565: // GX_TL_RGB565 + converted = DecodeLinear(width, data); + break; + case GX_TF_RGB5A3: // GX_TL_RGB5A3 + converted = DecodeLinear(width, data); + break; + } + return { + .format = wgpu::TextureFormat::RGBA8Unorm, + .width = width, + .height = 1, + .mips = 1, + .data = std::move(converted), + }; +} + +GXTexFmt tlut_texture_format(GXTlutFmt format) noexcept { + switch (format) { + DEFAULT_FATAL("tlut_texture_format: unsupported tlut format {}", format); + case GX_TL_IA8: + return GX_TF_IA8; + case GX_TL_RGB565: + return GX_TF_RGB565; + case GX_TL_RGB5A3: + return GX_TF_RGB5A3; + } +} + +ConvertedTexture convert_texture_palette(u32 textureFormat, uint32_t width, uint32_t height, uint32_t mips, + ArrayRef textureData, GXTlutFmt tlutFormat, uint16_t tlutEntries, + ArrayRef tlutData) { + const auto indices = convert_texture(textureFormat, width, height, mips, textureData); + if (indices.data.empty()) { + return {}; + } + mips = indices.mips; + const auto palette = convert_tlut(tlut_texture_format(tlutFormat), tlutEntries, tlutData); + if (palette.data.empty()) { + return {}; + } + + ByteBuffer pixels; + pixels.reserve_extra(indices.data.size() / sizeof(u16) * 4); + + const auto* indexData = reinterpret_cast(indices.data.data()); + const uint32_t baseWidth = width; + const uint32_t baseHeight = height; + size_t offset = 0; + for (u32 mip = 0; mip < mips; ++mip) { + const size_t pixelCount = static_cast(width) * height; + for (size_t i = 0; i < pixelCount; ++i) { + const u32 index = indexData[offset + i]; + if (index >= tlutEntries) { + constexpr uint8_t transparent[4] = {0, 0, 0, 0}; + pixels.append(transparent, sizeof(transparent)); + continue; + } + const size_t src = static_cast(index) * 4; + pixels.append(palette.data.data() + src, 4); + } + offset += pixelCount; + width = std::max(width >> 1, 1u); + height = std::max(height >> 1, 1u); + } + + bool hasArbitraryMips = arb_mip_check(baseWidth, baseHeight, mips, pixels); + return { + .format = wgpu::TextureFormat::RGBA8Unorm, + .width = baseWidth, + .height = baseHeight, + .mips = mips, + .data = std::move(pixels), + .hasArbitraryMips = hasArbitraryMips, + }; +} +} // namespace aurora::gfx diff --git a/aurora-main/lib/gfx/texture_convert.hpp b/aurora-main/lib/gfx/texture_convert.hpp new file mode 100644 index 0000000..aff4da8 --- /dev/null +++ b/aurora-main/lib/gfx/texture_convert.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "common.hpp" +#include "texture.hpp" +#include "../webgpu/gpu.hpp" + +namespace aurora::gfx { +static constexpr wgpu::TextureFormat to_wgpu(u32 gxFormat) { + switch (gxFormat) { + case GX_TF_C4: + case GX_TF_C8: + case GX_TF_C14X2: + return wgpu::TextureFormat::R16Sint; + default: + return wgpu::TextureFormat::RGBA8Unorm; + } +} + +struct ConvertedTexture { + wgpu::TextureFormat format; + uint32_t width; + uint32_t height; + uint32_t mips = 1; + ByteBuffer data; + bool hasArbitraryMips = false; +}; + +ConvertedTexture convert_texture(u32 format, uint32_t width, uint32_t height, uint32_t mips, ArrayRef data); +ConvertedTexture convert_texture_palette(u32 textureFormat, uint32_t width, uint32_t height, uint32_t mips, + ArrayRef textureData, GXTlutFmt tlutFormat, uint16_t tlutEntries, + ArrayRef tlutData); +ConvertedTexture convert_tlut(u32 format, uint32_t width, ArrayRef data); +GXTexFmt tlut_texture_format(GXTlutFmt format) noexcept; +} // namespace aurora::gfx diff --git a/aurora-main/lib/gfx/texture_replacement.cpp b/aurora-main/lib/gfx/texture_replacement.cpp new file mode 100644 index 0000000..5307691 --- /dev/null +++ b/aurora-main/lib/gfx/texture_replacement.cpp @@ -0,0 +1,800 @@ +#include "texture_replacement.hpp" + +#include "../internal.hpp" +#include "../gx/gx.hpp" +#include "../webgpu/gpu.hpp" +#include "dds_io.hpp" +#include "texture_convert.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "png_io.hpp" +#include "../fs_helper.hpp" + +using namespace aurora::gx; +using aurora::webgpu::g_device; + +namespace aurora::gfx::texture_replacement { +Module Log("aurora::gfx::texture_replacement"); + +struct RuntimeTextureKey { + uint64_t textureHash = 0; + uint64_t tlutHash = 0; + uint32_t width = 0; + uint32_t height = 0; + bool hasTlut = false; + uint32_t format = 0; + + bool operator==(const RuntimeTextureKey& rhs) const = default; + + template + friend H AbslHashValue(H h, const RuntimeTextureKey& key) { + return H::combine(std::move(h), key.textureHash, key.tlutHash, key.width, key.height, key.hasTlut, key.format); + } +}; + +struct TlutMetadata { + uint32_t size = 0; + uint32_t format = 0; + uint16_t entries = 0; + bool valid = false; + // Object the palette was registered from. GXLoadTlut consumes the registration, so this is what + // lets the same object be loaded into more than one hardware slot. + const GXTlutObj* source = nullptr; + ByteBuffer data; +}; + +struct CachedReplacement { + gfx::TextureHandle handle; + uint64_t bytes = 0; + std::list::iterator lruIt; +}; + +struct ReplacementIndexEntry { + std::filesystem::path path; +}; + +absl::flat_hash_map s_replacementIndex; +absl::flat_hash_map s_replacementCache; +absl::flat_hash_set s_failedKeys; +absl::flat_hash_set s_reportedMisses; +absl::flat_hash_map s_pendingTluts; +std::array s_loadedTluts{}; +std::list s_replacementLru; +std::filesystem::path s_replacementRoot; +std::filesystem::path s_dumpRoot; +uint64_t s_replacementCacheBytes = 0; +constexpr uint64_t kReplacementCacheBudgetBytes = 4294967296; // 4GB, reasonable for modern hardware? +constexpr uint64_t kReplacementWildcardTextureHash = 0xFFFFFFFFFFFFFFFFull; +constexpr uint64_t kReplacementWildcardTlutHash = 0xFFFFFFFFFFFFFFFEull; + +bool iequals_ascii(std::string_view lhs, std::string_view rhs) noexcept { + if (lhs.size() != rhs.size()) { + return false; + } + for (size_t i = 0; i < lhs.size(); ++i) { + if (std::tolower(static_cast(lhs[i])) != std::tolower(static_cast(rhs[i]))) { + return false; + } + } + return true; +} + +bool is_relative_to(const std::filesystem::path& path, const std::filesystem::path& root) noexcept { + if (root.empty()) { + return false; + } + auto pathIt = path.begin(); + auto rootIt = root.begin(); + for (; rootIt != root.end(); ++rootIt, ++pathIt) { + if (pathIt == path.end() || !iequals_ascii(fs_path_to_string(*pathIt), fs_path_to_string(*rootIt))) { + return false; + } + } + return true; +} + +bool is_sidecar_mip(std::string_view stem) noexcept { + constexpr std::string_view tag = "_mip"; + size_t i = stem.size(); + while (i > 0 && stem[i - 1] >= '0' && stem[i - 1] <= '9') { + --i; + } + + if (i == stem.size() || i < tag.size()) { + return false; + } + + return stem.substr(i - tag.size(), tag.size()) == tag; +} + +std::optional parse_hex(std::string_view text) noexcept { + if (text.empty()) { + return std::nullopt; + } + uint64_t value = 0; + for (const char ch : text) { + value <<= 4; + if (ch >= '0' && ch <= '9') { + value |= static_cast(ch - '0'); + } else if (ch >= 'a' && ch <= 'f') { + value |= static_cast(ch - 'a' + 10); + } else if (ch >= 'A' && ch <= 'F') { + value |= static_cast(ch - 'A' + 10); + } else { + return std::nullopt; + } + } + return value; +} + +std::optional parse_u32(std::string_view text, int base = 10) noexcept { + if (text.empty()) { + return std::nullopt; + } + + uint32_t value = 0; + const auto* begin = text.data(); + const auto* end = begin + text.size(); + const auto [ptr, ec] = std::from_chars(begin, end, value, base); + if (ec != std::errc{} || ptr != end) { + return std::nullopt; + } + return value; +} + +std::optional> parse_dimensions(std::string_view text) noexcept { + const size_t sep = text.find('x'); + if (sep == std::string_view::npos) { + return std::nullopt; + } + + const auto width = parse_u32(text.substr(0, sep)); + const auto height = parse_u32(text.substr(sep + 1)); + if (!width.has_value() || !height.has_value()) { + return std::nullopt; + } + return std::pair{*width, *height}; +} + +uint32_t texture_base_level_size(const GXTexObj_& obj) noexcept { + switch (obj.format()) { + case GX_TF_R8_PC: + return obj.width() * obj.height(); + case GX_TF_RGBA8_PC: + return obj.width() * obj.height() * 4; + default: + return GXGetTexBufferSize(obj.width(), obj.height(), obj.format(), false, 0); + } +} + +std::optional compute_referenced_tlut_hash(const GXTexObj_& obj) noexcept { + if (!is_palette_format(obj.format()) || obj.tlut >= s_loadedTluts.size()) { + return std::nullopt; + } + + const auto& tlut = s_loadedTluts[obj.tlut]; + const uint32_t textureSize = texture_base_level_size(obj); + const auto* textureData = static_cast(obj.data); + if (!tlut.valid || textureData == nullptr || textureSize == 0) { + return std::nullopt; + } + + uint32_t minIndex = 0xffff; + uint32_t maxIndex = 0; + switch (obj.format()) { + case GX_TF_C4: + for (uint32_t i = 0; i < textureSize; ++i) { + const uint32_t lowNibble = textureData[i] & 0xf; + const uint32_t highNibble = textureData[i] >> 4; + minIndex = std::min({minIndex, lowNibble, highNibble}); + maxIndex = std::max({maxIndex, lowNibble, highNibble}); + } + break; + case GX_TF_C8: + for (uint32_t i = 0; i < textureSize; ++i) { + const uint32_t index = textureData[i]; + minIndex = std::min(minIndex, index); + maxIndex = std::max(maxIndex, index); + } + break; + case GX_TF_C14X2: + for (uint32_t i = 0; i + sizeof(uint16_t) <= textureSize; i += sizeof(uint16_t)) { + uint16_t value = 0; + std::memcpy(&value, textureData + i, sizeof(value)); + const uint32_t index = bswap(value) & 0x3fff; + minIndex = std::min(minIndex, index); + maxIndex = std::max(maxIndex, index); + } + break; + default: + return std::nullopt; + } + + size_t tlutSize = 2 * (static_cast(maxIndex) + 1 - minIndex); + const size_t tlutOffset = 2 * static_cast(minIndex); + if (tlutOffset + tlutSize > tlut.data.size()) { + return std::nullopt; + } + return XXH64(tlut.data.data() + tlutOffset, tlutSize, 0); +} + +const TlutMetadata* get_loaded_tlut(const GXTexObj_& obj) noexcept { + if (!is_palette_format(obj.format()) || obj.tlut >= s_loadedTluts.size()) { + return nullptr; + } + + const auto& tlut = s_loadedTluts[obj.tlut]; + return tlut.valid ? &tlut : nullptr; +} + +bool ensure_directory(const std::filesystem::path& dir) noexcept { + std::error_code ec; + std::filesystem::create_directories(dir, ec); + return !ec; +} + +RuntimeTextureKey build_runtime_key(const GXTexObj_& obj) noexcept { + RuntimeTextureKey key{ + .width = obj.width(), + .height = obj.height(), + .hasTlut = is_palette_format(obj.format()), + .format = obj.format(), + }; + + const uint32_t textureSize = texture_base_level_size(obj); + if (obj.data != nullptr && textureSize != 0) { + key.textureHash = XXH64(obj.data, textureSize, 0); + } + if (key.hasTlut) { + key.tlutHash = compute_referenced_tlut_hash(obj).value_or(0); + } + return key; +} + +std::string format_replacement_filename(const RuntimeTextureKey& key) { + if (key.hasTlut) { + return fmt::format("tex1_{}x{}_{:016x}_{:016x}_{}.dds", key.width, key.height, + key.textureHash, key.tlutHash, key.format); + } + return fmt::format("tex1_{}x{}_{:016x}_{}.dds", key.width, key.height, key.textureHash, key.format); +} + +std::optional parse_replacement_filename(std::string_view filename) noexcept { + const size_t dot = filename.rfind('.'); + if (dot == std::string_view::npos) { + return std::nullopt; + } + + if (!iequals_ascii(filename.substr(dot), ".dds") && !iequals_ascii(filename.substr(dot), ".png")) { + return std::nullopt; + } + + const std::string_view stem = filename.substr(0, dot); + constexpr std::string_view prefix = "tex1_"; + if (!stem.starts_with(prefix)) { + return std::nullopt; + } + + std::array parts{}; + size_t partCount = 0; + size_t offset = 0; + bool consumedAll = false; + while (offset <= stem.size() && partCount < parts.size()) { + const size_t next = stem.find('_', offset); + parts[partCount++] = stem.substr(offset, next == std::string_view::npos ? stem.size() - offset : next - offset); + if (next == std::string_view::npos) { + consumedAll = true; + break; + } + offset = next + 1; + } + if (!consumedAll || partCount < 4 || partCount > 6 || parts[0] != "tex1") { + return std::nullopt; + } + + const auto dimensions = parse_dimensions(parts[1]); + if (!dimensions.has_value()) { + return std::nullopt; + } + + size_t index = 2; + if (parts[index] == "m") { + ++index; + } + + size_t remaining = partCount - index; + if (remaining != 2 && remaining != 3) { + return std::nullopt; + } + + uint64_t textureHash = 0; + if (parts[index] == "$") { + textureHash = kReplacementWildcardTextureHash; + } else { + const auto parsedTex = parse_hex(parts[index]); + if (!parsedTex.has_value()) { + return std::nullopt; + } + textureHash = *parsedTex; + } + + auto formatPart = parts[partCount - 1]; + if (formatPart == "arb") { + formatPart = parts[partCount - 2]; + remaining -= 1; + } + const auto format = parse_u32(formatPart); + if (!format.has_value()) { + return std::nullopt; + } + + uint64_t tlutHash = 0; + const bool hasTlut = remaining == 3; + if (hasTlut) { + const std::string_view tlutPart = parts[index + 1]; + if (tlutPart == "$") { + tlutHash = kReplacementWildcardTlutHash; + } else { + const auto parsedTlutHash = parse_hex(tlutPart); + if (!parsedTlutHash.has_value()) { + return std::nullopt; + } + tlutHash = *parsedTlutHash; + } + } + + return RuntimeTextureKey{ + .textureHash = textureHash, + .tlutHash = tlutHash, + .width = dimensions->first, + .height = dimensions->second, + .hasTlut = hasTlut, + .format = *format, + }; +} + +static std::optional load_texture_file(const std::filesystem::path& path) { + if (path.extension() == ".png") { + return png::load_png_file(path); + } else { + return dds::load_dds_file(path); + } +} + +constexpr bool isUnsupportedTextureFormat(const ConvertedTexture& texture) { + switch (texture.format) { + case wgpu::TextureFormat::BC1RGBAUnorm: + case wgpu::TextureFormat::BC1RGBAUnormSrgb: + case wgpu::TextureFormat::BC2RGBAUnorm: + case wgpu::TextureFormat::BC2RGBAUnormSrgb: + case wgpu::TextureFormat::BC3RGBAUnorm: + case wgpu::TextureFormat::BC3RGBAUnormSrgb: + case wgpu::TextureFormat::BC4RUnorm: + case wgpu::TextureFormat::BC4RSnorm: + case wgpu::TextureFormat::BC5RGUnorm: + case wgpu::TextureFormat::BC5RGSnorm: + case wgpu::TextureFormat::BC6HRGBUfloat: + case wgpu::TextureFormat::BC6HRGBFloat: + case wgpu::TextureFormat::BC7RGBAUnorm: + case wgpu::TextureFormat::BC7RGBAUnormSrgb: + return !webgpu::g_bcTexturesSupported; + default: + return false; + } +} + +std::optional load_replacement(const ReplacementIndexEntry& entry) noexcept { + auto base = load_texture_file(entry.path); + if (!base.has_value()) { + Log.warn("texture_replacement: failed to load texture {}", fs_path_to_string(entry.path)); + return std::nullopt; + } + if (isUnsupportedTextureFormat(base.value())) { + Log.warn( + "texture_replacement: failed to load texture {} due to unsupported format: {}", + fs_path_to_string(entry.path), + static_cast(base->format)); + return std::nullopt; + } + + std::vector more; + std::error_code ec; + for (uint32_t mipLevel = 1;; ++mipLevel) { + const auto mipPath = entry.path.parent_path() / fmt::format("{}_mip{}{}", fs_path_to_string(entry.path.stem()), mipLevel, fs_path_to_string(entry.path.extension())); + if (!std::filesystem::is_regular_file(mipPath, ec)) { + break; + } + + auto lvl = load_texture_file(mipPath); + const uint32_t ew = std::max(base->width >> mipLevel, 1u); + const uint32_t eh = std::max(base->height >> mipLevel, 1u); + const bool ok = lvl.has_value() && lvl->format == base->format && lvl->width == ew && lvl->height == eh; + if (!ok) { + if (!lvl.has_value()) { + Log.warn("texture_replacement: could not load mip {}", fs_path_to_string(mipPath)); + } else { + Log.warn("texture_replacement: expected {}x{} for mip {}, got {}x{}", ew, eh, fs_path_to_string(mipPath), + lvl->width, lvl->height); + } + + break; + } + more.push_back(std::move(*lvl)); + } + + if (more.empty()) { + return base; + } + + const uint32_t mips = 1u + static_cast(more.size()); + const uint64_t n = calc_texture_size(base->format, base->width, base->height, mips); + if (n == 0) { + return std::nullopt; + } + + ByteBuffer blob{static_cast(n)}; + uint8_t* const dst = blob.data(); + uint64_t o = 0; + const auto append = [&](const ByteBuffer& d) noexcept -> bool { + if (o + d.size() > n) { + return false; + } + std::memcpy(dst + o, d.data(), d.size()); + o += d.size(); + return true; + }; + if (!append(base->data)) { + return std::nullopt; + } + for (const auto& mip : more) { + if (!append(mip.data)) { + return std::nullopt; + } + } + if (o != n) { + return std::nullopt; + } + + return ConvertedTexture{ + .format = base->format, + .width = base->width, + .height = base->height, + .mips = mips, + .data = std::move(blob), + }; +} + +void touch_cached_replacement(decltype(s_replacementCache)::iterator it) noexcept { + if (it->second.lruIt != s_replacementLru.begin()) { + s_replacementLru.splice(s_replacementLru.begin(), s_replacementLru, it->second.lruIt); + it->second.lruIt = s_replacementLru.begin(); + } +} + +void evict_replacement_cache_if_needed() noexcept { + while (s_replacementCacheBytes > kReplacementCacheBudgetBytes && !s_replacementLru.empty()) { + const RuntimeTextureKey key = s_replacementLru.back(); + s_replacementLru.pop_back(); + + const auto it = s_replacementCache.find(key); + if (it == s_replacementCache.end()) { + continue; + } + + const uint64_t entryBytes = it->second.bytes; + s_replacementCache.erase(it); + s_replacementCacheBytes -= std::min(s_replacementCacheBytes, entryBytes); + } +} + +void build_index() noexcept { + if (!g_config.allowTextureReplacements) { + return; + } + + auto userPath = std::filesystem::path{reinterpret_cast(g_config.userPath)}; + auto cachePath = std::filesystem::path{reinterpret_cast(g_config.cachePath)}; + + s_replacementRoot = userPath / "texture_replacements"; + s_dumpRoot = cachePath / "texture_dumps"; + + if (!ensure_directory(s_replacementRoot)) { + return; + } + if (g_config.allowTextureDumps && !ensure_directory(s_dumpRoot)) { + return; + } + + std::error_code ec; + for (std::filesystem::recursive_directory_iterator it( + s_replacementRoot, + std::filesystem::directory_options::skip_permission_denied | + std::filesystem::directory_options::follow_directory_symlink, + ec); + it != std::filesystem::recursive_directory_iterator(); it.increment(ec)) { + if (ec) { + break; + } + + if (!it->is_regular_file()) { + continue; + } + + const auto& path = it->path(); + + if (is_relative_to(path, s_dumpRoot)) { + continue; + } + + if (!iequals_ascii(fs_path_to_string(path.extension()), ".dds") && !iequals_ascii(fs_path_to_string(path.extension()), ".png")) { + continue; + } + + if (is_sidecar_mip(fs_path_to_string(path.stem()))) { + continue; + } + + const auto parsed = parse_replacement_filename(fs_path_to_string(path.filename())); + if (!parsed.has_value()) { + continue; + } + + s_replacementIndex.try_emplace(*parsed, path); + } + + Log.info("Indexed {} texture replacements", s_replacementIndex.size()); +} + +const ReplacementIndexEntry* find_replacement_path(const RuntimeTextureKey& key) noexcept { + if (const auto it = s_replacementIndex.find(key); it != s_replacementIndex.end()) { + return &it->second; + } + + if (key.hasTlut) { + RuntimeTextureKey tlutWildcardKey = key; + tlutWildcardKey.tlutHash = kReplacementWildcardTlutHash; + if (const auto it = s_replacementIndex.find(tlutWildcardKey); it != s_replacementIndex.end()) { + return &it->second; + } + } + + RuntimeTextureKey textureWildcardKey = key; + textureWildcardKey.textureHash = kReplacementWildcardTextureHash; + if (const auto it = s_replacementIndex.find(textureWildcardKey); it != s_replacementIndex.end()) { + return &it->second; + } + + return nullptr; +} + +const gfx::TextureHandle* find_cached_replacement(const RuntimeTextureKey& key) noexcept { + const auto cached = s_replacementCache.find(key); + if (cached == s_replacementCache.end()) { + return nullptr; + } + + touch_cached_replacement(cached); + return &cached->second.handle; +} + +gfx::TextureHandle load_replacement_texture(const RuntimeTextureKey& key, const ReplacementIndexEntry& entry) noexcept { + const auto replacement = load_replacement(entry); + if (!replacement.has_value()) { + s_failedKeys.insert(key); + return {}; + } + + const auto label = fmt::format("TextureReplacement {}", format_replacement_filename(key)); + const wgpu::Extent3D size{ + .width = replacement->width, + .height = replacement->height, + .depthOrArrayLayers = 1, + }; + const wgpu::TextureDescriptor textureDescriptor{ + .label = label.c_str(), + .usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopyDst, + .dimension = wgpu::TextureDimension::e2D, + .size = size, + .format = replacement->format, + .mipLevelCount = replacement->mips, + .sampleCount = 1, + }; + auto texture = g_device.CreateTexture(&textureDescriptor); + const auto viewLabel = fmt::format("{} view", label); + const wgpu::TextureViewDescriptor textureViewDescriptor{ + .label = viewLabel.c_str(), + .format = replacement->format, + .dimension = wgpu::TextureViewDimension::e2D, + .mipLevelCount = replacement->mips, + }; + auto textureView = texture.CreateView(&textureViewDescriptor); + auto handle = std::make_shared(std::move(texture), std::move(textureView), wgpu::TextureView{}, size, + replacement->format, replacement->mips, gfx::InvalidTextureFormat); + handle->isReplacement = true; + gfx::write_texture(*handle, replacement->data); + return handle; +} + +void cache_replacement(const RuntimeTextureKey& key, const gfx::TextureHandle& handle) noexcept { + const uint64_t replacementBytes = + calc_texture_size(handle->format, handle->size.width, handle->size.height, handle->mipCount); + s_replacementLru.push_front(key); + s_replacementCache.emplace( + key, CachedReplacement{.handle = handle, .bytes = replacementBytes, .lruIt = s_replacementLru.begin()}); + s_replacementCacheBytes += replacementBytes; + evict_replacement_cache_if_needed(); +} + +bool dump_editable_texture_dds(const RuntimeTextureKey& key, const GXTexObj_& obj) noexcept { + const ArrayRef texData{static_cast(obj.data), UINT32_MAX}; + const uint32_t texWidth = obj.width(); + const uint32_t texHeight = obj.height(); + + ConvertedTexture pixels; + if (is_palette_format(obj.format())) { + const TlutMetadata* tlut = get_loaded_tlut(obj); + if (tlut == nullptr) { + return false; + } + pixels = + convert_texture_palette(obj.format(), texWidth, texHeight, 1, texData, static_cast(tlut->format), + tlut->entries, {tlut->data.data(), tlut->data.size()}); + } else { + pixels = convert_texture(obj.format(), texWidth, texHeight, 1, texData); + } + + const uint64_t rgbaBytes = calc_texture_size(wgpu::TextureFormat::RGBA8Unorm, texWidth, texHeight, 1); + + if (pixels.data.empty() || pixels.format != wgpu::TextureFormat::RGBA8Unorm || pixels.data.size() != rgbaBytes) { + return false; + } + + const auto path = s_dumpRoot / format_replacement_filename(key); + return dds::write_rgba8_dds(path, texWidth, texHeight, pixels.data); +} + +bool report_missing_key(const RuntimeTextureKey& key, const GXTexObj_& obj) noexcept { + if (!s_reportedMisses.insert(key).second) { + return false; + } + + if (g_config.allowTextureDumps) { + dump_editable_texture_dds(key, obj); + } + return true; +} + +void initialize() noexcept { build_index(); } + +void shutdown() noexcept { + s_replacementIndex.clear(); + s_replacementCache.clear(); + s_failedKeys.clear(); + s_reportedMisses.clear(); + s_pendingTluts.clear(); + for (auto& tlut : s_loadedTluts) { + tlut = {}; + } + s_replacementLru.clear(); + s_replacementCacheBytes = 0; + s_replacementRoot.clear(); + s_dumpRoot.clear(); +} + +// Mirrors of the guest palettes, kept only so find_replacement can hash and the DDS dump decode a +// CI palette. The renderer uploads from the guest pointer, so skip this work when replacement is off. +void register_tlut(const GXTlutObj* obj, const void* data, GXTlutFmt format, uint16_t entries) noexcept { + if (!g_config.allowTextureReplacements) { + return; + } + if (obj == nullptr || data == nullptr) { + return; + } + + // A registration is only consumed by a matching load_tlut, and the map is keyed by a guest address + // the game may free and reuse, so a table this large means stale entries: drop them wholesale. + constexpr size_t kMaxPendingTluts = 4096; + if (s_pendingTluts.size() >= kMaxPendingTluts && !s_pendingTluts.contains(obj)) { + s_pendingTluts.clear(); + } + + const size_t sz = static_cast(entries) * 2; + ByteBuffer buffer{sz}; + std::memcpy(buffer.data(), static_cast(data), sz); + s_pendingTluts[obj] = { + .size = static_cast(entries) * 2, + .format = static_cast(format), + .entries = entries, + .valid = true, + .source = obj, + .data = std::move(buffer), + }; +} + +void load_tlut(const GXTlutObj* obj, uint32_t idx) noexcept { + if (!g_config.allowTextureReplacements) { + return; + } + if (idx >= s_loadedTluts.size()) { + return; + } + + // Consume the pending registration. The map is keyed by a guest object address that can be freed + // or reused at any time, and entries used to live until shutdown. + if (const auto it = s_pendingTluts.find(obj); it != s_pendingTluts.end()) { + s_loadedTluts[idx] = std::move(it->second); + s_pendingTluts.erase(it); + return; + } + + // The same object may legally be loaded into several hardware slots; only the first load finds the + // pending entry, so recover the palette from whichever slot already holds it. + for (const auto& loaded : s_loadedTluts) { + if (!loaded.valid || loaded.source != obj) { + continue; + } + s_loadedTluts[idx] = { + .size = loaded.size, + .format = loaded.format, + .entries = loaded.entries, + .valid = loaded.valid, + .source = loaded.source, + .data = loaded.data.clone(), + }; + return; + } + + s_loadedTluts[idx] = {}; +} + +std::optional find_replacement(const GXTexObj_& obj) noexcept { + ZoneScoped; + + if (!g_config.allowTextureReplacements) { + return std::nullopt; + } + + const RuntimeTextureKey key = build_runtime_key(obj); + const auto* path = find_replacement_path(key); + if (path == nullptr) { + report_missing_key(key, obj); + return std::nullopt; + } + + if (const auto* cached = find_cached_replacement(key); cached != nullptr) { + return *cached; + } + + if (s_failedKeys.contains(key)) { + return std::nullopt; + } + + auto handle = load_replacement_texture(key, *path); + if (!handle) { + return std::nullopt; + } + + cache_replacement(key, handle); + return handle; +} + +std::string build_texture_replacement_name(const GXTexObj_& obj) noexcept { + const RuntimeTextureKey key = build_runtime_key(obj); + return format_replacement_filename(key); +} + +} // namespace aurora::gfx::texture_replacement diff --git a/aurora-main/lib/gfx/texture_replacement.hpp b/aurora-main/lib/gfx/texture_replacement.hpp new file mode 100644 index 0000000..4b9ee06 --- /dev/null +++ b/aurora-main/lib/gfx/texture_replacement.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "texture.hpp" +#include + +namespace aurora::gfx::texture_replacement { +void initialize() noexcept; +void shutdown() noexcept; +void register_tlut(const GXTlutObj* obj, const void* data, GXTlutFmt format, uint16_t entries) noexcept; +void load_tlut(const GXTlutObj* obj, uint32_t idx) noexcept; +std::optional find_replacement(const GXTexObj_& obj) noexcept; +std::string build_texture_replacement_name(const GXTexObj_& obj) noexcept; +} // namespace aurora::gfx::texture_replacement diff --git a/aurora-main/lib/gx/command_processor.cpp b/aurora-main/lib/gx/command_processor.cpp new file mode 100644 index 0000000..5f69705 --- /dev/null +++ b/aurora-main/lib/gx/command_processor.cpp @@ -0,0 +1,2449 @@ +#include "command_processor.hpp" + +#include "../gfx/common.hpp" +#include "../dolphin/gx/__gx.h" +#include "../gfx/texture_replacement.hpp" +#include "dolphin/gx/GXAurora.h" +#include "gx.hpp" +#include "gx_fmt.hpp" +#include "pipeline.hpp" +#include "shader_info.hpp" +#include "../internal.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace aurora::gx::fifo { +static Module Log("aurora::gx::fifo"); + +using IndexBuffer = std::vector; + +static u32 prepare_idx_template(IndexBuffer& buf, GXPrimitive prim, u16 vtxCount) { + size_t writePos = 0; + if (prim == GX_QUADS) { + // Retain the existing incomplete-quad behavior: every started group emits a complete six-index quad. + buf.resize(((static_cast(vtxCount) + 3u) / 4u) * 6u); + + for (u16 v = 0; v < vtxCount; v += 4) { + const u16 idx0 = v; + const u16 idx1 = static_cast(v + 1); + const u16 idx2 = static_cast(v + 2); + const u16 idx3 = static_cast(v + 3); + buf[writePos++] = idx0; + buf[writePos++] = idx1; + buf[writePos++] = idx2; + buf[writePos++] = idx2; + buf[writePos++] = idx3; + buf[writePos++] = idx0; + } + } else if (prim == GX_TRIANGLES) { + buf.resize(vtxCount); + for (u16 v = 0; v < vtxCount; ++v) { + buf[writePos++] = v; + } + } else if (prim == GX_TRIANGLEFAN) { + const u32 indexCount = vtxCount <= 3 ? vtxCount : 3u + (static_cast(vtxCount) - 3u) * 3u; + buf.resize(indexCount); + for (u16 v = 0; v < vtxCount; ++v) { + if (v < 3) { + buf[writePos++] = v; + continue; + } + buf[writePos++] = 0; + buf[writePos++] = static_cast(v - 1); + buf[writePos++] = v; + } + } else if (prim == GX_TRIANGLESTRIP) { + const u32 indexCount = vtxCount <= 3 ? vtxCount : 3u + (static_cast(vtxCount) - 3u) * 3u; + buf.resize(indexCount); + for (u16 v = 0; v < vtxCount; ++v) { + if (v < 3) { + buf[writePos++] = v; + continue; + } + if ((v & 1) == 0) { + buf[writePos++] = static_cast(v - 2); + buf[writePos++] = static_cast(v - 1); + } else { + buf[writePos++] = static_cast(v - 1); + buf[writePos++] = static_cast(v - 2); + } + buf[writePos++] = v; + } + } else if (prim == GX_LINES || prim == GX_LINESTRIP || prim == GX_POINTS) { + buf = {0, 1, 3, 3, 2, 0}; + writePos = 6; + } else + UNLIKELY FATAL("unsupported primitive type {}", static_cast(prim)); + CHECK(writePos == buf.size(), "index template size mismatch ({} != {})", writePos, buf.size()); + return static_cast(writePos); +} + +// GX FIFO opcodes - use CP_ prefix to avoid clashing with GXCommandList.h macros +static constexpr u8 CP_CMD_NOP = GX_NOP; +static constexpr u8 CP_CMD_LOAD_CP_REG = GX_LOAD_CP_REG; +static constexpr u8 CP_CMD_LOAD_XF_REG = GX_LOAD_XF_REG; +static constexpr u8 CP_CMD_LOAD_INDX_A = GX_LOAD_INDX_A; +static constexpr u8 CP_CMD_LOAD_INDX_B = GX_LOAD_INDX_B; +static constexpr u8 CP_CMD_LOAD_INDX_C = GX_LOAD_INDX_C; +static constexpr u8 CP_CMD_LOAD_INDX_D = GX_LOAD_INDX_D; +static constexpr u8 CP_CMD_CALL_DL = GX_CMD_CALL_DL; +static constexpr u8 CP_CMD_INVAL_VTX = GX_CMD_INVL_VC; +static constexpr u8 CP_CMD_LOAD_BP_REG = GX_LOAD_BP_REG & GX_OPCODE_MASK; + +// Primitive type mask +static constexpr u8 CP_OPCODE_MASK = GX_OPCODE_MASK; +static constexpr u8 CP_VAT_MASK = GX_VAT_MASK; +static constexpr u8 CP_PRIMITIVE_START = 0x80; +static constexpr u8 CP_PRIMITIVE_END = 0xBF; + +// Read helpers for big/little endian +#if _MSC_VER +template +__forceinline // Yes, this was necessary. +inline T unaligned_load(const T* ptr) { + return *static_cast(ptr); +} +#else +template +inline T unaligned_load(const T* ptr) { + T copy; + memcpy(©, ptr, sizeof(T)); + return copy; +} +#endif + +static inline u16 read_u16(const u8* ptr, bool bigEndian) { + const u16 val = unaligned_load(reinterpret_cast(ptr)); + if (bigEndian) { + return bswap(val); + } + return val; +} + +static inline u32 read_u32(const u8* ptr, bool bigEndian) { + const u32 val = unaligned_load(reinterpret_cast(ptr)); + if (bigEndian) { + return bswap(val); + } + return val; +} + +static bool is_draw_cmd(u8 cmd) { + return cmd >= CP_PRIMITIVE_START && cmd <= CP_PRIMITIVE_END; +} + +static GXPrimitive primitive_from_draw_cmd(u8 cmd) { + switch (cmd & CP_OPCODE_MASK) { + case GX_DRAW_QUADS: + case 0x88: + return GX_QUADS; + case GX_DRAW_TRIANGLES: + return GX_TRIANGLES; + case GX_DRAW_TRIANGLE_STRIP: + return GX_TRIANGLESTRIP; + case GX_DRAW_TRIANGLE_FAN: + return GX_TRIANGLEFAN; + case GX_DRAW_LINES: + return GX_LINES; + case GX_DRAW_LINE_STRIP: + return GX_LINESTRIP; + case GX_DRAW_POINTS: + return GX_POINTS; + default: + UNLIKELY FATAL("unsupported primitive command 0x{:02X}", cmd); + } +} + + + + +static u32 bp_get(u32 reg, u32 size, u32 shift); +static inline f32 read_f32(const u8* ptr, bool bigEndian); + +static GXPixelFmt decode_pixel_fmt(u32 peCtrl, u32 cmode1) { + switch (bp_get(peCtrl, 3, 0)) { + case 0: + return GX_PF_RGB8_Z24; + case 1: + return GX_PF_RGBA6_Z24; + case 2: + return GX_PF_RGB565_Z16; + case 3: + return GX_PF_Z24; + case 4: + switch (bp_get(cmode1, 2, 9)) { + case 0: + return GX_PF_Y8; + case 1: + return GX_PF_U8; + case 2: + return GX_PF_V8; + default: + Log.warn("command_processor: unsupported cmode1 pixel subtype {}", bp_get(cmode1, 2, 9)); + return GX_PF_Y8; + } + case 5: + return GX_PF_YUV420; + default: + Log.warn("command_processor: unsupported PE pixel format {}", bp_get(peCtrl, 3, 0)); + return GX_PF_RGB8_Z24; + } +} + +static inline u64 read_u64(const u8* ptr, bool bigEndian) { + u64 loaded; + // Unaligned-safe load + memcpy(&loaded, ptr, sizeof(u64)); + + if (bigEndian) { + return bswap(loaded); + } + + return loaded; +} + +struct TexBpRegMapping { + u8 texMapId; + enum class Kind : uint8_t { Mode0, Mode1, Image0, Image1, Image2, Image3, Tlut } kind; +}; + +static std::optional decode_tex_bp_reg(u32 regId) { + constexpr std::array mode0Ids{0x80u, 0x81u, 0x82u, 0x83u, 0xA0u, 0xA1u, 0xA2u, 0xA3u}; + constexpr std::array mode1Ids{0x84u, 0x85u, 0x86u, 0x87u, 0xA4u, 0xA5u, 0xA6u, 0xA7u}; + constexpr std::array image0Ids{0x88u, 0x89u, 0x8Au, 0x8Bu, 0xA8u, 0xA9u, 0xAAu, 0xABu}; + constexpr std::array image1Ids{0x8Cu, 0x8Du, 0x8Eu, 0x8Fu, 0xACu, 0xADu, 0xAEu, 0xAFu}; + constexpr std::array image2Ids{0x90u, 0x91u, 0x92u, 0x93u, 0xB0u, 0xB1u, 0xB2u, 0xB3u}; + constexpr std::array image3Ids{0x94u, 0x95u, 0x96u, 0x97u, 0xB4u, 0xB5u, 0xB6u, 0xB7u}; + constexpr std::array tlutIds{0x98u, 0x99u, 0x9Au, 0x9Bu, 0xB8u, 0xB9u, 0xBAu, 0xBBu}; + + for (u8 i = 0; i < MaxTextures; ++i) { + if (regId == mode0Ids[i]) { + return TexBpRegMapping{.texMapId = i, .kind = TexBpRegMapping::Kind::Mode0}; + } + if (regId == mode1Ids[i]) { + return TexBpRegMapping{.texMapId = i, .kind = TexBpRegMapping::Kind::Mode1}; + } + if (regId == image0Ids[i]) { + return TexBpRegMapping{.texMapId = i, .kind = TexBpRegMapping::Kind::Image0}; + } + if (regId == image1Ids[i]) { + return TexBpRegMapping{.texMapId = i, .kind = TexBpRegMapping::Kind::Image1}; + } + if (regId == image2Ids[i]) { + return TexBpRegMapping{.texMapId = i, .kind = TexBpRegMapping::Kind::Image2}; + } + if (regId == image3Ids[i]) { + return TexBpRegMapping{.texMapId = i, .kind = TexBpRegMapping::Kind::Image3}; + } + if (regId == tlutIds[i]) { + return TexBpRegMapping{.texMapId = i, .kind = TexBpRegMapping::Kind::Tlut}; + } + } + return std::nullopt; +} + +// Helper to convert packed RGBA8 to Vec4 +static Vec4 unpack_color(u32 packed) { + return { + static_cast(packed >> 24 & 0xFF) / 255.f, + static_cast(packed >> 16 & 0xFF) / 255.f, + static_cast(packed >> 8 & 0xFF) / 255.f, + static_cast(packed & 0xFF) / 255.f, + }; +} + +static inline f32 read_f32(const u8* ptr, bool bigEndian) { + u32 bits = read_u32(ptr, bigEndian); + f32 val; + std::memcpy(&val, &bits, sizeof(val)); + return val; +} + +// Marks draw state dirty *and* invalidates the resolved-pipeline memo. +static inline void mark_pipeline_state_dirty() noexcept { + g_gxState.stateDirty = true; + g_gxState.pipelineStateGeneration = next_gx_state_epoch(); +} + +// Rejects a malformed XF write in release builds. +#define XF_REQUIRE(cond, msg, ...) \ + do { \ + if (!(cond)) UNLIKELY { \ + CHECK(cond, msg, ##__VA_ARGS__); \ + Log.warn(msg, ##__VA_ARGS__); \ + return true; \ + } \ + } while (0) + +static bool copy_xf_data(u32 addr, const u8* data, u32 len, bool bigEndian) { + if (addr < 0x78) { + // Position matrices (0x0000 - 0x0077) + u32 mtxIdx = addr / 12; + u32 startOffset = addr % 12; + // We only support full writes to matrices + XF_REQUIRE(mtxIdx < MaxPnMtx, "XF: PosMtx copy oob; mtxIdx={}", mtxIdx); + XF_REQUIRE(startOffset == 0 && len == 12, "XF: PosMtx sub-copy unsupported: offs={}, len={}", startOffset, len); + auto& mtx = g_gxState.pnMtx[mtxIdx].pos; + f32* flat = reinterpret_cast(&mtx); + for (u32 i = 0; i < len; i++) { + flat[i] = read_f32(data + i * 4, bigEndian); + } + g_gxState.stateDirty = true; + return true; + } else if (addr < 0x0F0) { + // Texture matrices (0x078-0x0EF) + u32 texBase = addr - 0x078; + u32 mtxIdx = texBase / 12; + u32 startOffset = texBase % 12; + XF_REQUIRE(mtxIdx < MaxTexMtx, "XF TexMtx copy oob; mtxIdx={}", mtxIdx); + XF_REQUIRE(startOffset == 0 && (len == 8 || len == 12), "XF TexMtx sub-copy unsupported: offs={}, len={}", + startOffset, len); + + // Determine if 2x4 or 3x4 from count + auto& mtx = g_gxState.texMtxs[mtxIdx]; + f32* flat = reinterpret_cast(&mtx); + for (u32 i = 0; i < len; i++) { + flat[i] = read_f32(data + i * 4, bigEndian); + } + g_gxState.stateDirty = true; + return true; + } else if (addr >= 0x400 && addr < 0x45A) { + // Normal matrices (0x400-0x459) + u32 nrmBase = addr - 0x400; + u32 mtxIdx = nrmBase / 9; + u32 startOffset = nrmBase % 9; + // We only support full writes to matrices + XF_REQUIRE(mtxIdx < MaxPnMtx, "XF: NrmMtx copy oob; mtxIdx={}", mtxIdx); + XF_REQUIRE(startOffset == 0 && len == 9, "XF: NrmMtx sub-copy unsupported: offs={}, len={}", startOffset, len); + auto& mtx = g_gxState.pnMtx[mtxIdx].nrm; + f32* flat = reinterpret_cast(&mtx); + for (u32 i = 0; i < len; i++) { + u32 xfIdx = i; + u32 row = xfIdx / 3; + u32 col = xfIdx % 3; + if (row < 3) { + flat[row * 4 + col] = read_f32(data + i * 4, bigEndian); + } + } + g_gxState.stateDirty = true; + return true; + } else if (addr >= 0x500 && addr < 0x5F0) { + // Post-transform texture matrices (0x500-0x5EF) + u32 ptBase = addr - 0x500; + u32 mtxIdx = ptBase / 12; + u32 startOffset = ptBase % 12; + XF_REQUIRE(mtxIdx < MaxPTTexMtx, "XF: PTTexMtx copy oob; mtxIdx={}", mtxIdx); + XF_REQUIRE(startOffset == 0 && len == 12, "XF: PTTexMtx sub-copy unsupported: offs={}, len={}", startOffset, len); + auto& mtx = g_gxState.ptTexMtxs[mtxIdx]; + f32* flat = reinterpret_cast(&mtx); + for (u32 i = 0; i < len; i++) { + flat[startOffset + i] = read_f32(data + i * 4, bigEndian); + } + g_gxState.stateDirty = true; + return true; + } else if (addr >= 0x600 && addr < 0x680) { + // Lights (0x600-0x67F) - 8 lights, 16 values each + u32 lightBase = addr - 0x600; + u32 lightIdx = lightBase / 0x10; + u32 startOffset = lightBase % 0x10; + XF_REQUIRE(lightIdx < GX::MaxLights, "XF: Light copy oob; lightIdx={}", lightIdx); + XF_REQUIRE(startOffset + len <= 0x10, + "XF: Light copy that crosses across light boundaries unsupported: offs={}, len={}", startOffset, len); + auto& light = g_gxState.lights[lightIdx]; + for (u32 i = 0; i < len; i++) { + u32 field = startOffset + i; + f32 val = read_f32(data + i * 4, bigEndian); + u32 ival = read_u32(data + i * 4, bigEndian); + switch (field) { + case 3: // Color (packed u32) + light.color = unpack_color(ival); + break; + case 4: + light.cosAtt[0] = val; + break; // a0 + case 5: + light.cosAtt[1] = val; + break; // a1 + case 6: + light.cosAtt[2] = val; + break; // a2 + case 7: + light.distAtt[0] = val; + break; // k0 + case 8: + light.distAtt[1] = val; + break; // k1 + case 9: + light.distAtt[2] = val; + break; // k2 + case 10: + light.pos[0] = val; + break; // px + case 11: + light.pos[1] = val; + break; // py + case 12: + light.pos[2] = val; + break; // pz + case 13: + light.dir[0] = val; + break; // nx + case 14: + light.dir[1] = val; + break; // ny + case 15: + light.dir[2] = val; + break; // nz + default: + break; // padding (0-2) + } + } + g_gxState.preparedLightsDirty = true; + g_gxState.stateDirty = true; + return true; + } + return false; +} + +static void apply_xf_viewport() { + const auto& vp = g_gxState.xfViewport; + const f32 sx = vp[0]; + const f32 sy = vp[1]; + const f32 sz = vp[2]; + const f32 ox = vp[3]; + const f32 oy = vp[4]; + const f32 oz = vp[5]; + const f32 width = sx * 2.0f; + const f32 height = -sy * 2.0f; + constexpr f32 z24Scale = 16777216.0f; + + set_logical_viewport({ + .left = ox - 340.0f - width / 2.0f, + .top = oy - 340.0f - height / 2.0f, + .width = width, + .height = height, + .znear = (oz - sz) / z24Scale, + .zfar = oz / z24Scale, + }); +} + +static void apply_xf_projection() { + const auto& raw = g_gxState.xfProjection; + auto& proj = g_gxState.proj; + proj = {}; + proj.m0[0] = raw[0]; + proj.m1[1] = raw[2]; + proj.m2[2] = raw[4]; + proj.m2[3] = raw[5]; + + if (g_gxState.projType == GX_ORTHOGRAPHIC) { + proj.m0[3] = raw[1]; + proj.m1[3] = raw[3]; + proj.m3[3] = 1.0f; + } else { + proj.m0[2] = raw[1]; + proj.m1[2] = raw[3]; + proj.m3[2] = -1.0f; + } + + g_gxState.stateDirty = true; +} + +// Forward declarations for register handlers +static void handle_bp(u32 value, bool bigEndian); +static void handle_cp(u8 addr, u32 value, bool bigEndian); +static void handle_xf(const u8* data, u32& pos, u32 size, bool bigEndian); +static bool handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndian); +static bool handle_aurora(const u8* data, u32& pos, u32 size, bool bigEndian); + +void process(const u8* data, u32 size, bool bigEndian) { + ZoneScoped; + // Everything decoded here mutates renderer state (GX state, the recorded command lists and the mapped staging buffers), so take the renderer GPU mutex once for the whole drain rather than once per draw command. + std::lock_guard gpuLock(aurora::renderer_gpu_mutex()); + u32 pos = 0; + + while (pos < size) { + u8 cmd = data[pos++]; + u8 opcode = cmd & CP_OPCODE_MASK; + // Log.warn("Processing opcode {:02x} at pos {} (size {})", opcode, pos - 1, size); + + switch (opcode) { + case CP_CMD_NOP: + continue; + + case CP_CMD_LOAD_BP_REG: { + CHECK(pos + 4 <= size, "BP reg read overrun"); + u32 value = read_u32(data + pos, bigEndian); + pos += 4; + handle_bp(value, bigEndian); + break; + } + + case CP_CMD_LOAD_CP_REG: { + CHECK(pos + 5 <= size, "CP reg read overrun"); + u8 addr = data[pos++]; + u32 value = read_u32(data + pos, bigEndian); + pos += 4; + handle_cp(addr, value, bigEndian); + break; + } + + case CP_CMD_LOAD_XF_REG: { + handle_xf(data, pos, size, bigEndian); + break; + } + + case CP_CMD_LOAD_INDX_A: + case CP_CMD_LOAD_INDX_B: + case CP_CMD_LOAD_INDX_C: + case CP_CMD_LOAD_INDX_D: { + ZoneScopedN("LOAD_INDX"); + CHECK(pos + 4 <= size, "indexed XF read overrun"); + const u32 value = read_u32(data + pos, bigEndian); + pos += 4; + + const u32 arrayType = GX_POS_MTX_ARRAY + ((opcode - CP_CMD_LOAD_INDX_A) / 0x08); + const u32 srcArrayIdx = value >> 16; + const u16 len = static_cast(((value >> 12) & 0x0f) + 1); + const u16 dstAddr = static_cast(value & 0x0fff); + auto const& array = g_gxState.arrays[arrayType]; + const u32 byteOffset = srcArrayIdx * array.stride; + const u32 byteCount = static_cast(len) * 4u; + if (array.data == nullptr || array.stride == 0 || byteOffset + byteCount > array.size) { + static u32 invalidIndexedXfLogCount = 0; + if (invalidIndexedXfLogCount < 16) { + Log.warn("Skipping indexed XF load with invalid source array: array={} idx={} stride={} offset={} bytes={} " + "size={} dst=0x{:04X}", + arrayType, srcArrayIdx, array.stride, byteOffset, byteCount, array.size, dstAddr); + ++invalidIndexedXfLogCount; + } + break; + } + u8* srcData = ((u8*)array.data) + byteOffset; + if (!copy_xf_data(dstAddr, srcData, len, bigEndian)) { +#ifndef NDEBUG + Log.debug("Unimplemented indexed XF load (opcode 0x{:02X}, dstAddr=0x{:04X})", opcode, dstAddr); +#endif + } + break; + } + + case CP_CMD_CALL_DL: { + // Call display list: 8 bytes (address + size) + CHECK(pos + 8 <= size, "call DL read overrun"); + Log.warn("Ignoring nested GX_CMD_CALL_DL"); + pos += 8; + break; + } + + case CP_CMD_INVAL_VTX: { + // GXInvalidateVtxCache tells the GPU that CPU-written indexed vertex arrays must be observed by subsequent draws. + for (int i = GX_VA_POS; i <= GX_VA_TEX7; ++i) { + g_gxState.arrays[i].cachedRange = {}; + } + break; + } + + case GX_LOAD_AURORA: { + if (!handle_aurora(data, pos, size, bigEndian)) { + return; + } + break; + } + + default: + // Draw commands occupy the full 0x80-0xBF range. + if (is_draw_cmd(cmd)) { + if (!handle_draw(cmd, data, pos, size, bigEndian)) { + return; + } + } else { + static u32 unknownLogCount = 0; + if (unknownLogCount < 16) { + // Hex dump surrounding bytes for debugging + u32 dumpStart = (pos > 17) ? pos - 17 : 0; + u32 dumpEnd = (pos + 16 < size) ? pos + 16 : size; + std::string hex; + for (u32 i = dumpStart; i < dumpEnd; i++) { + if (i == pos - 1) + hex += fmt::format("[{:02x}]", data[i]); + else + hex += fmt::format(" {:02x}", data[i]); + } + Log.warn(" hex dump (pos {}-{}):{}", dumpStart, dumpEnd - 1, hex); + Log.warn("command_processor: unknown opcode 0x{:02X} at pos {}", cmd, pos - 1); + ++unknownLogCount; + } + } + break; + } + } +} + +// Helper to extract bit fields from a 32-bit register +inline static u32 bp_get(u32 reg, u32 size, u32 shift) { return reg >> shift & (1u << size) - 1; } + +static u8 normal_frac_bits(GXCompType type) { + switch (type) { + case GX_U8: + return 7; + case GX_S8: + return 6; + case GX_U16: + return 15; + case GX_S16: + return 14; + default: + return 0; + } +} + +static void refresh_copy_filter_flags() { + bool aa = false; + for (const auto& sample : g_gxState.copyFilterSamplePattern) { + aa |= sample[0] != 6 || sample[1] != 6; + } + g_gxState.copyFilterAa = aa; + + static constexpr std::array DefaultVFilter{0, 0, 21, 22, 21, 0, 0}; + bool vf = false; + for (size_t i = 0; i < DefaultVFilter.size(); ++i) { + vf |= g_gxState.copyFilterVFilter[i] != DefaultVFilter[i]; + } + g_gxState.copyFilterVf = vf; +} + +// BP register handler - decodes BP (RAS/pixel engine) register writes and updates g_gxState +static void handle_bp(u32 value, bool bigEndian) { + u32 regId = (value >> 24) & 0xFF; + // Mask off the register ID from the value for field extraction + // (the regId is stored in bits 24-31, data is in bits 0-23) + + if (regId == 0xFE) { + g_gxState.bpRegCache[regId] = value & 0x00FFFFFF; + return; + } else { + const u32 ssMask = g_gxState.bpRegCache[0xFE]; + // A preceding 0xFE write is rare; the common path only has to prove the mask is already wide open. + if (ssMask != 0x00FFFFFF) UNLIKELY { + g_gxState.bpRegCache[0xFE] = 0x00FFFFFF; + } + const u32 merged = (g_gxState.bpRegCache[regId] & ~ssMask) | (value & ssMask); + value = (regId << 24) | (merged & 0x00FFFFFF); + if (g_gxState.bpRegCache[regId] == value) return; + g_gxState.bpRegCache[regId] = value; + } + // TEV color combiner stages (0xC0, 0xC2, 0xC4, ... 0xDE) + if (regId >= 0xC0 && regId <= 0xDE && (regId & 1) == 0) { + u32 stage = (regId - 0xC0) / 2; + if (stage < MaxTevStages) { + auto& s = g_gxState.tevStages[stage]; + s.colorPass.d = static_cast(bp_get(value, 4, 0)); + s.colorPass.c = static_cast(bp_get(value, 4, 4)); + s.colorPass.b = static_cast(bp_get(value, 4, 8)); + s.colorPass.a = static_cast(bp_get(value, 4, 12)); + s.colorOp.clamp = bp_get(value, 1, 19) != 0; + s.colorOp.outReg = static_cast(bp_get(value, 2, 22)); + if (bp_get(value, 2, 16) == 3) { + // Bias==3 means compare mode: reconstruct GXTevOp enum (8 + 3-bit hw value) + u32 hwOp = bp_get(value, 1, 18) | (bp_get(value, 2, 20) << 1); + s.colorOp.op = static_cast(hwOp + 8); + s.colorOp.bias = GX_TB_ZERO; + s.colorOp.scale = GX_CS_SCALE_1; + } else { + // Normal mode: bit18 is op (0=ADD, 1=SUB), bits16-17 is bias, bits20-21 is scale + s.colorOp.op = static_cast(bp_get(value, 1, 18)); + s.colorOp.bias = static_cast(bp_get(value, 2, 16)); + s.colorOp.scale = static_cast(bp_get(value, 2, 20)); + } + mark_pipeline_state_dirty(); + } + return; + } + + // TEV alpha combiner stages (0xC1, 0xC3, 0xC5, ... 0xDF) + if (regId >= 0xC1 && regId <= 0xDF && (regId & 1) == 1) { + u32 stage = (regId - 0xC1) / 2; + if (stage < MaxTevStages) { + auto& s = g_gxState.tevStages[stage]; + s.tevSwapRas = static_cast(bp_get(value, 2, 0)); + s.tevSwapTex = static_cast(bp_get(value, 2, 2)); + s.alphaPass.d = static_cast(bp_get(value, 3, 4)); + s.alphaPass.c = static_cast(bp_get(value, 3, 7)); + s.alphaPass.b = static_cast(bp_get(value, 3, 10)); + s.alphaPass.a = static_cast(bp_get(value, 3, 13)); + s.alphaOp.clamp = bp_get(value, 1, 19) != 0; + s.alphaOp.outReg = static_cast(bp_get(value, 2, 22)); + if (bp_get(value, 2, 16) == 3) { + u32 hwOp = bp_get(value, 1, 18) | (bp_get(value, 2, 20) << 1); + s.alphaOp.op = static_cast(hwOp + 8); + s.alphaOp.bias = GX_TB_ZERO; + s.alphaOp.scale = GX_CS_SCALE_1; + } else { + s.alphaOp.op = static_cast(bp_get(value, 1, 18)); + s.alphaOp.bias = static_cast(bp_get(value, 2, 16)); + s.alphaOp.scale = static_cast(bp_get(value, 2, 20)); + } + mark_pipeline_state_dirty(); + } + return; + } + + switch (regId) { + // genMode (0x00) + case 0x00: { + g_gxState.numTexGens = bp_get(value, 4, 0); + g_gxState.numChans = bp_get(value, 3, 4); + // genMode owns the same numTexGens/numChans that XF 0x3F/0x09 decode, so the XF cache can no longer vouch for those two slots. + g_gxState.invalidateXfReg(0x3F); + g_gxState.invalidateXfReg(0x09); + g_gxState.numTevStages = bp_get(value, 4, 10) + 1; + u32 hwCull = bp_get(value, 2, 14); + // Swap front/back to match GX convention + switch (hwCull) { + case GX_CULL_FRONT: + g_gxState.cullMode = GX_CULL_BACK; + break; + case GX_CULL_BACK: + g_gxState.cullMode = GX_CULL_FRONT; + break; + default: + g_gxState.cullMode = static_cast(hwCull); + break; + } + g_gxState.numIndStages = bp_get(value, 3, 16); + mark_pipeline_state_dirty(); + break; + } + + // Display copy sample pattern (0x01-0x04), six 4-bit samples per BP reg. + case 0x01: + case 0x02: + case 0x03: + case 0x04: { + const size_t first = static_cast(regId - 0x01) * 6; + for (size_t i = 0; i < 6; ++i) { + const size_t sample = first + i; + g_gxState.copyFilterSamplePattern[sample / 2][sample % 2] = static_cast(bp_get(value, 4, i * 4)); + } + refresh_copy_filter_flags(); + break; + } + + // Indirect texture mask (0x0F). + case 0x0F: + g_gxState.indTexMask = static_cast(value & 0xFF); + g_gxState.stateDirty = true; + break; + + // TEV indirect stages (0x10-0x1F) + case 0x10: + case 0x11: + case 0x12: + case 0x13: + case 0x14: + case 0x15: + case 0x16: + case 0x17: + case 0x18: + case 0x19: + case 0x1A: + case 0x1B: + case 0x1C: + case 0x1D: + case 0x1E: + case 0x1F: { + u32 stage = regId - 0x10; + if (stage < MaxTevStages) { + auto& s = g_gxState.tevStages[stage]; + s.indTexStage = static_cast(bp_get(value, 2, 0)); + s.indTexFormat = static_cast(bp_get(value, 2, 2)); + s.indTexBiasSel = static_cast(bp_get(value, 3, 4)); + s.indTexAlphaSel = static_cast(bp_get(value, 2, 7)); + s.indTexMtxId = static_cast(bp_get(value, 4, 9)); + s.indTexWrapS = static_cast(bp_get(value, 3, 13)); + s.indTexWrapT = static_cast(bp_get(value, 3, 16)); + s.indTexUseOrigLOD = bp_get(value, 1, 19) != 0; + s.indTexAddPrev = bp_get(value, 1, 20) != 0; + mark_pipeline_state_dirty(); + } + break; + } + + // Scissor registers (0x20, 0x21) + case 0x20: + case 0x21: { + const u32 scis0 = g_gxState.bpRegCache[0x20]; + const u32 scis1 = g_gxState.bpRegCache[0x21]; + const int32_t tp = static_cast(bp_get(scis0, 11, 0)) - 342; + const int32_t lf = static_cast(bp_get(scis0, 11, 12)) - 342; + const int32_t bm = static_cast(bp_get(scis1, 11, 0)) - 342; + const int32_t rt = static_cast(bp_get(scis1, 11, 12)) - 342; + const int32_t wd = std::max(rt - lf + 1, 0); + const int32_t ht = std::max(bm - tp + 1, 0); + set_logical_scissor({lf, tp, wd, ht}); + break; + } + + // Line/point size (0x22) + case 0x22: { + g_gxState.lineWidth = static_cast(bp_get(value, 8, 0)); + g_gxState.pointSize = static_cast(bp_get(value, 8, 8)); + g_gxState.lineTexOffset = static_cast(bp_get(value, 3, 16)); + g_gxState.pointTexOffset = static_cast(bp_get(value, 3, 19)); + g_gxState.lineHalfAspect = bp_get(value, 1, 22) != 0; + g_gxState.stateDirty = true; + break; + } + + // Indirect texture scale (0x25, 0x26) + case 0x25: { + if (MaxIndStages > 0) { + g_gxState.indStages[0].scaleS = static_cast(bp_get(value, 4, 0)); + g_gxState.indStages[0].scaleT = static_cast(bp_get(value, 4, 4)); + } + if (MaxIndStages > 1) { + g_gxState.indStages[1].scaleS = static_cast(bp_get(value, 4, 8)); + g_gxState.indStages[1].scaleT = static_cast(bp_get(value, 4, 12)); + } + mark_pipeline_state_dirty(); + break; + } + case 0x26: { + if (MaxIndStages > 2) { + g_gxState.indStages[2].scaleS = static_cast(bp_get(value, 4, 0)); + g_gxState.indStages[2].scaleT = static_cast(bp_get(value, 4, 4)); + } + if (MaxIndStages > 3) { + g_gxState.indStages[3].scaleS = static_cast(bp_get(value, 4, 8)); + g_gxState.indStages[3].scaleT = static_cast(bp_get(value, 4, 12)); + } + mark_pipeline_state_dirty(); + break; + } + + // Indirect texture reference (0x27) + case 0x27: { + for (u32 i = 0; i < MaxIndStages; i++) { + g_gxState.indStages[i].texMapId = static_cast(bp_get(value, 3, i * 6)); + g_gxState.indStages[i].texCoordId = static_cast(bp_get(value, 3, i * 6 + 3)); + } + mark_pipeline_state_dirty(); + break; + } + + // TEV order / tref (0x28-0x2F) - 2 stages per register + case 0x28: + case 0x29: + case 0x2A: + case 0x2B: + case 0x2C: + case 0x2D: + case 0x2E: + case 0x2F: { + u32 idx = regId - 0x28; + u32 stage0 = idx * 2; + u32 stage1 = idx * 2 + 1; + + // Channel ID reverse mapping from hardware to GX + static const GXChannelID r2c[] = {GX_COLOR0A0, GX_COLOR1A1, GX_COLOR0A0, GX_COLOR1A1, + GX_COLOR0A0, GX_ALPHA_BUMP, GX_ALPHA_BUMPN, GX_COLOR_ZERO}; + + if (stage0 < MaxTevStages) { + auto& s = g_gxState.tevStages[stage0]; + s.texMapId = static_cast(bp_get(value, 3, 0)); + s.texCoordId = static_cast(bp_get(value, 3, 3)); + // bit 6 = tex enable + if (!bp_get(value, 1, 6)) { + s.texMapId = GX_TEXMAP_NULL; + } + u32 chanHw = bp_get(value, 3, 7); + s.channelId = (chanHw < 8) ? r2c[chanHw] : GX_COLOR_NULL; + } + if (stage1 < MaxTevStages) { + auto& s = g_gxState.tevStages[stage1]; + s.texMapId = static_cast(bp_get(value, 3, 12)); + s.texCoordId = static_cast(bp_get(value, 3, 15)); + if (!bp_get(value, 1, 18)) { + s.texMapId = GX_TEXMAP_NULL; + } + u32 chanHw = bp_get(value, 3, 19); + s.channelId = (chanHw < 8) ? r2c[chanHw] : GX_COLOR_NULL; + } + mark_pipeline_state_dirty(); + break; + } + + // Z mode (0x40) + case 0x40: { + g_gxState.depthCompare = bp_get(value, 1, 0) != 0; + g_gxState.depthFunc = static_cast(bp_get(value, 3, 1)); + g_gxState.depthUpdate = bp_get(value, 1, 4) != 0; + mark_pipeline_state_dirty(); + break; + } + + // Blend mode / cmode0 (0x41) + case 0x41: { + bool blendEn = bp_get(value, 1, 0) != 0; + bool logicEn = bp_get(value, 1, 1) != 0; + bool dither = bp_get(value, 1, 2) != 0; + g_gxState.colorUpdate = bp_get(value, 1, 3) != 0; + g_gxState.alphaUpdate = bp_get(value, 1, 4) != 0; + g_gxState.blendFacDst = static_cast(bp_get(value, 3, 5)); + g_gxState.blendFacSrc = static_cast(bp_get(value, 3, 8)); + bool subtract = bp_get(value, 1, 11) != 0; + g_gxState.blendOp = static_cast(bp_get(value, 4, 12)); + + if (subtract) { + g_gxState.blendMode = GX_BM_SUBTRACT; + } else if (blendEn) { + g_gxState.blendMode = GX_BM_BLEND; + } else if (logicEn) { + g_gxState.blendMode = GX_BM_LOGIC; + } else { + g_gxState.blendMode = GX_BM_NONE; + } + mark_pipeline_state_dirty(); + break; + } + + // Dst alpha / cmode1 (0x42) + case 0x42: { + u8 alpha = bp_get(value, 8, 0); + bool enabled = bp_get(value, 1, 8) != 0; + g_gxState.dstAlpha = enabled ? alpha : UINT32_MAX; + g_gxState.pixelFmt = decode_pixel_fmt(g_gxState.bpRegCache[0x43], value); + mark_pipeline_state_dirty(); + break; + } + + // PE control (0x43) - pixel format, z format, zcomp location + case 0x43: { + g_gxState.pixelFmt = decode_pixel_fmt(value, g_gxState.bpRegCache[0x42]); + g_gxState.zFmt = static_cast(bp_get(value, 3, 3)); + g_gxState.zCompLocBeforeTex = bp_get(value, 1, 6) != 0; + mark_pipeline_state_dirty(); + break; + } + case 0x44: + g_gxState.fieldMask = value & 0x3u; + break; + + // Bounding box clear/update registers (0x55, 0x56) + case 0x55: + case 0x56: { + const u32 offset = (regId & 2u); + g_gxState.boundingBox[offset] = static_cast(value & 0x3ffu); + g_gxState.boundingBox[offset + 1] = static_cast((value >> 10) & 0x3ffu); + break; + } + case 0x58: + g_gxState.revBits = value & 0x00FFFFFFu; + break; + + // Scissor box offset (0x59) + case 0x59: { + g_gxState.scissorOffsetX = static_cast(((value & 0x3ffu) << 1) - 0x156u); + g_gxState.scissorOffsetY = static_cast(((value & 0xffc00u) >> 9) - 0x156u); + set_logical_scissor(g_gxState.logicalScissor); + break; + } + + // TLUT load address / execute (0x64, 0x65) + case 0x64: + break; + case 0x65: { + const auto idx = bp_get(value, 10, 0); + if (idx < MaxTluts) { + auto& slot = g_gxState.loadedTluts[idx]; + slot.loadTlut0 = g_gxState.bpRegCache[0x64]; + slot.numEntries = static_cast(bp_get(value, 10, 10) + 1); + } + break; + } + case 0x68: + g_gxState.fieldMode = value & 0x1u; + break; + + // Alpha compare (0xF3) + case 0xF3: { + g_gxState.alphaCompare.ref0 = bp_get(value, 8, 0); + g_gxState.alphaCompare.ref1 = bp_get(value, 8, 8); + g_gxState.alphaCompare.comp0 = static_cast(bp_get(value, 3, 16)); + g_gxState.alphaCompare.comp1 = static_cast(bp_get(value, 3, 19)); + g_gxState.alphaCompare.op = static_cast(bp_get(value, 2, 22)); + mark_pipeline_state_dirty(); + break; + } + + // TEV K color/alpha select (0xF6-0xFD) + case 0xF6: + case 0xF7: + case 0xF8: + case 0xF9: + case 0xFA: + case 0xFB: + case 0xFC: + case 0xFD: { + u32 kselIdx = regId - 0xF6; + // Swap table entries (packed into pairs of ksel registers) + if (kselIdx < MaxTevSwap * 2) { + u32 swapIdx = kselIdx / 2; + if (kselIdx & 1) { + g_gxState.tevSwapTable[swapIdx].blue = static_cast(bp_get(value, 2, 0)); + g_gxState.tevSwapTable[swapIdx].alpha = static_cast(bp_get(value, 2, 2)); + } else { + g_gxState.tevSwapTable[swapIdx].red = static_cast(bp_get(value, 2, 0)); + g_gxState.tevSwapTable[swapIdx].green = static_cast(bp_get(value, 2, 2)); + } + } + // K color/alpha selection for 2 stages per register + u32 stage0 = kselIdx * 2; + u32 stage1 = kselIdx * 2 + 1; + if (stage0 < MaxTevStages) { + g_gxState.tevStages[stage0].kcSel = static_cast(bp_get(value, 5, 4)); + g_gxState.tevStages[stage0].kaSel = static_cast(bp_get(value, 5, 9)); + } + if (stage1 < MaxTevStages) { + g_gxState.tevStages[stage1].kcSel = static_cast(bp_get(value, 5, 14)); + g_gxState.tevStages[stage1].kaSel = static_cast(bp_get(value, 5, 19)); + } + mark_pipeline_state_dirty(); + break; + } + + // Fog A/B parameters (0xEE-0xF0) + // FOG0 (0xEE): A parameter - sign(1)|exp(8)|mantissa(11) partial IEEE 754 float + case 0xEE: { + g_gxState.fog.fog0Raw = value; + u32 a_mant = bp_get(value, 11, 0); + u32 a_exp = bp_get(value, 8, 11); + u32 a_sign = bp_get(value, 1, 19); + u32 a_bits = (a_sign << 31) | (a_exp << 23) | (a_mant << 12); + std::memcpy(&g_gxState.fog.aRaw, &a_bits, sizeof(g_gxState.fog.aRaw)); + u32 b_s = g_gxState.fog.fog2Raw & 0x1F; + g_gxState.fog.a = std::ldexp(g_gxState.fog.aRaw, static_cast(b_s)); + g_gxState.stateDirty = true; + break; + } + // FOG1 (0xEF): B mantissa (24-bit) + case 0xEF: { + g_gxState.fog.fog1Raw = value; + g_gxState.fog.bMagnitude = bp_get(value, 24, 0); + u32 b_s = g_gxState.fog.fog2Raw & 0x1F; + float B_mant = static_cast(g_gxState.fog.bMagnitude) / 8388638.0f; + g_gxState.fog.b = std::ldexp(B_mant, static_cast(b_s) - 1); + g_gxState.stateDirty = true; + break; + } + // FOG2 (0xF0): B shift/exponent (5-bit) + case 0xF0: { + g_gxState.fog.fog2Raw = value; + u32 b_s = bp_get(value, 5, 0); + g_gxState.fog.bShift = b_s; + u32 a_mant = bp_get(g_gxState.fog.fog0Raw, 11, 0); + u32 a_exp = bp_get(g_gxState.fog.fog0Raw, 8, 11); + u32 a_sign = bp_get(g_gxState.fog.fog0Raw, 1, 19); + u32 a_bits = (a_sign << 31) | (a_exp << 23) | (a_mant << 12); + std::memcpy(&g_gxState.fog.aRaw, &a_bits, sizeof(g_gxState.fog.aRaw)); + g_gxState.fog.a = std::ldexp(g_gxState.fog.aRaw, static_cast(b_s)); + g_gxState.fog.bMagnitude = bp_get(g_gxState.fog.fog1Raw, 24, 0); + float B_mant = static_cast(g_gxState.fog.bMagnitude) / 8388638.0f; + g_gxState.fog.b = std::ldexp(B_mant, static_cast(b_s) - 1); + g_gxState.stateDirty = true; + break; + } + + // Fog type + C parameter from FOG3 (0xF1) + case 0xF1: { + const u32 fogFunc = bp_get(value, 3, 21); + const u32 fogProj = bp_get(value, 1, 20); + GXFogType fogType = static_cast(fogFunc | (fogProj << 3)); + g_gxState.fog.type = fogType; + // Decode C parameter (same partial float encoding as A) + u32 c_mant = bp_get(value, 11, 0); + u32 c_exp = bp_get(value, 8, 11); + u32 c_sign = bp_get(value, 1, 19); + u32 c_bits = (c_sign << 31) | (c_exp << 23) | (c_mant << 12); + std::memcpy(&g_gxState.fog.c, &c_bits, sizeof(g_gxState.fog.c)); + mark_pipeline_state_dirty(); + break; + } + + // Fog color from FOGCLR (0xF2) + case 0xF2: { + u8 b = bp_get(value, 8, 0); + u8 g = bp_get(value, 8, 8); + u8 r = bp_get(value, 8, 16); + g_gxState.fog.color = { + static_cast(r) / 255.f, + static_cast(g) / 255.f, + static_cast(b) / 255.f, + 1.f, + }; + g_gxState.stateDirty = true; + break; + } + + // TEV and K color registers (0xE0-0xE7): even are RA, odd are BG. + // Bit 23 selects a K color register over a TEV color register. + case 0xE0: + case 0xE1: + case 0xE2: + case 0xE3: + case 0xE4: + case 0xE5: + case 0xE6: + case 0xE7: { + u32 idx = (regId - 0xE0) / 2; + bool isRA = (regId & 1) == 0; + bool isKColor = bp_get(value, 1, 23) != 0; + + if (isKColor) { + // K color register (8-bit components) + if (idx < GX_MAX_KCOLOR) { + auto& kc = g_gxState.kcolors[idx]; + if (isRA) { + kc[0] = static_cast(bp_get(value, 8, 0)) / 255.f; // R + kc[3] = static_cast(bp_get(value, 8, 12)) / 255.f; // A + } else { + kc[2] = static_cast(bp_get(value, 8, 0)) / 255.f; // B + kc[1] = static_cast(bp_get(value, 8, 12)) / 255.f; // G + } + g_gxState.stateDirty = true; + } + } else { + // TEV color register (11-bit signed components) + if (idx < MaxTevRegs) { + auto& cr = g_gxState.colorRegs[idx]; + if (isRA) { + // 11-bit signed: sign-extend from 11 bits + s32 r = bp_get(value, 11, 0); + if (r & 0x400) + r |= ~0x7FF; // sign extend + s32 a = bp_get(value, 11, 12); + if (a & 0x400) + a |= ~0x7FF; + cr[0] = static_cast(r) / 255.f; + cr[3] = static_cast(a) / 255.f; + } else { + s32 b = bp_get(value, 11, 0); + if (b & 0x400) + b |= ~0x7FF; + s32 g = bp_get(value, 11, 12); + if (g & 0x400) + g |= ~0x7FF; + cr[2] = static_cast(b) / 255.f; + cr[1] = static_cast(g) / 255.f; + } + g_gxState.stateDirty = true; + } + } + break; + } + + // Indirect texture matrices (0x06-0x0E), three consecutive registers per 3x2 matrix: + // matrix 0 at 0x06, matrix 1 at 0x09, matrix 2 at 0x0C. + case 0x06: + case 0x07: + case 0x08: + case 0x09: + case 0x0A: + case 0x0B: + case 0x0C: + case 0x0D: + case 0x0E: { + u32 idx = (regId - 0x06) / 3; // matrix index (0-2) + u32 column = (regId - 0x06) % 3; // column index (0-2) + auto& info = g_gxState.indTexMtxs[idx]; + + // Decode one packed matrix column: [m[0][column], m[1][column]]. + s32 col0 = bp_get(value, 11, 0); + if (col0 & 0x400) + col0 |= ~0x7FF; // sign-extend from 11 bits + s32 col1 = bp_get(value, 11, 11); + if (col1 & 0x400) + col1 |= ~0x7FF; + + auto& packedColumn = column == 0 ? info.mtx.m0 : (column == 1 ? info.mtx.m1 : info.mtx.m2); + packedColumn.x = static_cast(col0) / 1024.0f; + packedColumn.y = static_cast(col1) / 1024.0f; + + // Accumulate the indirect matrix scale exponent. The SDK writes two bits per column, but the + // hardware ignores the third column's top bit, leaving 5 bits for adjScale = scaleExp + 17. + u32 scaleBits = bp_get(value, 2, 22); + u32 shift = column * 2; + if (column == 2) { + info.adjScaleRaw = (info.adjScaleRaw & ~(1u << shift)) | ((scaleBits & 1u) << shift); + } else { + info.adjScaleRaw = (info.adjScaleRaw & ~(3u << shift)) | (scaleBits << shift); + } + info.scaleExp = static_cast(info.adjScaleRaw) - 17; + + g_gxState.stateDirty = true; + break; + } + + // SU texture coordinate scale registers (0x30-0x3F): even (suTs0) carry S-axis scale, bias, cyl + // wrap and line/point offset; odd (suTs1) carry the T-axis equivalents. + case 0x30: + case 0x31: + case 0x32: + case 0x33: + case 0x34: + case 0x35: + case 0x36: + case 0x37: + case 0x38: + case 0x39: + case 0x3A: + case 0x3B: + case 0x3C: + case 0x3D: + case 0x3E: + case 0x3F: { + u32 coordIdx = (regId - 0x30) / 2; + bool isT = (regId & 1) != 0; + auto& tcs = g_gxState.texCoordScales[coordIdx]; + if (isT) { + tcs.scaleT = static_cast(bp_get(value, 16, 0)); + tcs.biasT = bp_get(value, 1, 16) != 0; + tcs.cylWrapT = bp_get(value, 1, 17) != 0; + } else { + tcs.scaleS = static_cast(bp_get(value, 16, 0)); + tcs.biasS = bp_get(value, 1, 16) != 0; + tcs.cylWrapS = bp_get(value, 1, 17) != 0; + tcs.lineOffset = bp_get(value, 1, 18) != 0; + tcs.pointOffset = bp_get(value, 1, 19) != 0; + } + g_gxState.stateDirty = true; + break; + } + + // Copy clear color (0x4F-0x50) and depth (0x51) + case 0x49: { + g_gxState.dispCopySrc.x = static_cast(bp_get(value, 10, 0)); + g_gxState.dispCopySrc.y = static_cast(bp_get(value, 10, 10)); + break; + } + case 0x4A: { + g_gxState.dispCopySrc.width = static_cast(bp_get(value, 10, 0) + 1u); + g_gxState.dispCopySrc.height = static_cast(bp_get(value, 10, 10) + 1u); + break; + } + case 0x4D: { + g_gxState.dispCopyDstWidth = static_cast(((bp_get(value, 10, 0) << 5) + 31u) >> 1); + break; + } + case 0x4E: { + const u32 iScale = bp_get(value, 9, 0); + if (iScale != 0) { + g_gxState.dispCopyYScale = 256.f / static_cast(iScale); + } + break; + } + case 0x4F: { + u8 r = bp_get(value, 8, 0); + u8 a = bp_get(value, 8, 8); + g_gxState.clearColor[0] = static_cast(r) / 255.f; + g_gxState.clearColor[3] = static_cast(a) / 255.f; + g_gxState.stateDirty = true; + break; + } + case 0x50: { + u8 b = bp_get(value, 8, 0); + u8 g = bp_get(value, 8, 8); + g_gxState.clearColor[2] = static_cast(b) / 255.f; + g_gxState.clearColor[1] = static_cast(g) / 255.f; + g_gxState.stateDirty = true; + break; + } + case 0x51: { + g_gxState.clearDepth = bp_get(value, 24, 0); + g_gxState.stateDirty = true; + break; + } + case 0xF4: { + g_gxState.zTextureBias = value & 0x00FFFFFFu; + mark_pipeline_state_dirty(); + break; + } + case 0xF5: { + g_gxState.zTextureFmt = static_cast(bp_get(value, 2, 0)); + g_gxState.zTextureOp = static_cast(bp_get(value, 2, 2)); + mark_pipeline_state_dirty(); + break; + } + case 0x52: { + g_gxState.copyClamp = static_cast(bp_get(value, 2, 0)); + g_gxState.texCopyFmt = static_cast(bp_get(value, 4, 3)); + g_gxState.dispCopyGamma = static_cast(bp_get(value, 2, 7)); + g_gxState.texCopyHalfScale = bp_get(value, 1, 9) != 0; + g_gxState.dispCopyFrame2Field = bp_get(value, 2, 12); + break; + } + case 0x53: { + g_gxState.copyFilterVFilter[0] = static_cast(bp_get(value, 6, 0)); + g_gxState.copyFilterVFilter[1] = static_cast(bp_get(value, 6, 6)); + g_gxState.copyFilterVFilter[2] = static_cast(bp_get(value, 6, 12)); + g_gxState.copyFilterVFilter[3] = static_cast(bp_get(value, 6, 18)); + refresh_copy_filter_flags(); + break; + } + case 0x54: { + g_gxState.copyFilterVFilter[4] = static_cast(bp_get(value, 6, 0)); + g_gxState.copyFilterVFilter[5] = static_cast(bp_get(value, 6, 6)); + g_gxState.copyFilterVFilter[6] = static_cast(bp_get(value, 6, 12)); + refresh_copy_filter_flags(); + break; + } + case 0xE8: + case 0xE9: + case 0xEA: + case 0xEB: + case 0xEC: + case 0xED: + g_gxState.fogRange[regId - 0xE8] = value & 0x00FFFFFFu; + mark_pipeline_state_dirty(); + break; + + default: + if (const auto mapping = decode_tex_bp_reg(regId); mapping.has_value()) { + auto& slot = g_gxState.loadedTextures[mapping->texMapId]; + bool changed = false; + switch (mapping->kind) { + case TexBpRegMapping::Kind::Mode0: + changed = slot.mode0 != value; + if (changed) { + slot.mode0 = value; + } + break; + case TexBpRegMapping::Kind::Mode1: + changed = slot.mode1 != value; + if (changed) { + slot.mode1 = value; + } + break; + case TexBpRegMapping::Kind::Image0: + changed = slot.image0 != value; + if (changed) { + slot.image0 = value; + slot.mWidth = 0; + slot.mHeight = 0; + slot.mFormat = gfx::InvalidTextureFormat; + } + break; + case TexBpRegMapping::Kind::Image3: + changed = slot.image3 != value; + if (changed) { + slot.image3 = value; + } + break; + case TexBpRegMapping::Kind::Tlut: + // TLUT region's TMEM offset + break; + case TexBpRegMapping::Kind::Image1: + case TexBpRegMapping::Kind::Image2: + // GXTexRegion regs + break; + } + if (changed) { + g_gxState.stateDirty = true; + } + } else { +#ifndef NDEBUG + Log.debug("Unhandled BP register 0x{:02X} (value 0x{:06X})", regId, value & 0xFFFFFF); +#endif + } + break; + } +} + +extern "C" void GXApplyBPReg(u8 reg, u32 value) { + handle_bp((static_cast(reg) << 24) | (value & 0x00FFFFFFu), true); +} + +static bool cacheable_cp_register(u8 addr) { + return addr == 0x30 || addr == 0x40 || addr == 0x50 || addr == 0x60 || (addr >= 0x70 && addr <= 0x97); +} + +static std::array s_cpRegisterCache{}; +static std::array s_cpRegisterCacheValid{}; + +void reset_cp_register_cache() { + s_cpRegisterCache.fill(0); + s_cpRegisterCacheValid.fill(false); +} + +static bool cp_register_write_unchanged(u8 addr, u32 value) { + if (!cacheable_cp_register(addr)) return false; + + if (s_cpRegisterCacheValid[addr] && s_cpRegisterCache[addr] == value) { + return true; + } + s_cpRegisterCacheValid[addr] = true; + s_cpRegisterCache[addr] = value; + return false; +} + +// CP register handler - decodes CP register writes and updates g_gxState +static void handle_cp(u8 addr, u32 value, bool bigEndian) { + if (cp_register_write_unchanged(addr, value)) return; + + switch (addr) { + // VCD low (0x50) + case 0x50: { + auto& vd = g_gxState.vtxDesc; + auto& svd = g_gxState.sourceVtxDesc; + vd[GX_VA_PNMTXIDX] = static_cast(bp_get(value, 1, 0)); + vd[GX_VA_TEX0MTXIDX] = static_cast(bp_get(value, 1, 1)); + vd[GX_VA_TEX1MTXIDX] = static_cast(bp_get(value, 1, 2)); + vd[GX_VA_TEX2MTXIDX] = static_cast(bp_get(value, 1, 3)); + vd[GX_VA_TEX3MTXIDX] = static_cast(bp_get(value, 1, 4)); + vd[GX_VA_TEX4MTXIDX] = static_cast(bp_get(value, 1, 5)); + vd[GX_VA_TEX5MTXIDX] = static_cast(bp_get(value, 1, 6)); + vd[GX_VA_TEX6MTXIDX] = static_cast(bp_get(value, 1, 7)); + vd[GX_VA_TEX7MTXIDX] = static_cast(bp_get(value, 1, 8)); + vd[GX_VA_POS] = static_cast(bp_get(value, 2, 9)); + vd[GX_VA_NRM] = static_cast(bp_get(value, 2, 11)); + vd[GX_VA_CLR0] = static_cast(bp_get(value, 2, 13)); + vd[GX_VA_CLR1] = static_cast(bp_get(value, 2, 15)); + for (int attr = GX_VA_PNMTXIDX; attr <= GX_VA_CLR1; ++attr) { + svd[attr] = vd[attr]; + } + mark_pipeline_state_dirty(); + g_gxState.clearVtxSizeCache(); + break; + } + + // VCD high (0x60) + case 0x60: { + auto& vd = g_gxState.vtxDesc; + auto& svd = g_gxState.sourceVtxDesc; + vd[GX_VA_TEX0] = static_cast(bp_get(value, 2, 0)); + vd[GX_VA_TEX1] = static_cast(bp_get(value, 2, 2)); + vd[GX_VA_TEX2] = static_cast(bp_get(value, 2, 4)); + vd[GX_VA_TEX3] = static_cast(bp_get(value, 2, 6)); + vd[GX_VA_TEX4] = static_cast(bp_get(value, 2, 8)); + vd[GX_VA_TEX5] = static_cast(bp_get(value, 2, 10)); + vd[GX_VA_TEX6] = static_cast(bp_get(value, 2, 12)); + vd[GX_VA_TEX7] = static_cast(bp_get(value, 2, 14)); + for (int attr = GX_VA_TEX0; attr <= GX_VA_TEX7; ++attr) { + svd[attr] = vd[attr]; + } + mark_pipeline_state_dirty(); + g_gxState.clearVtxSizeCache(); + break; + } + + // Matrix index A (0x30) + case 0x30: { + g_gxState.currentPnMtx = bp_get(value, 6, 0) / 3; + for (u32 i = 0; i < 4 && i < MaxTexCoord; i++) { + auto texMtx = static_cast(bp_get(value, 6, 6 + i * 6)); + assert(texMtx >= 0 && texMtx <= GXTexMtx::GX_IDENTITY); + g_gxState.tcgs[i].mtx = texMtx; + } + // Same matrix indices as XF 0x18, written from a different bank. + g_gxState.invalidateXfReg(0x18); + mark_pipeline_state_dirty(); + break; + } + + // Matrix index B (0x40) + case 0x40: { + for (u32 i = 0; i < 4 && (i + 4) < MaxTexCoord; i++) { + auto texMtx = static_cast(bp_get(value, 6, i * 6)); + assert(texMtx >= 0 && texMtx <= GXTexMtx::GX_IDENTITY); + g_gxState.tcgs[i + 4].mtx = texMtx; + } + // Same matrix indices as XF 0x19, written from a different bank. + g_gxState.invalidateXfReg(0x19); + mark_pipeline_state_dirty(); + break; + } + + default: + // VAT A registers (0x70-0x77) + if (addr >= 0x70 && addr <= 0x77) { + u32 fmt = addr - 0x70; + auto& vf = g_gxState.vtxFmts[fmt]; + vf.attrs[GX_VA_POS].cnt = static_cast(bp_get(value, 1, 0)); + vf.attrs[GX_VA_POS].type = static_cast(bp_get(value, 3, 1)); + vf.attrs[GX_VA_POS].frac = static_cast(bp_get(value, 5, 4)); + vf.attrs[GX_VA_NRM].type = static_cast(bp_get(value, 3, 10)); + if (bp_get(value, 1, 31) != 0) { + vf.attrs[GX_VA_NRM].cnt = GX_NRM_NBT3; + } else { + vf.attrs[GX_VA_NRM].cnt = bp_get(value, 1, 9) != 0 ? GX_NRM_NBT : GX_NRM_XYZ; + } + vf.attrs[GX_VA_NRM].frac = normal_frac_bits(vf.attrs[GX_VA_NRM].type); + vf.attrs[GX_VA_CLR0].cnt = static_cast(bp_get(value, 1, 13)); + vf.attrs[GX_VA_CLR0].type = static_cast(bp_get(value, 3, 14)); + vf.attrs[GX_VA_CLR0].frac = 0; + vf.attrs[GX_VA_CLR1].cnt = static_cast(bp_get(value, 1, 17)); + vf.attrs[GX_VA_CLR1].type = static_cast(bp_get(value, 3, 18)); + vf.attrs[GX_VA_CLR1].frac = 0; + vf.attrs[GX_VA_TEX0].cnt = static_cast(bp_get(value, 1, 21)); + vf.attrs[GX_VA_TEX0].type = static_cast(bp_get(value, 3, 22)); + vf.attrs[GX_VA_TEX0].frac = static_cast(bp_get(value, 5, 25)); + mark_pipeline_state_dirty(); + g_gxState.clearVtxSizeCache(); + } + // VAT B registers (0x80-0x87) + else if (addr >= 0x80 && addr <= 0x87) { + u32 fmt = addr - 0x80; + auto& vf = g_gxState.vtxFmts[fmt]; + vf.attrs[GX_VA_TEX1].cnt = static_cast(bp_get(value, 1, 0)); + vf.attrs[GX_VA_TEX1].type = static_cast(bp_get(value, 3, 1)); + vf.attrs[GX_VA_TEX1].frac = static_cast(bp_get(value, 5, 4)); + vf.attrs[GX_VA_TEX2].cnt = static_cast(bp_get(value, 1, 9)); + vf.attrs[GX_VA_TEX2].type = static_cast(bp_get(value, 3, 10)); + vf.attrs[GX_VA_TEX2].frac = static_cast(bp_get(value, 5, 13)); + vf.attrs[GX_VA_TEX3].cnt = static_cast(bp_get(value, 1, 18)); + vf.attrs[GX_VA_TEX3].type = static_cast(bp_get(value, 3, 19)); + vf.attrs[GX_VA_TEX3].frac = static_cast(bp_get(value, 5, 22)); + vf.attrs[GX_VA_TEX4].cnt = static_cast(bp_get(value, 1, 27)); + vf.attrs[GX_VA_TEX4].type = static_cast(bp_get(value, 3, 28)); + // TEX4 frac is in VAT C + mark_pipeline_state_dirty(); + g_gxState.clearVtxSizeCache(); + } + // VAT C registers (0x90-0x97) + else if (addr >= 0x90 && addr <= 0x97) { + u32 fmt = addr - 0x90; + auto& vf = g_gxState.vtxFmts[fmt]; + vf.attrs[GX_VA_TEX4].frac = static_cast(bp_get(value, 5, 0)); + vf.attrs[GX_VA_TEX5].cnt = static_cast(bp_get(value, 1, 5)); + vf.attrs[GX_VA_TEX5].type = static_cast(bp_get(value, 3, 6)); + vf.attrs[GX_VA_TEX5].frac = static_cast(bp_get(value, 5, 9)); + vf.attrs[GX_VA_TEX6].cnt = static_cast(bp_get(value, 1, 14)); + vf.attrs[GX_VA_TEX6].type = static_cast(bp_get(value, 3, 15)); + vf.attrs[GX_VA_TEX6].frac = static_cast(bp_get(value, 5, 18)); + vf.attrs[GX_VA_TEX7].cnt = static_cast(bp_get(value, 1, 23)); + vf.attrs[GX_VA_TEX7].type = static_cast(bp_get(value, 3, 24)); + vf.attrs[GX_VA_TEX7].frac = static_cast(bp_get(value, 5, 27)); + mark_pipeline_state_dirty(); + g_gxState.clearVtxSizeCache(); + } + // Array base addresses (0xA0-0xAF) + else if (addr >= 0xA0 && addr <= 0xAF) { + Log.error("CP_REG_ARRAYBASE_ID is not supported on Aurora. Use GX_LOAD_AURORA_ARRAYBASE instead."); + } + // Array strides (0xB0-0xBF) + else if (addr >= 0xB0 && addr <= 0xBF) { + u32 attrIdx = addr - 0xB0 + GX_VA_POS; + if (attrIdx < GX_VA_MAX_ATTR) { + auto& array = g_gxState.arrays[attrIdx]; + const auto newStride = static_cast(value); + if (array.stride != newStride) { + array.stride = newStride; + mark_pipeline_state_dirty(); + } + } + } + break; + } +} + +// XF register handler - decodes XF (transform unit) register writes and updates g_gxState +static void handle_xf(const u8* data, u32& pos, u32 size, bool bigEndian) { + // These bounds must hold in release too: CHECK() is a no-op under NDEBUG, so relying on it alone let a truncated guest display list read past `data`. + if (pos > size || size - pos < 4) UNLIKELY { + CHECK(false, "XF header read overrun"); + pos = size; + return; + } + u32 header = read_u32(data + pos, bigEndian); + pos += 4; + + u32 count = ((header >> 16) & 0xFFFF) + 1; + u32 addr = header & 0xFFFF; + u32 dataBytes = count * 4; + // Log.warn(" xf: addr {:04x} count {} dataBytes {} pos {} -> {}", addr, count, dataBytes, pos, pos + dataBytes); + if (size - pos < dataBytes) UNLIKELY { + CHECK(false, "XF data read overrun: need {} bytes at pos {}", dataBytes, pos); + pos = size; + return; + } + + const u8* xfData = data + pos; + + if (copy_xf_data(addr, xfData, count, bigEndian)) { + // copy_xf_data handled everything. + } else if (addr >= 0x1000) { + // XF registers (0x1000+) + u32 xfAddr = addr - 0x1000; + bool viewportUpdated = false; + bool projectionUpdated = false; + for (u32 i = 0; i < count; i++) { + u32 reg = xfAddr + i; + u32 val = read_u32(xfData + i * 4, bigEndian); + + // Skip register writes that decode to state we already hold. + const bool cacheable = reg < g_gxState.xfRegCache.size(); + const bool unchanged = cacheable && g_gxState.xfRegMatches(reg, val); + if (cacheable) g_gxState.storeXfReg(reg, val); + // Viewport (0x1A-0x1F) and projection (0x20-0x26) keep their unconditional apply below; only the banks that already had skip semantics and the TexGen bank drop out here. + if (unchanged && (reg <= 0x19 || reg >= 0x3F)) continue; + + switch (reg) { + case 0x00: + g_gxState.xfError = val; + break; + case 0x08: + // XF vertex specs (numColors, numNormals, numTexCoords) - informational + break; + case 0x09: + // numChans + g_gxState.numChans = val; + g_gxState.stateDirty = true; + break; + case 0x0A: + // Ambient color 0 + g_gxState.colorChannelState[GX_COLOR0].ambColor = unpack_color(val); + g_gxState.colorChannelState[GX_ALPHA0].ambColor = unpack_color(val); + g_gxState.stateDirty = true; + break; + case 0x0B: + // Ambient color 1 + g_gxState.colorChannelState[GX_COLOR1].ambColor = unpack_color(val); + g_gxState.colorChannelState[GX_ALPHA1].ambColor = unpack_color(val); + g_gxState.stateDirty = true; + break; + case 0x0C: + // Material color 0 + g_gxState.colorChannelState[GX_COLOR0].matColor = unpack_color(val); + g_gxState.colorChannelState[GX_ALPHA0].matColor = unpack_color(val); + g_gxState.stateDirty = true; + break; + case 0x0D: + // Material color 1 + g_gxState.colorChannelState[GX_COLOR1].matColor = unpack_color(val); + g_gxState.colorChannelState[GX_ALPHA1].matColor = unpack_color(val); + g_gxState.stateDirty = true; + break; + case 0x0E: + case 0x0F: + case 0x10: + case 0x11: { + // Channel control registers + u32 chanId = reg - 0x0E; + if (chanId < MaxColorChannels) { + auto& chan = g_gxState.colorChannelConfig[chanId]; + chan.matSrc = static_cast(bp_get(val, 1, 0)); + chan.lightingEnabled = bp_get(val, 1, 1) != 0; + u32 lightsLo = bp_get(val, 4, 2); + chan.ambSrc = static_cast(bp_get(val, 1, 6)); + chan.diffFn = static_cast(bp_get(val, 2, 7)); + u32 lightsHi = bp_get(val, 4, 11); + switch (bp_get(val, 2, 9)) { + case 1: + chan.attnFn = GX_AF_SPEC; + break; + case 3: + chan.attnFn = GX_AF_SPOT; + break; + case 0: + case 2: + default: + chan.attnFn = GX_AF_NONE; + break; + } + u32 lightMask = lightsLo | (lightsHi << 4); + g_gxState.colorChannelState[chanId].lightMask = GX::LightMask{lightMask}; + mark_pipeline_state_dirty(); + } + break; + } + case 0x12: + g_gxState.dualTex = val; + mark_pipeline_state_dirty(); + break; + case 0x18: { + // Matrix index A: PnMtx + TexCoord0-3 matrix indices + g_gxState.currentPnMtx = bp_get(val, 6, 0) / 3; + for (u32 i = 0; i < 4 && i < MaxTexCoord; i++) { + auto texMtx = static_cast(bp_get(val, 6, 6 + i * 6)); + assert(texMtx >= 0 && texMtx <= GXTexMtx::GX_IDENTITY); + g_gxState.tcgs[i].mtx = texMtx; + } + mark_pipeline_state_dirty(); + break; + } + case 0x19: { + // Matrix index B: TexCoord4-7 matrix indices + for (u32 i = 0; i < 4 && (i + 4) < MaxTexCoord; i++) { + g_gxState.tcgs[i + 4].mtx = static_cast(bp_get(val, 6, i * 6)); + } + mark_pipeline_state_dirty(); + break; + } + case 0x1A: + case 0x1B: + case 0x1C: + case 0x1D: + case 0x1E: + case 0x1F: { + // Viewport: sx, sy, sz, ox, oy, oz at XF 0x101A-0x101F + const u32 vpOff = reg - 0x1A; + g_gxState.xfViewport[vpOff] = read_f32(xfData + i * 4, bigEndian); + viewportUpdated = true; + break; + } + case 0x20: + case 0x21: + case 0x22: + case 0x23: + case 0x24: + case 0x25: + case 0x26: { + // Projection: 6 params + type at XF 0x1020-0x1026 + const u32 projOff = reg - 0x20; + if (projOff < g_gxState.xfProjection.size()) { + g_gxState.xfProjection[projOff] = read_f32(xfData + i * 4, bigEndian); + } else { + g_gxState.projType = static_cast(val); + } + projectionUpdated = true; + break; + } + case 0x3F: + // numTexGens + g_gxState.numTexGens = val; + mark_pipeline_state_dirty(); + break; + default: + // TexGen config (0x40-0x4F) and post-transform (0x50-0x5F) + if (reg >= 0x40 && reg <= 0x4F) { + u32 tcIdx = reg - 0x40; + if (tcIdx < MaxTexCoord) { + auto& tcg = g_gxState.tcgs[tcIdx]; + bool proj = bp_get(val, 1, 1) != 0; + u32 form = bp_get(val, 1, 2); + u32 tgType = bp_get(val, 3, 4); + u32 srcRow = bp_get(val, 5, 7); + tcg.inputFormAB11 = form == 0; + + if (tgType == 0) { + tcg.type = proj ? GX_TG_MTX3x4 : GX_TG_MTX2x4; + } else if (tgType == 1) { + // Bump mapping + tcg.type = static_cast(bp_get(val, 3, 15) + 2); + } else if (tgType == 2 || tgType == 3) { + tcg.type = GX_TG_SRTG; + } + + // Decode source from row + static const GXTexGenSrc rowToSrc[] = {GX_TG_POS, GX_TG_NRM, GX_TG_COLOR0, GX_TG_BINRM, GX_TG_TANGENT, + GX_TG_TEX0, GX_TG_TEX1, GX_TG_TEX2, GX_TG_TEX3, GX_TG_TEX4, + GX_TG_TEX5, GX_TG_TEX6, GX_TG_TEX7}; + if (srcRow < 13) { + tcg.src = rowToSrc[srcRow]; + } + mark_pipeline_state_dirty(); + } + } else if (reg >= 0x50 && reg <= 0x5F) { + u32 tcIdx = reg - 0x50; + if (tcIdx < MaxTexCoord) { + g_gxState.tcgs[tcIdx].postMtx = static_cast(bp_get(val, 6, 0) + 64); + g_gxState.tcgs[tcIdx].normalize = bp_get(val, 1, 8) != 0; + mark_pipeline_state_dirty(); + } + } else { +#ifndef NDEBUG + Log.debug("Unhandled XF register 0x{:04X} (value 0x{:08X})", reg, val); +#endif + } + break; + } + } + if (viewportUpdated) { + apply_xf_viewport(); + } + if (projectionUpdated) { + apply_xf_projection(); + } + } + + pos += dataBytes; +} + +static void handle_draw_overrun(u8 cmd, u16 vtxCount, u32 vtxSize, u32 totalVtxBytes, const u8* data, const u32& pos, + u32 size) { + static u32 truncatedDrawLogCount = 0; + if (truncatedDrawLogCount >= 64) { + if (truncatedDrawLogCount == 64) { + Log.warn("suppressing further truncated draw diagnostics"); + } + ++truncatedDrawLogCount; + return; + } + ++truncatedDrawLogCount; + + // Hex dump around the draw command for debugging + u32 cmdPos = pos - 2 - 1; // opcode byte position (before vtxCount and pos++) + u32 dumpStart = (cmdPos > 16) ? cmdPos - 16 : 0; + u32 dumpEnd = (cmdPos + 32 < size) ? cmdPos + 32 : size; + std::string hex; + for (u32 i = dumpStart; i < dumpEnd; i++) { + if (i == cmdPos) + hex += fmt::format("[{:02x}]", data[i]); + else + hex += fmt::format(" {:02x}", data[i]); + } + Log.warn(" hex dump around truncated draw cmd (pos {}-{}):{}", dumpStart, dumpEnd - 1, hex); + const auto fmt = static_cast(cmd & CP_VAT_MASK); + const auto& vtxFmt = g_gxState.vtxFmts[fmt]; + Log.warn(" truncated draw cmd=0x{:02X} fmt={} vtxCount={} vtxSize={} desc pn={} pos={} nrm={} clr0={} clr1={} " + "tex0={} tex1={} tex2={} tex3={} tex4={} tex5={} tex6={} tex7={}", + cmd, static_cast(fmt), vtxCount, vtxSize, g_gxState.vtxDesc[GX_VA_PNMTXIDX], + g_gxState.vtxDesc[GX_VA_POS], g_gxState.vtxDesc[GX_VA_NRM], g_gxState.vtxDesc[GX_VA_CLR0], + g_gxState.vtxDesc[GX_VA_CLR1], g_gxState.vtxDesc[GX_VA_TEX0], g_gxState.vtxDesc[GX_VA_TEX1], + g_gxState.vtxDesc[GX_VA_TEX2], g_gxState.vtxDesc[GX_VA_TEX3], g_gxState.vtxDesc[GX_VA_TEX4], + g_gxState.vtxDesc[GX_VA_TEX5], g_gxState.vtxDesc[GX_VA_TEX6], g_gxState.vtxDesc[GX_VA_TEX7]); + Log.warn(" fmt {} attrs pos({},{}) nrm({},{}) clr0({},{}) tex0({},{}) tex1({},{})", + static_cast(fmt), static_cast(vtxFmt.attrs[GX_VA_POS].cnt), + static_cast(vtxFmt.attrs[GX_VA_POS].type), static_cast(vtxFmt.attrs[GX_VA_NRM].cnt), + static_cast(vtxFmt.attrs[GX_VA_NRM].type), static_cast(vtxFmt.attrs[GX_VA_CLR0].cnt), + static_cast(vtxFmt.attrs[GX_VA_CLR0].type), static_cast(vtxFmt.attrs[GX_VA_TEX0].cnt), + static_cast(vtxFmt.attrs[GX_VA_TEX0].type), static_cast(vtxFmt.attrs[GX_VA_TEX1].cnt), + static_cast(vtxFmt.attrs[GX_VA_TEX1].type)); + Log.warn("stopping FIFO decode at truncated draw: need {} bytes at pos {}, have {}", totalVtxBytes, pos, size); +} + +// Draw command handler - parses vertices inline and caches results +static u32 calculate_last_vtx_size(GXVtxFmt fmt) { + u32 vtxSize = 0; + const auto& vtxFmt = g_gxState.vtxFmts[fmt]; + for (int i = GX_VA_PNMTXIDX; i <= GX_VA_TEX7; ++i) { + const auto attr = static_cast(i); + const auto& attrFmt = vtxFmt.attrs[i]; + switch (g_gxState.vtxDesc[i]) { + case GX_NONE: + break; + case GX_DIRECT: { + vtxSize += comp_type_size(attr, attrFmt.type) * comp_cnt_count(attr, attrFmt.cnt); + break; + } + case GX_INDEX8: + vtxSize += (attr == GX_VA_NRM && attrFmt.cnt == GX_NRM_NBT3) ? 3 : 1; + break; + case GX_INDEX16: + vtxSize += (attr == GX_VA_NRM && attrFmt.cnt == GX_NRM_NBT3) ? 6 : 2; + break; + } + } + + g_gxState.lastVtxFmt = fmt; + g_gxState.lastVtxSize = vtxSize; + + return vtxSize; +} + +static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, + gfx::Range vertRange, uint16_t usedPnMtxMask, + HashType matrixTopologySignature, + HashType geometrySignature, bool interpolationIdentityActive); + +// The per-draw geometry signature, matrix-usage mask and draw-identity hashes exist purely to feed frame interpolation (build_uniform consumes them only after its `frame_interpolation_fps() == 0` early-out). +static inline bool frame_interpolation_identity_needed() noexcept { + return frame_interpolation_active() && g_gxState.projType == GX_PERSPECTIVE; +} + +static uint32_t matrix_index_prefix_size(GXVtxFmt fmt) noexcept { + const auto& vtxFmt = g_gxState.vtxFmts[fmt]; + uint32_t size = 0; + for (int i = GX_VA_PNMTXIDX; i < GX_VA_POS; ++i) { + const auto attr = static_cast(i); + switch (g_gxState.vtxDesc[i]) { + case GX_NONE: + break; + case GX_DIRECT: + size += comp_type_size(attr, vtxFmt.attrs[i].type) * + comp_cnt_count(attr, vtxFmt.attrs[i].cnt); + break; + case GX_INDEX8: + ++size; + break; + case GX_INDEX16: + size += 2; + break; + } + } + return size; +} + +static HashType draw_geometry_signature(GXVtxFmt fmt, const uint8_t* vertices, + uint16_t vtxCount, uint32_t vtxStride) noexcept { + Hasher hasher; + hasher.update(vtxCount); + hasher.update(vtxStride); + + // Matrix-index bytes select an instance's current XF palette slots, so they are deliberately excluded from mesh identity. + const uint32_t matrixPrefix = std::min(matrix_index_prefix_size(fmt), vtxStride); + if (matrixPrefix == 0) { + // Most draws have no direct matrix-index prefix. + hasher.update(vertices, static_cast(vtxCount) * vtxStride); + } else { + for (uint16_t vertex = 0; vertex < vtxCount; ++vertex) { + hasher.update(vertices + static_cast(vertex) * vtxStride + matrixPrefix, + vtxStride - matrixPrefix); + } + } + + // Identical index streams can address different vertex arrays. + for (int i = GX_VA_POS; i <= GX_VA_TEX7; ++i) { + if (g_gxState.vtxDesc[i] != GX_INDEX8 && g_gxState.vtxDesc[i] != GX_INDEX16) { + continue; + } + const auto& array = g_gxState.arrays[i]; + const auto attribute = static_cast(i); + const auto source = reinterpret_cast(array.data); + hasher.update(attribute); + hasher.update(source); + hasher.update(array.stride); + } + return static_cast(hasher.digest()); +} + +struct PnMtxUsage { + uint16_t mask = 0; + HashType topologySignature = 0; +}; + +// Slot mask only. +static uint16_t pn_mtx_mask(const uint8_t* vertices, uint16_t vtxCount, + uint32_t vtxStride) noexcept { + if (g_gxState.vtxDesc[GX_VA_PNMTXIDX] != GX_DIRECT) { + return static_cast(1u << std::min(g_gxState.currentPnMtx, MaxPnMtx - 1)); + } + uint16_t mask = 0; + for (uint16_t vertex = 0; vertex < vtxCount; ++vertex) { + const uint32_t matrixIndex = vertices[static_cast(vertex) * vtxStride] / 3u; + if (matrixIndex < MaxPnMtx) { + mask |= static_cast(1u << matrixIndex); + } + } + return mask; +} + +static PnMtxUsage pn_mtx_usage(const uint8_t* vertices, uint16_t vtxCount, + uint32_t vtxStride) noexcept { + if (g_gxState.vtxDesc[GX_VA_PNMTXIDX] != GX_DIRECT) { + const uint32_t matrixIndex = std::min(g_gxState.currentPnMtx, MaxPnMtx - 1); + return { + .mask = static_cast(1u << matrixIndex), + // There is no vertex matrix-index topology in this path. + .topologySignature = 0, + }; + } + + Hasher topologyHasher; + topologyHasher.update(vtxCount); + uint16_t mask = 0; + for (uint16_t vertex = 0; vertex < vtxCount; ++vertex) { + // GX matrix-index attributes precede every other vertex attribute and are always one byte. + const uint8_t rawMatrixIndex = vertices[static_cast(vertex) * vtxStride]; + topologyHasher.update(rawMatrixIndex); + const uint32_t matrixIndex = rawMatrixIndex / 3u; + if (matrixIndex < MaxPnMtx) { + mask |= static_cast(1u << matrixIndex); + } + } + return { + .mask = mask, + .topologySignature = static_cast(topologyHasher.digest()), + }; +} + +// Whether the most recent unmerged draw recorded an interpolation snapshot, so a draw merged into it knows there is a snapshot to extend. +static bool s_lastDrawRecordedInterpolation = false; + +struct CachedIndexTemplate { + bool valid = false; + GXPrimitive prim = static_cast(0); + u16 vtxCount = 0; + u32 indexCount = 0; + IndexBuffer indices{}; +}; + +static const CachedIndexTemplate& cached_index_template(GXPrimitive prim, u16 vtxCount) { + // Topology expansion is immutable for a primitive/count pair. + constexpr size_t CacheSize = 256; + static std::array cache{}; + const u32 key = (static_cast(underlying(prim)) << 16) | vtxCount; + auto& entry = cache[(key ^ (key >> 9)) & (CacheSize - 1)]; + if (entry.valid && entry.prim == prim && entry.vtxCount == vtxCount) LIKELY { + return entry; + } + + entry.valid = true; + entry.prim = prim; + entry.vtxCount = vtxCount; + entry.indexCount = prepare_idx_template(entry.indices, prim, vtxCount); + return entry; +} + +static IndexBuffer handle_draw_idx_buf; + +static ArrayRef offset_index_template(const CachedIndexTemplate& indexTemplate, + u16 vtxStart) { + // Grow-only: resizing down and back up made every merge zero-fill the buffer before the transform immediately overwrote it. + const size_t count = indexTemplate.indices.size(); + if (handle_draw_idx_buf.size() < count) { + handle_draw_idx_buf.resize(count); + } + const u16* src = indexTemplate.indices.data(); + u16* dst = handle_draw_idx_buf.data(); + for (size_t i = 0; i < count; ++i) { + dst[i] = static_cast(src[i] + vtxStart); + } + return {dst, count}; +} + +struct CachedPipelineState { + gfx::PipelineRef ref = 0; + HashType configHash = 0; + // Carried here so the draw can be recorded without keeping the PipelineConfig that produced it alive; it is the only field of the config the draw itself still needs. + u32 dstAlpha = UINT32_MAX; + ShaderInfo shaderInfo{}; +}; + +static const CachedPipelineState& cached_pipeline_state(const PipelineConfig& config) { + constexpr size_t CacheSize = 1024; + struct Entry { + bool valid = false; + PipelineConfig config{}; + CachedPipelineState state{}; + }; + static std::array cache{}; + + const HashType hash = xxh3_hash(config, static_cast(gfx::ShaderType::GX)); + auto& entry = cache[hash & (CacheSize - 1)]; + if (entry.valid && entry.state.configHash == hash && + std::memcmp(&entry.config, &config, sizeof(config)) == 0) LIKELY { + return entry.state; + } + + entry.valid = true; + entry.config = config; + entry.state = { + .ref = gfx::pipeline_ref(config), + .configHash = hash, + .dstAlpha = config.dstAlpha, + .shaderInfo = build_shader_info(config.shaderConfig), + }; + return entry.state; +} + +// Resolving a pipeline the long way costs a ~2.7KB zero-init, a full populate_pipeline_config, an XXH3 over the whole config and a memcmp against the hash-indexed slot -- roughly 11KB of memory traffic for a result that is almost always identical to the previous draw's. +static const CachedPipelineState& resolve_pipeline_state(GXPrimitive prim, GXVtxFmt fmt) { + struct Memo { + const CachedPipelineState* state = nullptr; + u32 generation = 0; + u32 sampleCount = 0; + GXPrimitive prim = static_cast(0); + GXVtxFmt fmt = static_cast(0); + }; + static Memo memo{}; + + const u32 sampleCount = gfx::get_sample_count(); + const u32 generation = g_gxState.pipelineStateGeneration; + if (memo.state != nullptr && memo.generation == generation && memo.sampleCount == sampleCount && + memo.prim == prim && memo.fmt == fmt) LIKELY { + return *memo.state; + } + + PipelineConfig config{}; + populate_pipeline_config(config, prim, fmt); + // cached_pipeline_state hands back a reference into a fixed direct-mapped table, so the address stays valid; the entry it points at can only be rewritten by another call to that function, and every such call goes through this miss path and replaces the memo in the same breath. + const CachedPipelineState& state = cached_pipeline_state(config); + memo = Memo{ + .state = &state, + .generation = generation, + .sampleCount = sampleCount, + .prim = prim, + .fmt = fmt, + }; + return state; +} + +bool submit_raw_draw(GXPrimitive prim, GXVtxFmt fmt, const uint8_t* vertices, uint16_t vtxCount, + uint32_t vertexBytes) { + ZoneScoped; + if (vertices == nullptr || vtxCount == 0 || vertexBytes == 0) { + return false; + } + + if (__gx->dirtyState != 0) UNLIKELY { + __GXSetDirtyState(); + } + + // Raw bridge draws consume live decoded GX state that is also maintained by the HLE producer. + drain(); + + u32 vtxSize; + if (g_gxState.lastVtxFmt == fmt) LIKELY { + vtxSize = g_gxState.lastVtxSize; + } else UNLIKELY { + vtxSize = calculate_last_vtx_size(fmt); + } + + const u32 expectedVertexBytes = static_cast(vtxCount) * vtxSize; + if (expectedVertexBytes == 0 || expectedVertexBytes != vertexBytes) { + static uint32_t rawVertexSizeMismatchCount = 0; + if (rawVertexSizeMismatchCount++ < 64) { + Log.warn("raw draw vertex-size mismatch prim={} fmt={} count={} cached_stride={} expected={} supplied={}", + static_cast(prim), static_cast(fmt), vtxCount, vtxSize, + expectedVertexBytes, vertexBytes); + } + return false; + } + + // This entry point bypasses process(), so it owns the renderer lock itself. + std::lock_guard gpuLock(aurora::renderer_gpu_mutex()); + const gfx::Range vertRange = gfx::push_verts(vertices, vertexBytes); + const bool interpolationIdentityActive = frame_interpolation_identity_needed(); + const PnMtxUsage matrixUsage = interpolationIdentityActive + ? pn_mtx_usage(vertices, vtxCount, vtxSize) + : PnMtxUsage{}; + handle_draw_unmerged(prim, fmt, vtxCount, vertRange, + matrixUsage.mask, matrixUsage.topologySignature, + interpolationIdentityActive ? draw_geometry_signature(fmt, vertices, vtxCount, vtxSize) : 0, + interpolationIdentityActive); + return true; +} + +static bool handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndian) { + ZoneScoped; + GXVtxFmt fmt = static_cast(cmd & CP_VAT_MASK); + GXPrimitive prim = primitive_from_draw_cmd(cmd); + + if (pos + 2 > size) { + return false; + } + u16 vtxCount = read_u16(data + pos, bigEndian); + pos += 2; + + u32 vtxSize; + if (g_gxState.lastVtxFmt == fmt) LIKELY { + vtxSize = g_gxState.lastVtxSize; + } else UNLIKELY { + vtxSize = calculate_last_vtx_size(fmt); + } + + u32 totalVtxBytes = vtxCount * vtxSize; + if (pos + totalVtxBytes > size) UNLIKELY { + handle_draw_overrun(cmd, vtxCount, vtxSize, totalVtxBytes, data, pos, size); + return false; + } + + + // Push raw vertex data to buffer + const uint8_t* vertices = data + pos; + gfx::Range vertRange = gfx::push_verts(vertices, totalVtxBytes); + pos += totalVtxBytes; + + // Try to merge with previous draw call + if (!g_gxState.stateDirty) LIKELY { + auto* lastDraw = gfx::get_last_draw_command(); + // Only if the previous draw call was a single instance draw (no lines/points handling) + if (lastDraw != nullptr && prim != GX_LINES && prim != GX_LINESTRIP && prim != GX_POINTS && + lastDraw->instanceCount == 1) LIKELY { + const auto& indexTemplate = cached_index_template(prim, vtxCount); + const auto indices = offset_index_template(indexTemplate, lastDraw->vtxCount); + const u32 numIndices = indexTemplate.indexCount; + const gfx::Range idxRange = gfx::push_indices(indices); + CHECK(lastDraw->vertRange.offset + lastDraw->vertRange.size == vertRange.offset, + "Non-consecutive vertex ranges ({} < {})", lastDraw->vertRange.offset + lastDraw->vertRange.size, + vertRange.offset); + CHECK(lastDraw->idxRange.offset + lastDraw->idxRange.size == idxRange.offset, + "Non-consecutive index ranges ({} < {})", lastDraw->idxRange.offset + lastDraw->idxRange.size, + idxRange.offset); + lastDraw->vertRange.size += vertRange.size; + lastDraw->idxRange.size += idxRange.size; + lastDraw->vtxCount += vtxCount; + lastDraw->indexCount += numIndices; + ++gfx::g_mergedDrawCallCount; + // This primitive now renders through the draw we merged into, so its palette slots belong to that draw's interpolation snapshot as well. + if (s_lastDrawRecordedInterpolation) UNLIKELY { + extend_interpolation_draw(pn_mtx_mask(vertices, vtxCount, vtxSize)); + } + return true; + } + } + + const bool interpolationIdentityActive = frame_interpolation_identity_needed(); + const PnMtxUsage matrixUsage = interpolationIdentityActive + ? pn_mtx_usage(vertices, vtxCount, vtxSize) + : PnMtxUsage{}; + handle_draw_unmerged(prim, fmt, vtxCount, vertRange, + matrixUsage.mask, matrixUsage.topologySignature, + interpolationIdentityActive ? draw_geometry_signature(fmt, vertices, vtxCount, vtxSize) : 0, + interpolationIdentityActive); + return true; +} + +static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, + gfx::Range vertRange, uint16_t usedPnMtxMask, + HashType matrixTopologySignature, + HashType geometrySignature, bool interpolationIdentityActive) { + ZoneScoped; + // GX_CULL_ALL rasterizes nothing on hardware - no color, no depth. + if (g_gxState.cullMode == GX_CULL_ALL && prim != GX_LINES && prim != GX_LINESTRIP && prim != GX_POINTS) + UNLIKELY { + // Leave stateDirty alone: the next draw re-resolving its pipeline is the safe direction, and nothing about this draw reached the GPU. + return; + } + // Callers hold the renderer GPU mutex for the whole drain (process() and submit_raw_draw); taking it again per draw only cost a recursive re-entry. + const auto& indexTemplate = cached_index_template(prim, vtxCount); + const u32 numIndices = indexTemplate.indexCount; + const gfx::Range idxRange = gfx::push_indices(ArrayRef{ + indexTemplate.indices.data(), indexTemplate.indices.size()}); + + // Build pipeline, bind groups, and push draw command + BindGroupRanges ranges{}; + for (int i = GX_VA_POS; i <= GX_VA_TEX7; ++i) { + if (g_gxState.vtxDesc[i] != GX_INDEX8 && g_gxState.vtxDesc[i] != GX_INDEX16) { + continue; + } + auto& array = g_gxState.arrays[i]; + if (array.cachedRange.size > 0) { + ranges.vaRanges[i - GX_VA_POS] = array.cachedRange; + } else { + const auto range = gfx::push_storage(static_cast(array.data), array.size); + ranges.vaRanges[i - GX_VA_POS] = range; + array.cachedRange = range; + } + } + + const auto& pipelineState = resolve_pipeline_state(prim, fmt); + const auto& info = pipelineState.shaderInfo; + + resolve_sampled_textures(info); + + const auto bindGroups = build_bind_groups(info); + + const auto pipeline = pipelineState.ref; + + // Draw-identity hashing only feeds frame interpolation, and only for perspective draws: build_uniform reads the identity exclusively past its `!perspective || frame_interpolation_fps() == 0` early-out. + FrameInterpolationDrawIdentity drawIdentity{}; + if (interpolationIdentityActive) UNLIKELY { + const HashType drawShape = static_cast(vtxCount) | + (static_cast(underlying(prim)) << 16) | + (static_cast(underlying(fmt)) << 24); + const HashType pipelineDrawSignature = xxh3_hash(pipelineState.configHash, drawShape); + const HashType textureSignature = xxh3_hash(bindGroups.textureBindGroup); + const HashType materialAndTopology = + xxh3_hash(matrixTopologySignature, + xxh3_hash(bindGroups.textureBindGroup, pipelineDrawSignature)); + drawIdentity = FrameInterpolationDrawIdentity{ + .combined = xxh3_hash(geometrySignature, materialAndTopology), + .pipeline = pipelineDrawSignature, + .texture = textureSignature, + .matrixTopology = matrixTopologySignature, + }; + } + const auto uniformRanges = + build_uniform(info, vertRange.offset, ranges, drawIdentity, interpolationIdentityActive, + usedPnMtxMask); + s_lastDrawRecordedInterpolation = interpolationIdentityActive; + + uint32_t instanceCount = 1; + if (prim == GX_LINES) { + instanceCount = vtxCount / 2; + } else if (prim == GX_LINESTRIP) { + instanceCount = vtxCount - 1; + } else if (prim == GX_POINTS) { + instanceCount = vtxCount; + } + gfx::push_draw_command(DrawData{ + .pipeline = pipeline, + .vertRange = vertRange, + .idxRange = idxRange, + .uniformRange = uniformRanges.current, + .interpolatedUniformRanges = uniformRanges.interpolated, + .vtxCount = vtxCount, + .indexCount = numIndices, + .instanceCount = instanceCount, + .bindGroups = bindGroups, + .dstAlpha = pipelineState.dstAlpha, + }); + g_gxState.stateDirty = false; +} + +std::string read_string(const u8* data, u32& pos, u32 size, bool bigEndian) { + CHECK(pos + 2 <= size, "Aurora string length read overrun"); + const u16 length = read_u16(data + pos, bigEndian); + pos += 2; + + CHECK(pos + length <= size, "Aurora string read overrun"); + std::string str(reinterpret_cast(data) + pos, length); + pos += length; + return str; +} + +bool handle_aurora(const u8* data, u32& pos, u32 size, bool bigEndian) { + ZoneScoped; + if (pos + 2 > size) { + return false; + } + u16 subCmd = read_u16(data + pos, bigEndian); + pos += 2; + + // Setting of vertex array bases. + if (subCmd == GX_LOAD_AURORA_VIEWPORT_RENDER) { + CHECK(pos + 24 <= size, "GX_LOAD_AURORA_VIEWPORT_RENDER read overrun"); + const f32 left = read_f32(data + pos, bigEndian); + pos += 4; + const f32 top = read_f32(data + pos, bigEndian); + pos += 4; + const f32 width = read_f32(data + pos, bigEndian); + pos += 4; + const f32 height = read_f32(data + pos, bigEndian); + pos += 4; + const f32 nearZ = read_f32(data + pos, bigEndian); + pos += 4; + const f32 farZ = read_f32(data + pos, bigEndian); + pos += 4; + set_render_viewport({ + .left = left, + .top = top, + .width = width, + .height = height, + .znear = nearZ, + .zfar = farZ, + }); + } else if (subCmd == GX_LOAD_AURORA_SCISSOR_RENDER) { + CHECK(pos + 16 <= size, "GX_LOAD_AURORA_SCISSOR_RENDER read overrun"); + const int32_t left = static_cast(read_u32(data + pos, bigEndian)); + pos += 4; + const int32_t top = static_cast(read_u32(data + pos, bigEndian)); + pos += 4; + const int32_t width = static_cast(read_u32(data + pos, bigEndian)); + pos += 4; + const int32_t height = static_cast(read_u32(data + pos, bigEndian)); + pos += 4; + set_render_scissor({left, top, width, height}); + } else if (subCmd >= GX_LOAD_AURORA_ARRAYBASE && subCmd <= (GX_LOAD_AURORA_ARRAYBASE | 0x0f)) { + CHECK(pos + 13 <= size, "GX_LOAD_AURORA_ARRAYBASE read overrun"); + u32 attrIdx = subCmd - GX_LOAD_AURORA_ARRAYBASE + GX_VA_POS; + + u64 arrayAddr = read_u64(data + pos, bigEndian); + pos += 8; + u32 arraySize = read_u32(data + pos, bigEndian); + pos += 4; + bool le = data[pos] == 1; + pos += 1; + + auto& array = g_gxState.arrays[attrIdx]; + const auto newData = reinterpret_cast(arrayAddr); + if (array.data != newData || array.size != arraySize || array.le != le) { + array.data = newData; + array.size = arraySize; + array.le = le; + // Only drop the cached upload when the backing array actually changes. + array.cachedRange = {}; + mark_pipeline_state_dirty(); + } + } else if (subCmd == GX_LOAD_AURORA_TEXOBJ) { + CHECK(pos + 34 <= size, "GX_LOAD_AURORA_TEXOBJ read overrun"); + const auto texMapId = data[pos]; + pos += 1; + CHECK(texMapId < MaxTextures, "invalid texture map id {}", texMapId); + auto& slot = g_gxState.loadedTextures[texMapId]; + GXTexObj_ next = slot; + next.data = reinterpret_cast(read_u64(data + pos, bigEndian)); + pos += 8; + next.mWidth = read_u32(data + pos, bigEndian); + pos += 4; + next.mHeight = read_u32(data + pos, bigEndian); + pos += 4; + next.mFormat = static_cast(read_u32(data + pos, bigEndian)); + pos += 4; + next.tlut = static_cast(read_u32(data + pos, bigEndian)); + pos += 4; + if (data[pos] != 0) { + next.flags |= 1u; + } else { + next.flags &= ~1u; + } + pos += 1; + next.texObjId = read_u32(data + pos, bigEndian); + pos += 4; + next.texDataVersion = read_u32(data + pos, bigEndian); + pos += 4; + next.set_no_cache(false); // Reset no-cache flag + const bool changed = slot.data != next.data || slot.mWidth != next.mWidth || slot.mHeight != next.mHeight || + slot.mFormat != next.mFormat || slot.tlut != next.tlut || slot.flags != next.flags || + slot.texObjId != next.texObjId || slot.texDataVersion != next.texDataVersion; + slot = next; + if (changed) { + g_gxState.stateDirty = true; + } + } else if (subCmd == GX_LOAD_AURORA_TLUT) { + CHECK(pos + 23 <= size, "GX_LOAD_AURORA_TLUT read overrun"); + const auto idx = data[pos]; + pos += 1; + CHECK(idx < MaxTluts, "invalid tlut slot {}", idx); + auto& slot = g_gxState.loadedTluts[idx]; + slot.data = reinterpret_cast(read_u64(data + pos, bigEndian)); + pos += 8; + slot.format = static_cast(read_u32(data + pos, bigEndian)); + pos += 4; + slot.numEntries = read_u16(data + pos, bigEndian); + pos += 2; + slot.tlutObjId = read_u32(data + pos, bigEndian); + pos += 4; + slot.tlutDataVersion = read_u32(data + pos, bigEndian); + pos += 4; + slot.set_no_cache(false); // Reset no-cache flag + g_gxState.stateDirty = true; + } else if (subCmd == GX_LOAD_AURORA_DESTROY_TEXOBJ) { + CHECK(pos + 4 <= size, "GX_LOAD_AURORA_DESTROY_TEXOBJ read overrun"); + evict_texture_object(read_u32(data + pos, bigEndian)); + pos += 4; + } else if (subCmd == GX_LOAD_AURORA_DESTROY_TLUT) { + CHECK(pos + 4 <= size, "GX_LOAD_AURORA_DESTROY_TLUT read overrun"); + evict_tlut_object(read_u32(data + pos, bigEndian)); + pos += 4; + } else if (subCmd == GX_LOAD_AURORA_DESTROY_COPY_TEX) { + CHECK(pos + 8 <= size, "GX_LOAD_AURORA_DESTROY_COPY_TEX read overrun"); + evict_copy_texture(reinterpret_cast(read_u64(data + pos, bigEndian))); + pos += 8; + } else if (subCmd == GX_LOAD_AURORA_INVALIDATE_TEX_ALL) { + invalidate_static_texture_cache(); + } else if (subCmd == GX_LOAD_AURORA_DEBUG_GROUP_PUSH) { + auto label = read_string(data, pos, size, bigEndian); + gfx::push_debug_group(std::move(label)); + } else if (subCmd == GX_LOAD_AURORA_DEBUG_GROUP_POP) { + aurora_pop_debug_group(); + } else if (subCmd == GX_LOAD_AURORA_DEBUG_MARKER_INSERT) { + auto label = read_string(data, pos, size, bigEndian); + gfx::insert_debug_marker(std::move(label)); + } + + else { + static u32 unknownAuroraLogCount = 0; + if (unknownAuroraLogCount < 16) { + Log.warn("Unknown Aurora subcommand: {:04X}; stopping FIFO decode", subCmd); + ++unknownAuroraLogCount; + } + return false; + } + return true; +} + +} // namespace aurora::gx::fifo diff --git a/aurora-main/lib/gx/command_processor.hpp b/aurora-main/lib/gx/command_processor.hpp new file mode 100644 index 0000000..0e15ad5 --- /dev/null +++ b/aurora-main/lib/gx/command_processor.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "dolphin/gx/GXEnum.h" +#include "../internal.hpp" + +namespace aurora::gx::fifo { + +// Reset the CP write-deduplication state whenever the GX shadow state is reinitialized. +void reset_cp_register_cache(); + +// Process a buffer of GX FIFO commands +void process(const uint8_t* data, uint32_t size, bool bigEndian); + +// Submit already-packed direct vertex bytes against the current GX state. +bool submit_raw_draw(GXPrimitive prim, GXVtxFmt fmt, const uint8_t* vertices, uint16_t vtxCount, + uint32_t vertexBytes); + +} // namespace aurora::gx::fifo diff --git a/aurora-main/lib/gx/fifo.cpp b/aurora-main/lib/gx/fifo.cpp new file mode 100644 index 0000000..9b4dcc0 --- /dev/null +++ b/aurora-main/lib/gx/fifo.cpp @@ -0,0 +1,94 @@ +#include "fifo.hpp" +#include "command_processor.hpp" +#include "../internal.hpp" + +#include +#include +#include +#include + +#include "tracy/Tracy.hpp" + +namespace aurora::gx::fifo { +static Module Log("aurora::gx::fifo"); + +namespace detail { +uint8_t* sBufferData = nullptr; +uint32_t sBufferSize = 0; +uint32_t sBufferCapacity = 0; +bool sInDisplayList = false; +uint8_t* sDlBuffer = nullptr; +uint32_t sDlSize = 0; +uint32_t sDlWritePos = 0; +} // namespace detail + +void init() { + constexpr uint32_t initialCapacity = 64 * 1024; + reset_cp_register_cache(); + free(detail::sBufferData); + detail::sBufferData = static_cast(malloc(initialCapacity)); + detail::sBufferSize = 0; + detail::sBufferCapacity = initialCapacity; + detail::sInDisplayList = false; + detail::sDlBuffer = nullptr; + detail::sDlSize = 0; + detail::sDlWritePos = 0; +} + +void write_data_grow(const void* data, uint32_t length) { + uint32_t needed = detail::sBufferSize + length; + uint32_t newCap = std::max(detail::sBufferCapacity * 2, needed); + detail::sBufferData = static_cast(realloc(detail::sBufferData, newCap)); + std::memcpy(detail::sBufferData + detail::sBufferSize, data, length); + detail::sBufferSize = needed; + detail::sBufferCapacity = newCap; +} + +void begin_display_list(uint8_t* buf, uint32_t size) { + detail::sInDisplayList = true; + detail::sDlBuffer = buf; + detail::sDlSize = size; + detail::sDlWritePos = 0; +} + +uint32_t end_display_list() { + detail::sInDisplayList = false; + uint32_t bytesWritten = detail::sDlWritePos; + uint32_t padded = (bytesWritten + 31) & ~31u; + while (detail::sDlWritePos < padded && detail::sDlWritePos < detail::sDlSize) { + detail::sDlBuffer[detail::sDlWritePos++] = 0; + } + detail::sDlBuffer = nullptr; + detail::sDlSize = 0; + detail::sDlWritePos = 0; + return padded; +} + +bool in_display_list() { return detail::sInDisplayList; } + +// How much of the producer's frame is spent blocked before it may decode the next batch of GX commands. +static void note_drain_wait(uint64_t nanos) noexcept { + ZoneScopedN("FIFO drain wait"); + TracyPlot("aurora: fifoDrainWaitUs", static_cast(nanos / 1000)); +} + +void drain() { + // SEALED, not DONE. + const auto waited = aurora::wait_for_frame_worker_sealed(); + if (waited.count() > 0) UNLIKELY { + note_drain_wait(static_cast(waited.count())); + } + if (detail::sBufferSize == 0) { + return; + } + process(detail::sBufferData, detail::sBufferSize, true); + detail::sBufferSize = 0; +} + +const uint8_t* get_buffer_data() { return detail::sBufferData; } +uint32_t get_buffer_size() { return detail::sBufferSize; } +void clear_buffer() { + detail::sBufferSize = 0; +} + +} // namespace aurora::gx::fifo diff --git a/aurora-main/lib/gx/fifo.hpp b/aurora-main/lib/gx/fifo.hpp new file mode 100644 index 0000000..b01ea45 --- /dev/null +++ b/aurora-main/lib/gx/fifo.hpp @@ -0,0 +1,89 @@ +#pragma once + +#include "../internal.hpp" + +#include + +namespace aurora::gx::fifo { + +namespace detail { +extern uint8_t* sBufferData; +extern uint32_t sBufferSize; +extern uint32_t sBufferCapacity; +extern bool sInDisplayList; +extern uint8_t* sDlBuffer; +extern uint32_t sDlSize; +extern uint32_t sDlWritePos; +} // namespace detail + +void init(); + +// Out-of-line slow path: grows internal buffer then appends data +void write_data_grow(const void* data, uint32_t length); + +inline void write_data(const void* data, const uint32_t length) { + if (!detail::sInDisplayList) + LIKELY { + if (detail::sBufferSize + length <= detail::sBufferCapacity) + LIKELY { + std::memcpy(detail::sBufferData + detail::sBufferSize, data, length); + detail::sBufferSize += length; + return; + } + write_data_grow(data, length); + } + else if (detail::sDlWritePos + length <= detail::sDlSize) { + std::memcpy(detail::sDlBuffer + detail::sDlWritePos, data, length); + detail::sDlWritePos += length; + } +} + +inline void write_u8(const uint8_t val) { + if (!detail::sInDisplayList) + LIKELY { + if (detail::sBufferSize < detail::sBufferCapacity) + LIKELY { + detail::sBufferData[detail::sBufferSize++] = val; + return; + } + write_data_grow(&val, 1); + } + else if (detail::sDlWritePos < detail::sDlSize) { + detail::sDlBuffer[detail::sDlWritePos++] = val; + } +} + +inline void write_u16(const uint16_t val) { + const auto out = bswap(val); + write_data(&out, sizeof(out)); +} + +inline void write_u32(const uint32_t val) { + const auto out = bswap(val); + write_data(&out, sizeof(out)); +} + +inline void write_u64(const uint64_t val) { + const auto out = bswap(val); + write_data(&out, sizeof(out)); +} + +inline void write_f32(const float val) { + const auto out = bswap(val); + write_data(&out, sizeof(out)); +} + +// Display list recording +void begin_display_list(uint8_t* buf, uint32_t size); +uint32_t end_display_list(); +bool in_display_list(); + +// Drain the internal FIFO buffer through the command processor +void drain(); + +// Internal buffer inspection +const uint8_t* get_buffer_data(); +uint32_t get_buffer_size(); +void clear_buffer(); + +} // namespace aurora::gx::fifo diff --git a/aurora-main/lib/gx/frame_interpolation.cpp b/aurora-main/lib/gx/frame_interpolation.cpp new file mode 100644 index 0000000..b0283ec --- /dev/null +++ b/aurora-main/lib/gx/frame_interpolation.cpp @@ -0,0 +1,1523 @@ +#include "frame_interpolation.hpp" + +#include "../internal.hpp" +#include "aurora/gfx.h" + +// Guest matrices really do carry NaN/Inf, and the isfinite guards here keep them +// out of the MatchEdge sort. Needs -fno-finite-math-only (see runtime/CMakeLists.txt). +#if defined(__FINITE_MATH_ONLY__) && __FINITE_MATH_ONLY__ +#error "frame_interpolation.cpp must be compiled with -fno-finite-math-only; its NaN filtering is load-bearing for memory safety (see comment above)." +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace aurora::gx { +namespace detail { +std::atomic_uint32_t g_frameInterpolationFps{0}; +} // namespace detail + +namespace { +static Module Log("aurora::gx::interp"); + +// Diagnostics for aurora_get_frame_interpolation_diagnostics. Individually atomic: +// one writer, and a torn read is harmless for an overlay. +std::atomic_uint32_t s_diagCandidates{0}; +std::atomic_uint32_t s_diagMatchable{0}; +std::atomic_uint32_t s_diagMatches{0}; +std::atomic_bool s_diagEligible{false}; +std::atomic_bool s_diagReplaySafe{true}; +std::atomic_uint64_t s_diagFramesSealed{0}; +std::atomic_uint64_t s_diagFramesLowMatch{0}; +std::atomic_uint64_t s_diagFramesReplayUnsafe{0}; +std::atomic_uint64_t s_diagSlotReductions{0}; +std::atomic_uint64_t s_diagLateSealDrops{0}; +// Persistent worker pool for the per-sample interpolation tasks. libc++ has no +// parallel execution policies, so without it the seal loop runs serially. Leaked. +class InterpolationWorkerPool { +public: + static InterpolationWorkerPool& instance() { + static InterpolationWorkerPool* pool = new InterpolationWorkerPool(); + return *pool; + } + + // Runs fn(index) for every index in [0, count). Returns once every index has + // been processed. Not reentrant; only the producer/seal thread dispatches. + template + void run(size_t count, const Fn& fn) { + if (count == 0) { + return; + } + if (m_workers.empty()) { + for (size_t i = 0; i < count; ++i) { + fn(i); + } + return; + } + { + // Job state and the generation bump publish under one lock, so a late worker + // can never observe a half-written job. + std::lock_guard lock(m_mutex); + m_invoke = [&fn](size_t index) { fn(index); }; + m_count.store(count, std::memory_order_relaxed); + m_next.store(0, std::memory_order_relaxed); + m_remaining.store(count, std::memory_order_relaxed); + ++m_generation; + } + m_wake.notify_all(); + consume(); + // Wait for stragglers to leave consume() entirely, not just finish their chunks, + // so the next dispatch can safely reset the shared counters. + while (m_remaining.load(std::memory_order_acquire) != 0 || + m_active.load(std::memory_order_acquire) != 0) { + std::this_thread::yield(); + } + m_invoke = nullptr; + } + +private: + static constexpr size_t kChunk = 16; + + InterpolationWorkerPool() { + const unsigned hardware = std::thread::hardware_concurrency(); + // The caller helps too. Cap the helpers: tasks are short memcpy+math, so dispatch + // overhead and memory bandwidth dominate past a few threads. + const unsigned helpers = hardware > 2 ? std::min(hardware - 1, 6u) : 0; + m_workers.reserve(helpers); + for (unsigned i = 0; i < helpers; ++i) { + m_workers.emplace_back([this] { worker_loop(); }); + } + } + + void consume() { + const size_t count = m_count.load(std::memory_order_relaxed); + while (true) { + const size_t begin = m_next.fetch_add(kChunk, std::memory_order_relaxed); + if (begin >= count) { + return; + } + const size_t end = std::min(begin + kChunk, count); + for (size_t index = begin; index < end; ++index) { + m_invoke(index); + } + m_remaining.fetch_sub(end - begin, std::memory_order_release); + } + } + + void worker_loop() { + uint64_t seenGeneration = 0; + while (true) { + { + std::unique_lock lock(m_mutex); + m_wake.wait(lock, [&] { return m_generation != seenGeneration; }); + seenGeneration = m_generation; + // Counted under the mutex: when the dispatcher sees m_active == 0 every worker is + // parked or has not read the current generation, so a counter reset is safe. + m_active.fetch_add(1, std::memory_order_relaxed); + } + consume(); + m_active.fetch_sub(1, std::memory_order_release); + } + } + + std::vector m_workers; + std::mutex m_mutex; + std::condition_variable m_wake; + uint64_t m_generation = 0; + std::function m_invoke; + std::atomic_size_t m_count{0}; + std::atomic_size_t m_next{0}; + std::atomic_size_t m_remaining{0}; + std::atomic_size_t m_active{0}; +}; +struct FrameTransformSnapshot { + Mat4x4 projection{}; + Mat3x4 position{}; + Mat3x4 normal{}; + uint16_t usedMatrixMask = 1; + struct IndexedMatrices { + std::array, MaxPnMtx> position{}; + std::array, MaxPnMtx> normal{}; + // Content hash of each used slot's position/normal pair. A mesh split across draws + // repeats bone matrices byte for byte, so the seal can pair slots across draws. + std::array slotHash{}; + }; + std::unique_ptr indexedMatrices; +}; + +struct FrameTransformEntry { + FrameInterpolationDrawIdentity identity; + FrameTransformSnapshot transform; + // Constant-velocity prediction of a non-indexed transform, so repeated meshes match + // on where they will be. Not for indexed draws: PNMTXIDX slot identity pairs those. + Mat3x4 predictedPosition{}; + bool hasPrediction = false; +}; + +struct Quaternion { + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + float w = 1.0f; +}; + +struct PendingUniformInterpolation { + size_t currentTransformIndex = 0; + const uint8_t* sourceUniformData = nullptr; + uint8_t* uniformData = nullptr; + size_t uniformSize = 0; + size_t projectionOffset = 0; + size_t positionOffset = 0; + size_t normalOffset = 0; + size_t currentMatrix = 0; + uint32_t numerator = 0; + uint32_t denominator = 1; + bool indexedMatrices = false; +}; + +std::vector s_previousFrameTransforms; +std::vector s_currentFrameTransforms; +std::unordered_map> s_previousTransformIndices; +std::unordered_map> s_currentTransformIndices; +std::unordered_map> s_previousStableTransformIndices; +std::unordered_map> s_currentStableTransformIndices; + +// Free list for the indexed-matrix snapshots; per-draw heap allocation was the +// hottest cost in this path. Unused slots keep stale data, consumers mask first. +std::vector> s_indexedMatricesPool; +constexpr size_t kMaximumPooledIndexedMatrices = 4096; + +std::unique_ptr acquire_indexed_matrices() { + if (s_indexedMatricesPool.empty()) { + return std::make_unique(); + } + auto block = std::move(s_indexedMatricesPool.back()); + s_indexedMatricesPool.pop_back(); + return block; +} + +void recycle_transform_entries(std::vector& entries) noexcept { + for (auto& entry : entries) { + if (entry.transform.indexedMatrices && + s_indexedMatricesPool.size() < kMaximumPooledIndexedMatrices) { + s_indexedMatricesPool.push_back(std::move(entry.transform.indexedMatrices)); + } + } + entries.clear(); +} + +// Whether the frame in s_previousFrameTransforms recorded any palette draw; +// record_interpolation_draw uses it to decide whether staging can pay off. +bool s_previousFrameHasIndexedMatrices = false; + +// Hands the frame that just sealed to the next frame's matching and returns +// the retiring one's matrix blocks to the pool. +void retire_frame_transforms() noexcept { + s_previousFrameHasIndexedMatrices = + std::any_of(s_currentFrameTransforms.begin(), s_currentFrameTransforms.end(), + [](const FrameTransformEntry& entry) noexcept { + return static_cast(entry.transform.indexedMatrices); + }); + s_previousFrameTransforms.swap(s_currentFrameTransforms); + recycle_transform_entries(s_currentFrameTransforms); +} + +// clear() destroys every node, so empty the vectors in place and let a stable scene +// reuse them. Fall back to a real clear once the map outgrows the live set. +void clear_index_map_keep_nodes(std::unordered_map>& map, + size_t liveEntries) { + if (map.size() > liveEntries + 256) { + map.clear(); + return; + } + for (auto& entry : map) { + entry.second.clear(); + } +} +uint32_t s_perspectiveCandidates = 0; +// Candidates that also existed last frame. Freshly spawned effects have no partner +// by definition, so counting them measured spawn churn instead of matcher health. +uint32_t s_perspectiveMatchable = 0; +uint32_t s_perspectiveMatches = 0; +std::vector s_pendingUniformInterpolations; +std::atomic_bool s_hasInterpolatedFrame{false}; +std::atomic_bool s_frameInterpolationReplaySafe{true}; +uint32_t s_previousInterpolationFps = 0; + +// Adaptive slot count: the target moves at most one step per pacing window, while +// s_activeInterpolationSamples is latched per frame and must not move mid-frame. +std::atomic_uint32_t s_interpolationSampleTarget{0}; +std::atomic_uint32_t s_activeInterpolationSamples{0}; + +// Set when the producer already overran its retrace budget; the next seal then skips +// inserted slots for that one frame, which helps the late frame catch back up. +std::atomic_bool s_dropInterpolationAtSeal{false}; + +// Windowed backstop for sustained overload. The wide reduce/restore gap is the +// hysteresis, so a scene that can sustain N slots settles there instead of flapping. +std::atomic_uint32_t s_pacingWindowFrames{0}; +std::atomic_uint32_t s_pacingWindowMisses{0}; +constexpr uint32_t kPacingWindowFrames = 60; // ~1 second of guest frames +constexpr uint32_t kPacingReducePercent = 75; +constexpr uint32_t kPacingRestorePercent = 25; + +uint32_t maximum_interpolation_samples() noexcept { + const uint32_t targetFps = frame_interpolation_fps(); + if (targetFps == 0) { + return 0; + } + return std::min(targetFps / 60 - 1, MaxInterpolatedFrames); +} + +constexpr float kMinimumScale = 1.0e-5f; +constexpr float kMaximumTranslationPerFrame = 1500.0f; +constexpr float kMinimumQuaternionDot = 0.70710678f; +constexpr size_t kNoPreparedPair = std::numeric_limits::max(); + +HashType combine_identity(HashType first, HashType second) noexcept { + return xxh3_hash(second, first); +} + +HashType stable_identity(const FrameInterpolationDrawIdentity& identity) noexcept { + return combine_identity(combine_identity(identity.pipeline, identity.texture), + identity.matrixTopology); +} + +float dot3(const std::array& a, const std::array& b) noexcept { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +} + +std::array cross3(const std::array& a, const std::array& b) noexcept { + return { + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0], + }; +} + +Quaternion quaternion_from_rotation(const std::array, 3>& m) noexcept { + Quaternion q; + const float trace = m[0][0] + m[1][1] + m[2][2]; + if (trace > 0.0f) { + const float s = std::sqrt(trace + 1.0f) * 2.0f; + q.w = 0.25f * s; + q.x = (m[2][1] - m[1][2]) / s; + q.y = (m[0][2] - m[2][0]) / s; + q.z = (m[1][0] - m[0][1]) / s; + } else if (m[0][0] > m[1][1] && m[0][0] > m[2][2]) { + const float s = std::sqrt(1.0f + m[0][0] - m[1][1] - m[2][2]) * 2.0f; + q.w = (m[2][1] - m[1][2]) / s; + q.x = 0.25f * s; + q.y = (m[0][1] + m[1][0]) / s; + q.z = (m[0][2] + m[2][0]) / s; + } else if (m[1][1] > m[2][2]) { + const float s = std::sqrt(1.0f + m[1][1] - m[0][0] - m[2][2]) * 2.0f; + q.w = (m[0][2] - m[2][0]) / s; + q.x = (m[0][1] + m[1][0]) / s; + q.y = 0.25f * s; + q.z = (m[1][2] + m[2][1]) / s; + } else { + const float s = std::sqrt(1.0f + m[2][2] - m[0][0] - m[1][1]) * 2.0f; + q.w = (m[1][0] - m[0][1]) / s; + q.x = (m[0][2] + m[2][0]) / s; + q.y = (m[1][2] + m[2][1]) / s; + q.z = 0.25f * s; + } + return q; +} + +bool decompose_affine(const Mat3x4& matrix, std::array, 3>& rotation, + std::array& scale, std::array& translation, + Quaternion& quaternion) noexcept { + const std::array, 3> rows{matrix.m0, matrix.m1, matrix.m2}; + // V*T*R*S puts non-uniform model scale on the columns, so scale must be read off the + // columns; row extraction misreads R*S as shear whenever the rotation tilts an axis. + for (size_t row = 0; row < 3; ++row) { + translation[row] = rows[row].w(); + if (!std::isfinite(translation[row])) { + return false; + } + } + for (size_t column = 0; column < 3; ++column) { + const float x = rows[0][column]; + const float y = rows[1][column]; + const float z = rows[2][column]; + scale[column] = std::sqrt(x * x + y * y + z * z); + if (!std::isfinite(scale[column]) || scale[column] < kMinimumScale) { + return false; + } + rotation[0][column] = x / scale[column]; + rotation[1][column] = y / scale[column]; + rotation[2][column] = z / scale[column]; + } + + const auto rotationColumn = [&rotation](size_t column) noexcept { + return std::array{rotation[0][column], rotation[1][column], rotation[2][column]}; + }; + if (std::abs(dot3(rotationColumn(0), rotationColumn(1))) > 0.05f || + std::abs(dot3(rotationColumn(0), rotationColumn(2))) > 0.05f || + std::abs(dot3(rotationColumn(1), rotationColumn(2))) > 0.05f) { + return false; + } + + const float determinant = dot3(rotation[0], cross3(rotation[1], rotation[2])); + if (!std::isfinite(determinant) || std::abs(std::abs(determinant) - 1.0f) > 0.1f) { + return false; + } + if (determinant < 0.0f) { + scale[0] = -scale[0]; + for (size_t row = 0; row < 3; ++row) { + rotation[row][0] = -rotation[row][0]; + } + } + + quaternion = quaternion_from_rotation(rotation); + return std::isfinite(quaternion.x) && std::isfinite(quaternion.y) && std::isfinite(quaternion.z) && + std::isfinite(quaternion.w); +} + +std::array, 3> rotation_from_quaternion(const Quaternion& q) noexcept { + const float xx = q.x * q.x; + const float yy = q.y * q.y; + const float zz = q.z * q.z; + const float xy = q.x * q.y; + const float xz = q.x * q.z; + const float yz = q.y * q.z; + const float wx = q.w * q.x; + const float wy = q.w * q.y; + const float wz = q.w * q.z; + return {{ + {{1.0f - 2.0f * (yy + zz), 2.0f * (xy - wz), 2.0f * (xz + wy)}}, + {{2.0f * (xy + wz), 1.0f - 2.0f * (xx + zz), 2.0f * (yz - wx)}}, + {{2.0f * (xz - wy), 2.0f * (yz + wx), 1.0f - 2.0f * (xx + yy)}}, + }}; +} + +struct PreparedAffinePair { + Mat3x4 previous{}; + Mat3x4 current{}; + std::array previousScale{}; + std::array currentScale{}; + std::array previousTranslation{}; + std::array currentTranslation{}; + Quaternion previousQuaternion{}; + Quaternion currentQuaternion{}; + bool linear = false; + bool identical = false; + bool valid = false; +}; + +struct PreparedTransformInterpolation { + size_t nonIndexedPairOffset = kNoPreparedPair; + std::array indexedPairOffsets{}; + // Previous-frame entry the projection interpolates from. For a draw that borrowed + // its matrices from a sibling, that is the sibling's partner. + size_t previousProjectionEntry = kNoPreparedPair; + bool indexedValid = false; + + PreparedTransformInterpolation() { + indexedPairOffsets.fill(kNoPreparedPair); + } +}; + +PreparedAffinePair prepare_affine_pair(const Mat3x4& previous, + const Mat3x4& current) noexcept { + PreparedAffinePair pair{.previous = previous, .current = current}; + if (std::memcmp(&previous, ¤t, sizeof(current)) == 0) { + pair.identical = true; + pair.valid = true; + return pair; + } + + std::array, 3> previousRotation{}; + std::array, 3> currentRotation{}; + if (!decompose_affine(previous, previousRotation, pair.previousScale, + pair.previousTranslation, pair.previousQuaternion) || + !decompose_affine(current, currentRotation, pair.currentScale, + pair.currentTranslation, pair.currentQuaternion)) { + return pair; + } + + float translationDeltaSquared = 0.0f; + for (size_t i = 0; i < 3; ++i) { + const float delta = pair.currentTranslation[i] - pair.previousTranslation[i]; + translationDeltaSquared += delta * delta; + } + if (!std::isfinite(translationDeltaSquared) || + translationDeltaSquared > kMaximumTranslationPerFrame * kMaximumTranslationPerFrame) { + return pair; + } + + float quaternionDot = pair.previousQuaternion.x * pair.currentQuaternion.x + + pair.previousQuaternion.y * pair.currentQuaternion.y + + pair.previousQuaternion.z * pair.currentQuaternion.z + + pair.previousQuaternion.w * pair.currentQuaternion.w; + if (quaternionDot < 0.0f) { + pair.currentQuaternion.x = -pair.currentQuaternion.x; + pair.currentQuaternion.y = -pair.currentQuaternion.y; + pair.currentQuaternion.z = -pair.currentQuaternion.z; + pair.currentQuaternion.w = -pair.currentQuaternion.w; + quaternionDot = -quaternionDot; + } + if (!std::isfinite(quaternionDot) || quaternionDot < kMinimumQuaternionDot) { + return pair; + } + pair.valid = true; + return pair; +} + +// Indexed matrices are final palette transforms, so interpolate coefficients instead +// of decomposing: ((1-t)M0 + tM1)v keeps a shared mesh boundary shared. +PreparedAffinePair prepare_indexed_pair(const Mat3x4& previous, + const Mat3x4& current) noexcept { + PreparedAffinePair pair{ + .previous = previous, + .current = current, + .linear = true, + }; + if (std::memcmp(&previous, ¤t, sizeof(current)) == 0) { + pair.identical = true; + pair.valid = true; + return pair; + } + + const std::array, 3> previousRows{previous.m0, previous.m1, previous.m2}; + const std::array, 3> currentRows{current.m0, current.m1, current.m2}; + for (size_t row = 0; row < 3; ++row) { + for (size_t column = 0; column < 4; ++column) { + if (!std::isfinite(previousRows[row][column]) || + !std::isfinite(currentRows[row][column])) { + return pair; + } + } + } + + const float dx = current.m0.w() - previous.m0.w(); + const float dy = current.m1.w() - previous.m1.w(); + const float dz = current.m2.w() - previous.m2.w(); + const float translationDeltaSquared = dx * dx + dy * dy + dz * dz; + if (!(translationDeltaSquared <= + kMaximumTranslationPerFrame * kMaximumTranslationPerFrame)) { + return pair; + } + pair.valid = true; + return pair; +} + +bool evaluate_affine_pair(const PreparedAffinePair& pair, float weight, + Mat3x4& output) noexcept { + if (!pair.valid || pair.identical) { + output = pair.current; + return pair.valid; + } + + if (pair.linear) { + const std::array*, 3> previousRows{ + &pair.previous.m0, &pair.previous.m1, &pair.previous.m2}; + const std::array*, 3> currentRows{ + &pair.current.m0, &pair.current.m1, &pair.current.m2}; + const std::array*, 3> outputRows{&output.m0, &output.m1, &output.m2}; + for (size_t row = 0; row < 3; ++row) { + for (size_t column = 0; column < 4; ++column) { + const float before = (*previousRows[row])[column]; + (*outputRows[row])[column] = + before + ((*currentRows[row])[column] - before) * weight; + } + } + return true; + } + + // Consecutive 60 Hz transforms stay in one hemisphere and within 90 degrees, so + // normalized lerp is stable and skips three transcendentals per matrix. + const float previousWeight = 1.0f - weight; + const float currentWeight = weight; + Quaternion interpolated{ + pair.previousQuaternion.x * previousWeight + pair.currentQuaternion.x * currentWeight, + pair.previousQuaternion.y * previousWeight + pair.currentQuaternion.y * currentWeight, + pair.previousQuaternion.z * previousWeight + pair.currentQuaternion.z * currentWeight, + pair.previousQuaternion.w * previousWeight + pair.currentQuaternion.w * currentWeight, + }; + const float quaternionLength = std::sqrt(interpolated.x * interpolated.x + + interpolated.y * interpolated.y + + interpolated.z * interpolated.z + + interpolated.w * interpolated.w); + if (!std::isfinite(quaternionLength) || quaternionLength < kMinimumScale) { + output = pair.current; + return false; + } + interpolated.x /= quaternionLength; + interpolated.y /= quaternionLength; + interpolated.z /= quaternionLength; + interpolated.w /= quaternionLength; + + const auto interpolatedRotation = rotation_from_quaternion(interpolated); + std::array interpolatedScale{}; + std::array interpolatedTranslation{}; + for (size_t i = 0; i < 3; ++i) { + interpolatedScale[i] = pair.previousScale[i] + + (pair.currentScale[i] - pair.previousScale[i]) * weight; + interpolatedTranslation[i] = + pair.previousTranslation[i] + + (pair.currentTranslation[i] - pair.previousTranslation[i]) * weight; + } + // Column scale mirrors decompose_affine: the reconstruction is R*S, with + // each scale component applied down its column. + output = { + {interpolatedRotation[0][0] * interpolatedScale[0], + interpolatedRotation[0][1] * interpolatedScale[1], + interpolatedRotation[0][2] * interpolatedScale[2], interpolatedTranslation[0]}, + {interpolatedRotation[1][0] * interpolatedScale[0], + interpolatedRotation[1][1] * interpolatedScale[1], + interpolatedRotation[1][2] * interpolatedScale[2], interpolatedTranslation[1]}, + {interpolatedRotation[2][0] * interpolatedScale[0], + interpolatedRotation[2][1] * interpolatedScale[1], + interpolatedRotation[2][2] * interpolatedScale[2], interpolatedTranslation[2]}, + }; + return true; +} + +bool interpolate_affine_impl(const Mat3x4& previous, const Mat3x4& current, + float weight, Mat3x4& output) noexcept { + return evaluate_affine_pair(prepare_affine_pair(previous, current), weight, output); +} + +Mat4x4 interpolate_projection(const Mat4x4& previous, const Mat4x4& current, + float weight) noexcept { + Mat4x4 result = current; + for (size_t row = 0; row < 4; ++row) { + for (size_t column = 0; column < 4; ++column) { + const float before = previous[row][column]; + const float after = current[row][column]; + if (std::isfinite(before) && std::isfinite(after)) { + result[row][column] = before + (after - before) * weight; + } + } + } + return result; +} +} // namespace + +bool interpolate_transform(const Mat3x4& previous, const Mat3x4& current, + float weight, Mat3x4& output) noexcept { + return interpolate_affine_impl(previous, current, std::clamp(weight, 0.0f, 1.0f), output); +} + +bool interpolate_transform_midpoint(const Mat3x4& previous, const Mat3x4& current, + Mat3x4& output) noexcept { + return interpolate_transform(previous, current, 0.5f, output); +} + +bool interpolate_indexed_transform(const Mat3x4& previous, + const Mat3x4& current, float weight, + Mat3x4& output) noexcept { + return evaluate_affine_pair(prepare_indexed_pair(previous, current), + std::clamp(weight, 0.0f, 1.0f), output); +} + +float transform_match_distance_squared(const Mat3x4& previous, + const Mat3x4& current) noexcept { + // Translation dominates in game units; the 3x3 part only breaks ties between + // repeated meshes at the same origin. + double distance = 0.0; + const std::array, 3> previousRows{previous.m0, previous.m1, previous.m2}; + const std::array, 3> currentRows{current.m0, current.m1, current.m2}; + for (size_t row = 0; row < 3; ++row) { + for (size_t column = 0; column < 4; ++column) { + const double before = previousRows[row][column]; + const double after = currentRows[row][column]; + if (!std::isfinite(before) || !std::isfinite(after)) { + return std::numeric_limits::infinity(); + } + const double delta = after - before; + distance += delta * delta; + } + } + if (!std::isfinite(distance) || distance > std::numeric_limits::max()) { + return std::numeric_limits::infinity(); + } + return static_cast(distance); +} + +namespace { +// Translation-only delta, matching the kMaximumTranslationPerFrame gate. Non-finite +// inputs propagate NaN; callers compare with `<=` so NaN fails the gate. +float translation_delta_squared(const Mat3x4& previous, + const Mat3x4& current) noexcept { + const float dx = current.m0.w() - previous.m0.w(); + const float dy = current.m1.w() - previous.m1.w(); + const float dz = current.m2.w() - previous.m2.w(); + return dx * dx + dy * dy + dz * dz; +} + +// 2*current - previous. Not a valid rigid transform, but it is only a matching +// reference and its one-frame error stays far below instance spacing. +Mat3x4 extrapolate_transform(const Mat3x4& previous, + const Mat3x4& current) noexcept { + Mat3x4 predicted{}; + const std::array*, 3> previousRows{&previous.m0, &previous.m1, &previous.m2}; + const std::array*, 3> currentRows{¤t.m0, ¤t.m1, ¤t.m2}; + const std::array*, 3> predictedRows{&predicted.m0, &predicted.m1, &predicted.m2}; + for (size_t row = 0; row < 3; ++row) { + for (size_t column = 0; column < 4; ++column) { + (*predictedRows[row])[column] = + 2.0f * (*currentRows[row])[column] - (*previousRows[row])[column]; + } + } + return predicted; +} +} // namespace + +float snapshot_match_distance_squared(const FrameTransformEntry& previousEntry, + const FrameTransformSnapshot& current) noexcept { + const FrameTransformSnapshot& previous = previousEntry.transform; + if (static_cast(previous.indexedMatrices) != static_cast(current.indexedMatrices)) { + return std::numeric_limits::infinity(); + } + if (!current.indexedMatrices) { + // Match against the predicted position, falling back to the last known one for + // entries with no motion history. + const Mat3x4& reference = + previousEntry.hasPrediction ? previousEntry.predictedPosition : previous.position; + return transform_match_distance_squared(reference, current.position); + } + + if (previous.usedMatrixMask == 0 || current.usedMatrixMask == 0) { + return std::numeric_limits::infinity(); + } + + // PNMTXIDX is an absolute palette slot baked into the vertices, so nearest matching + // swaps joint histories. A changed mask has no safe correspondence: leave the draw. + if (previous.usedMatrixMask != current.usedMatrixMask) { + return std::numeric_limits::infinity(); + } + + double distance = 0.0; + size_t matchedMatrices = 0; + for (size_t currentIndex = 0; currentIndex < MaxPnMtx; ++currentIndex) { + if ((current.usedMatrixMask & (1u << currentIndex)) == 0) { + continue; + } + const float matrixDistance = transform_match_distance_squared( + previous.indexedMatrices->position[currentIndex], + current.indexedMatrices->position[currentIndex]); + if (!std::isfinite(matrixDistance)) { + return std::numeric_limits::infinity(); + } + distance += matrixDistance; + ++matchedMatrices; + } + if (matchedMatrices == 0 || !std::isfinite(distance) || + distance > std::numeric_limits::max()) { + return std::numeric_limits::infinity(); + } + return static_cast(distance / static_cast(matchedMatrices)); +} + +void set_frame_interpolation_fps(uint32_t targetFps) noexcept { + if (targetFps != 120 && targetFps != 180 && targetFps != 240) { + targetFps = 0; + } + detail::g_frameInterpolationFps.store(targetFps, std::memory_order_release); + // Start at the configured quality; the controller only ever backs off from here. + s_interpolationSampleTarget.store(targetFps == 0 ? 0u : std::min(targetFps / 60 - 1, MaxInterpolatedFrames), + std::memory_order_release); + s_dropInterpolationAtSeal.store(false, std::memory_order_release); + s_pacingWindowFrames.store(0, std::memory_order_release); + s_pacingWindowMisses.store(0, std::memory_order_release); + // A reconfiguration starts a fresh diagnostic window. + s_diagFramesSealed.store(0, std::memory_order_relaxed); + s_diagFramesLowMatch.store(0, std::memory_order_relaxed); + s_diagFramesReplayUnsafe.store(0, std::memory_order_relaxed); + s_diagSlotReductions.store(0, std::memory_order_relaxed); + s_diagLateSealDrops.store(0, std::memory_order_relaxed); +} + +void report_producer_paced(bool paced) noexcept { + const uint32_t maximumSamples = maximum_interpolation_samples(); + if (maximumSamples == 0) { + s_dropInterpolationAtSeal.store(false, std::memory_order_release); + s_pacingWindowFrames.store(0, std::memory_order_release); + s_pacingWindowMisses.store(0, std::memory_order_release); + s_interpolationSampleTarget.store(0, std::memory_order_release); + return; + } + + // Per-frame decision, no streaks: a late frame seals without its inserted slots and + // the next one is back at full count. See s_dropInterpolationAtSeal. + s_dropInterpolationAtSeal.store(!paced, std::memory_order_release); + + const uint32_t frames = s_pacingWindowFrames.load(std::memory_order_acquire) + 1; + const uint32_t misses = + s_pacingWindowMisses.load(std::memory_order_acquire) + (paced ? 0u : 1u); + if (frames < kPacingWindowFrames) { + s_pacingWindowFrames.store(frames, std::memory_order_release); + s_pacingWindowMisses.store(misses, std::memory_order_release); + return; + } + // Window complete: adjust the target one step at most, then start over. At + // one decision per window this cannot spam the log or flap within a second. + uint32_t target = + std::min(s_interpolationSampleTarget.load(std::memory_order_acquire), maximumSamples); + if (misses * 100u >= frames * kPacingReducePercent) { + if (target > 0) { + --target; + s_diagSlotReductions.fetch_add(1, std::memory_order_relaxed); + Log.info("interpolation slots reduced to {}: {}/{} frames overran their retrace budget", + target, misses, frames); + } + } else if (misses * 100u <= frames * kPacingRestorePercent) { + if (target < maximumSamples) { + ++target; + Log.info("interpolation slots restored to {}: {}/{} frames overran their retrace budget", + target, misses, frames); + } + } + s_interpolationSampleTarget.store(target, std::memory_order_release); + s_pacingWindowFrames.store(0, std::memory_order_release); + s_pacingWindowMisses.store(0, std::memory_order_release); +} + +void begin_frame_interpolation() noexcept { + const uint32_t targetFps = frame_interpolation_fps(); + if (targetFps != s_previousInterpolationFps) { + recycle_transform_entries(s_previousFrameTransforms); + s_previousInterpolationFps = targetFps; + } + // Latch the slot count for the frame starting here; see s_activeInterpolationSamples + // for why it cannot move again until the seal. + s_activeInterpolationSamples.store( + std::min(s_interpolationSampleTarget.load(std::memory_order_acquire), + maximum_interpolation_samples()), + std::memory_order_release); + // Normally empty (finalize swapped and recycled); an aborted frame can leave + // entries behind, whose matrix blocks go back to the pool here. + recycle_transform_entries(s_currentFrameTransforms); + // Keep buckets, nodes and capacities across frames so a stable race scene does not + // pay thousands of small allocations every frame. + clear_index_map_keep_nodes(s_previousTransformIndices, s_previousFrameTransforms.size()); + clear_index_map_keep_nodes(s_previousStableTransformIndices, s_previousFrameTransforms.size()); + for (size_t i = 0; i < s_previousFrameTransforms.size(); ++i) { + const auto& identity = s_previousFrameTransforms[i].identity; + s_previousTransformIndices[identity.combined].push_back(i); + s_previousStableTransformIndices[stable_identity(identity)].push_back(i); + } + clear_index_map_keep_nodes(s_currentTransformIndices, s_previousFrameTransforms.size()); + clear_index_map_keep_nodes(s_currentStableTransformIndices, s_previousFrameTransforms.size()); + s_pendingUniformInterpolations.clear(); + s_perspectiveCandidates = 0; + s_perspectiveMatchable = 0; + s_perspectiveMatches = 0; + s_hasInterpolatedFrame.store(false, std::memory_order_release); + s_frameInterpolationReplaySafe.store(true, std::memory_order_release); +} + +void finalize_frame_interpolation() noexcept { + // A frame reported late seals without inserted slots, so the encode phase renders + // the native frame only. Its transforms still seed the next frame's matching. + if (s_dropInterpolationAtSeal.exchange(false, std::memory_order_acq_rel)) { + s_diagLateSealDrops.fetch_add(1, std::memory_order_relaxed); + s_hasInterpolatedFrame.store(false, std::memory_order_release); + s_pendingUniformInterpolations.clear(); + retire_frame_transforms(); + return; + } + // Below this bound a direct all-pairs build is cheaper than setting up the + // spatial grid; the produced edge set is identical either way. + constexpr size_t kAllPairsEdgeLimit = 1024; + // Bounds the all-pairs cost for indexed groups, which have no single translation to + // bucket on. Non-indexed groups of any size use the grid instead. + constexpr size_t kMaximumAssignmentEdges = 16384; + // Hard bound for the grid path; only a swarm co-located within one cell reaches it, + // which is the CPU-deformed shape the ordered fallback exists for. + constexpr size_t kMaximumGridEdges = 65536; + + struct MatchEdge { + float distance = std::numeric_limits::infinity(); + size_t previous = SIZE_MAX; + size_t current = SIZE_MAX; + }; + + // Frame-persistent matching scratch: sized to the frame's draw count, so a + // stable scene performs no matching allocations at all after warm-up. + static std::vector currentToPrevious; + static std::vector previousMatched; + static std::vector currentMatched; + static std::vector groupCurrentIndices; + static std::vector groupPreviousIndices; + static std::vector edges; + static std::unordered_map> gridCells; + currentToPrevious.assign(s_currentFrameTransforms.size(), SIZE_MAX); + previousMatched.assign(s_previousFrameTransforms.size(), 0); + currentMatched.assign(s_currentFrameTransforms.size(), 0); + + // Quantizes a translation onto the matching grid. Cell size equals the gate distance, + // so every acceptable pair lies within one cell along every axis. + constexpr uint64_t kInvalidCell = std::numeric_limits::max(); + const auto translation_cell = [](const Mat3x4& m) noexcept -> uint64_t { + const float x = m.m0.w(); + const float y = m.m1.w(); + const float z = m.m2.w(); + if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z)) { + return kInvalidCell; + } + constexpr float kInverseCell = 1.0f / kMaximumTranslationPerFrame; + constexpr int64_t kBias = int64_t{1} << 20; + const int64_t cx = static_cast(std::floor(x * kInverseCell)) + kBias; + const int64_t cy = static_cast(std::floor(y * kInverseCell)) + kBias; + const int64_t cz = static_cast(std::floor(z * kInverseCell)) + kBias; + // One cell of margin at each field edge so the +-1 neighbor arithmetic cannot borrow + // into the next packed coordinate. + if (cx < 1 || cy < 1 || cz < 1 || cx >= (kBias << 1) - 1 || cy >= (kBias << 1) - 1 || + cz >= (kBias << 1) - 1) { + return kInvalidCell; + } + return (static_cast(cx) << 42) | (static_cast(cy) << 21) | + static_cast(cz); + }; + + const auto matchGroups = + [&](const auto& currentGroups, const auto& previousGroups, + bool allowOrderedFallback) { + for (const auto& [signature, allCurrentIndices] : currentGroups) { + if (allCurrentIndices.empty()) { + continue; + } + const auto previousIt = previousGroups.find(signature); + if (previousIt == previousGroups.end() || previousIt->second.empty()) { + continue; + } + groupCurrentIndices.clear(); + groupPreviousIndices.clear(); + for (const size_t index : allCurrentIndices) { + if (!currentMatched[index]) { + groupCurrentIndices.push_back(index); + } + } + for (const size_t index : previousIt->second) { + if (!previousMatched[index]) { + groupPreviousIndices.push_back(index); + } + } + if (groupCurrentIndices.empty() || groupPreviousIndices.empty()) { + continue; + } + + // A unique draw has no identity ambiguity. Keep the conservative + // interpolation fallback for malformed/non-finite matrices. + if (groupPreviousIndices.size() == 1 && groupCurrentIndices.size() == 1) { + currentToPrevious[groupCurrentIndices.front()] = groupPreviousIndices.front(); + previousMatched[groupPreviousIndices.front()] = 1; + currentMatched[groupCurrentIndices.front()] = 1; + continue; + } + + // Drop pairs past the translation gate (`<=` also drops a NaN delta): they can only + // render as a snap, and matching them steals a neighbor's true partner. + const auto appendEdge = [&](size_t previousIndex, size_t currentIndex) { + const auto& previousEntry = s_previousFrameTransforms[previousIndex]; + const auto& currentSnapshot = s_currentFrameTransforms[currentIndex].transform; + if (!previousEntry.transform.indexedMatrices && !currentSnapshot.indexedMatrices && + !(translation_delta_squared(previousEntry.transform.position, + currentSnapshot.position) <= + kMaximumTranslationPerFrame * kMaximumTranslationPerFrame)) { + return; + } + const float distance = snapshot_match_distance_squared(previousEntry, currentSnapshot); + if (std::isfinite(distance)) { + edges.push_back({ + .distance = distance, + .previous = previousIndex, + .current = currentIndex, + }); + } + }; + + const auto orderedFallback = [&] { + // GX preserves submission order for these draws, so pair the leftovers in order + // once exact identities have claimed their instances. + if (!allowOrderedFallback) { + return; + } + const size_t pairCount = std::min(groupPreviousIndices.size(), groupCurrentIndices.size()); + for (size_t i = 0; i < pairCount; ++i) { + // Submission order shifts when the transparent sort reorders or culling drops an + // instance; leave over-gate pairs unmatched so they snap instead of sweeping. + const auto& previousEntry = s_previousFrameTransforms[groupPreviousIndices[i]]; + const auto& currentEntry = s_currentFrameTransforms[groupCurrentIndices[i]]; + if (!previousEntry.transform.indexedMatrices && + !currentEntry.transform.indexedMatrices && + !(translation_delta_squared(previousEntry.transform.position, + currentEntry.transform.position) <= + kMaximumTranslationPerFrame * kMaximumTranslationPerFrame)) { + continue; + } + currentToPrevious[groupCurrentIndices[i]] = groupPreviousIndices[i]; + previousMatched[groupPreviousIndices[i]] = 1; + currentMatched[groupCurrentIndices[i]] = 1; + } + }; + + edges.clear(); + const size_t allPairsEdges = groupPreviousIndices.size() * groupCurrentIndices.size(); + if (allPairsEdges <= kAllPairsEdgeLimit) { + for (const size_t currentIndex : groupCurrentIndices) { + for (const size_t previousIndex : groupPreviousIndices) { + appendEdge(previousIndex, currentIndex); + } + } + } else { + // Grid buckets need one translation per entry, which mixed or skinned groups lack, + // so those stay on the bounded all-pairs path. + bool gridable = true; + for (const size_t index : groupCurrentIndices) { + if (s_currentFrameTransforms[index].transform.indexedMatrices) { + gridable = false; + break; + } + } + if (gridable) { + for (const size_t index : groupPreviousIndices) { + if (s_previousFrameTransforms[index].transform.indexedMatrices) { + gridable = false; + break; + } + } + } + if (!gridable) { + if (groupPreviousIndices.size() > kMaximumAssignmentEdges / groupCurrentIndices.size()) { + orderedFallback(); + continue; + } + for (const size_t currentIndex : groupCurrentIndices) { + for (const size_t previousIndex : groupPreviousIndices) { + appendEdge(previousIndex, currentIndex); + } + } + } else { + // Previous entries are matched against their predicted position, so bucket them by + // that same reference (see snapshot_match_distance_squared). + gridCells.clear(); + for (const size_t previousIndex : groupPreviousIndices) { + const auto& previousEntry = s_previousFrameTransforms[previousIndex]; + const Mat3x4& reference = previousEntry.hasPrediction + ? previousEntry.predictedPosition + : previousEntry.transform.position; + const uint64_t cell = translation_cell(reference); + if (cell == kInvalidCell) { + continue; + } + gridCells[cell].push_back(static_cast(previousIndex)); + } + bool overflowed = false; + for (const size_t currentIndex : groupCurrentIndices) { + const auto& position = s_currentFrameTransforms[currentIndex].transform.position; + const uint64_t cell = translation_cell(position); + if (cell == kInvalidCell) { + continue; + } + for (int64_t dx = -1; dx <= 1 && !overflowed; ++dx) { + for (int64_t dy = -1; dy <= 1 && !overflowed; ++dy) { + for (int64_t dz = -1; dz <= 1 && !overflowed; ++dz) { + const uint64_t neighbor = cell + (static_cast(dx) << 42) + + (static_cast(dy) << 21) + + static_cast(dz); + const auto cellIt = gridCells.find(neighbor); + if (cellIt == gridCells.end()) { + continue; + } + for (const uint32_t previousIndex : cellIt->second) { + appendEdge(previousIndex, currentIndex); + if (edges.size() > kMaximumGridEdges) { + overflowed = true; + break; + } + } + } + } + } + if (overflowed) { + break; + } + } + if (overflowed) { + orderedFallback(); + continue; + } + } + } + + std::sort(edges.begin(), edges.end(), [](const MatchEdge& lhs, const MatchEdge& rhs) { + return lhs.distance < rhs.distance; + }); + for (const auto& edge : edges) { + if (previousMatched[edge.previous] || currentMatched[edge.current]) { + continue; + } + currentToPrevious[edge.current] = edge.previous; + previousMatched[edge.previous] = 1; + currentMatched[edge.current] = 1; + } + } + }; + matchGroups(s_currentTransformIndices, s_previousTransformIndices, false); + matchGroups(s_currentStableTransformIndices, s_previousStableTransformIndices, true); + + s_perspectiveMatches = static_cast(std::count_if( + currentToPrevious.begin(), currentToPrevious.end(), + [](size_t previousIndex) { return previousIndex != SIZE_MAX; })); + // Interpolation never pauses on match quality: an unmatched draw just renders its + // end-frame state, while a ratio gate flapped the whole output cadence instead. + const bool eligible = frame_interpolation_fps() != 0; + + // Overlay observability: the live match ratio, and how often the scene sits in + // low-match territory where inserted slots mostly duplicate draws. + { + constexpr uint32_t kLowMatchPercent = 55; + const bool replaySafe = frame_interpolation_replay_safe(); + s_diagCandidates.store(s_perspectiveCandidates, std::memory_order_relaxed); + s_diagMatchable.store(s_perspectiveMatchable, std::memory_order_relaxed); + s_diagMatches.store(s_perspectiveMatches, std::memory_order_relaxed); + s_diagEligible.store(eligible, std::memory_order_relaxed); + s_diagReplaySafe.store(replaySafe, std::memory_order_relaxed); + if (eligible) { + s_diagFramesSealed.fetch_add(1, std::memory_order_relaxed); + if (s_perspectiveMatchable != 0 && + s_perspectiveMatches * 100 < s_perspectiveMatchable * kLowMatchPercent) { + s_diagFramesLowMatch.fetch_add(1, std::memory_order_relaxed); + } + if (!replaySafe) { + s_diagFramesReplayUnsafe.fetch_add(1, std::memory_order_relaxed); + } + } + } + + // Only ordinary one-matrix draws use motion prediction. Indexed draws keep the slot + // identity encoded by PNMTXIDX and are prepared as one coherent draw below. + constexpr float kMaximumPredictionSeedDeltaSquared = + kMaximumTranslationPerFrame * kMaximumTranslationPerFrame; + for (size_t currentIndex = 0; currentIndex < currentToPrevious.size(); ++currentIndex) { + const size_t previousIndex = currentToPrevious[currentIndex]; + if (previousIndex == SIZE_MAX) { + continue; + } + auto& currentEntry = s_currentFrameTransforms[currentIndex]; + const auto& previousEntry = s_previousFrameTransforms[previousIndex]; + if (currentEntry.transform.indexedMatrices || previousEntry.transform.indexedMatrices) { + continue; + } + // Seed the next frame's matching with a constant-velocity reference, but never from + // a pair the interpolator would reject as a teleport (`<=` so NaN fails too). + if (translation_delta_squared(previousEntry.transform.position, + currentEntry.transform.position) <= + kMaximumPredictionSeedDeltaSquared) { + currentEntry.predictedPosition = extrapolate_transform(previousEntry.transform.position, + currentEntry.transform.position); + currentEntry.hasPrediction = true; + } + } + + if (eligible) { + // Prepare each matched pair once: every sample of a draw shares the same + // previous/current matrices. A flat vector keeps the sample tasks parallel. + std::vector preparedTransforms(s_currentFrameTransforms.size()); + std::vector preparedTransformState(s_currentFrameTransforms.size(), 0); + std::vector preparedPairs; + preparedPairs.reserve(s_pendingUniformInterpolations.size() * 2); + + const auto appendPreparedPair = [&](const Mat3x4& previousPosition, + const Mat3x4& currentPosition, + const Mat3x4& previousNormal, + const Mat3x4& currentNormal, + bool indexed) { + const size_t pairOffset = preparedPairs.size(); + preparedPairs.push_back(indexed ? prepare_indexed_pair(previousPosition, currentPosition) + : prepare_affine_pair(previousPosition, currentPosition)); + preparedPairs.push_back(indexed ? prepare_indexed_pair(previousNormal, currentNormal) + : prepare_affine_pair(previousNormal, currentNormal)); + return pairOffset; + }; + + // Content-keyed palette pairing: a mesh split across draws repeats bone matrices byte + // for byte, so a chunk with no partner of its own can borrow a sibling's. + static std::vector paletteDrawIndex; // draw -> compact palette index + static std::vector paletteDraws; // compact palette index -> draw + paletteDrawIndex.assign(s_currentFrameTransforms.size(), -1); + paletteDraws.clear(); + for (size_t drawIndex = 0; drawIndex < s_currentFrameTransforms.size(); ++drawIndex) { + const auto& transform = s_currentFrameTransforms[drawIndex].transform; + if (!transform.indexedMatrices || transform.usedMatrixMask == 0) { + continue; + } + paletteDrawIndex[drawIndex] = static_cast(paletteDraws.size()); + paletteDraws.push_back(static_cast(drawIndex)); + } + + struct PaletteSlotKey { + HashType hash = 0; + uint32_t palette = 0; + uint32_t slot = 0; + }; + struct ResolvedSlot { + const Mat3x4* position = nullptr; + const Mat3x4* normal = nullptr; + }; + static std::vector paletteSlotKeys; + static std::vector resolvedSlots; + static std::vector resolvedProjectionEntry; + paletteSlotKeys.clear(); + resolvedSlots.assign(paletteDraws.size() * MaxPnMtx, ResolvedSlot{}); + resolvedProjectionEntry.assign(paletteDraws.size(), kNoPreparedPair); + + for (uint32_t palette = 0; palette < paletteDraws.size(); ++palette) { + const auto& transform = s_currentFrameTransforms[paletteDraws[palette]].transform; + for (uint32_t slot = 0; slot < MaxPnMtx; ++slot) { + if ((transform.usedMatrixMask & (1u << slot)) == 0) { + continue; + } + paletteSlotKeys.push_back({transform.indexedMatrices->slotHash[slot], palette, slot}); + } + } + std::sort(paletteSlotKeys.begin(), paletteSlotKeys.end(), + [](const PaletteSlotKey& lhs, const PaletteSlotKey& rhs) { + if (lhs.hash != rhs.hash) { + return lhs.hash < rhs.hash; + } + if (lhs.palette != rhs.palette) { + return lhs.palette < rhs.palette; + } + return lhs.slot < rhs.slot; + }); + + for (size_t runStart = 0; runStart < paletteSlotKeys.size();) { + size_t runEnd = runStart + 1; + while (runEnd < paletteSlotKeys.size() && + paletteSlotKeys[runEnd].hash == paletteSlotKeys[runStart].hash) { + ++runEnd; + } + // Where this matrix was last frame, per the draws that did match. Partners that + // disagree mean no single previous pose, so the coupled unit duplicates. + const Mat3x4* sourcePosition = nullptr; + const Mat3x4* sourceNormal = nullptr; + size_t sourceEntry = kNoPreparedPair; + HashType sourceHash = 0; + bool ambiguous = false; + for (size_t keyIndex = runStart; keyIndex < runEnd && !ambiguous; ++keyIndex) { + const uint32_t drawIndex = paletteDraws[paletteSlotKeys[keyIndex].palette]; + const size_t previousIndex = currentToPrevious[drawIndex]; + if (previousIndex >= s_previousFrameTransforms.size()) { + continue; + } + const auto& current = s_currentFrameTransforms[drawIndex].transform; + const auto& previous = s_previousFrameTransforms[previousIndex].transform; + // A slot index is an absolute palette address, which is why it pairs matched draws + // and why a changed layout invalidates every slot as a source. + if (!previous.indexedMatrices || previous.usedMatrixMask != current.usedMatrixMask) { + continue; + } + const uint32_t slot = paletteSlotKeys[keyIndex].slot; + const HashType candidateHash = previous.indexedMatrices->slotHash[slot]; + if (sourcePosition == nullptr) { + sourcePosition = &previous.indexedMatrices->position[slot]; + sourceNormal = &previous.indexedMatrices->normal[slot]; + sourceHash = candidateHash; + sourceEntry = previousIndex; + } else if (candidateHash != sourceHash) { + ambiguous = true; + } + } + if (ambiguous || sourcePosition == nullptr) { + runStart = runEnd; + continue; + } + for (size_t keyIndex = runStart; keyIndex < runEnd; ++keyIndex) { + const auto& key = paletteSlotKeys[keyIndex]; + resolvedSlots[static_cast(key.palette) * MaxPnMtx + key.slot] = {sourcePosition, + sourceNormal}; + if (resolvedProjectionEntry[key.palette] == kNoPreparedPair) { + resolvedProjectionEntry[key.palette] = sourceEntry; + } + } + runStart = runEnd; + } + + for (const auto& task : s_pendingUniformInterpolations) { + if (task.currentTransformIndex >= preparedTransformState.size() || + preparedTransformState[task.currentTransformIndex] != 0) { + continue; + } + preparedTransformState[task.currentTransformIndex] = 1; + + const auto& current = s_currentFrameTransforms[task.currentTransformIndex].transform; + auto& prepared = preparedTransforms[task.currentTransformIndex]; + if (task.indexedMatrices) { + const int32_t palette = paletteDrawIndex[task.currentTransformIndex]; + if (palette < 0) { + continue; + } + prepared.previousProjectionEntry = resolvedProjectionEntry[palette]; + // A palette is one deformation unit: interpolating only the resolved slots cracks + // the mesh, so any unresolved slot duplicates the whole current draw. + bool allSlotsValid = true; + for (size_t slot = 0; slot < MaxPnMtx; ++slot) { + if ((current.usedMatrixMask & (1u << slot)) == 0) { + continue; + } + const auto& resolved = resolvedSlots[static_cast(palette) * MaxPnMtx + slot]; + if (resolved.position == nullptr) { + allSlotsValid = false; + break; + } + const size_t pairOffset = + appendPreparedPair(*resolved.position, current.indexedMatrices->position[slot], + *resolved.normal, current.indexedMatrices->normal[slot], true); + prepared.indexedPairOffsets[slot] = pairOffset; + if (!preparedPairs[pairOffset].valid || !preparedPairs[pairOffset + 1].valid) { + allSlotsValid = false; + break; + } + } + prepared.indexedValid = allSlotsValid; + } else { + const size_t previousTransformIndex = currentToPrevious[task.currentTransformIndex]; + if (previousTransformIndex >= s_previousFrameTransforms.size()) { + continue; + } + const auto& previous = s_previousFrameTransforms[previousTransformIndex].transform; + prepared.previousProjectionEntry = previousTransformIndex; + prepared.nonIndexedPairOffset = appendPreparedPair( + previous.position, current.position, previous.normal, current.normal, false); + } + } + + const auto interpolatePendingUniform = [&](const auto& task) { + if (task.currentTransformIndex >= s_currentFrameTransforms.size()) { + return; + } + + const auto& current = s_currentFrameTransforms[task.currentTransformIndex].transform; + std::memcpy(task.uniformData, task.sourceUniformData, task.uniformSize); + const auto& prepared = preparedTransforms[task.currentTransformIndex]; + if (task.indexedMatrices && !prepared.indexedValid) { + return; + } + // The projection comes from whichever previous entry supplied the transforms, which + // for a borrowed palette is a sibling's partner. + const size_t previousTransformIndex = prepared.previousProjectionEntry; + if (previousTransformIndex >= s_previousFrameTransforms.size()) { + return; + } + const auto& previous = s_previousFrameTransforms[previousTransformIndex].transform; + const float weight = + static_cast(task.numerator) / static_cast(task.denominator); + const auto interpolatedProjection = + interpolate_projection(previous.projection, current.projection, weight); + std::memcpy(task.uniformData + task.projectionOffset, &interpolatedProjection, + sizeof(interpolatedProjection)); + + const auto interpolateMatrixSlot = [&](size_t currentIndex, size_t pairOffset) { + if (pairOffset == kNoPreparedPair) { + return; + } + Mat3x4 interpolatedPosition{}; + Mat3x4 interpolatedNormal{}; + evaluate_affine_pair(preparedPairs[pairOffset], weight, interpolatedPosition); + evaluate_affine_pair(preparedPairs[pairOffset + 1], weight, interpolatedNormal); + std::memcpy(task.uniformData + task.positionOffset + currentIndex * sizeof(Mat3x4), + &interpolatedPosition, sizeof(interpolatedPosition)); + std::memcpy(task.uniformData + task.normalOffset + currentIndex * sizeof(Mat3x4), + &interpolatedNormal, sizeof(interpolatedNormal)); + }; + if (task.indexedMatrices) { + if (!current.indexedMatrices) { + return; + } + for (size_t currentIndex = 0; currentIndex < MaxPnMtx; ++currentIndex) { + if ((current.usedMatrixMask & (1u << currentIndex)) == 0) { + continue; + } + interpolateMatrixSlot(currentIndex, prepared.indexedPairOffsets[currentIndex]); + } + } else { + interpolateMatrixSlot(task.currentMatrix, prepared.nonIndexedPairOffset); + } + }; + // A handful of tasks costs more to schedule than to run. The pool stands in for + // std::execution::par, which libc++ does not provide at all. + constexpr size_t kMinimumParallelInterpolationTasks = 64; + if (s_pendingUniformInterpolations.size() < kMinimumParallelInterpolationTasks) { + std::for_each(s_pendingUniformInterpolations.begin(), s_pendingUniformInterpolations.end(), + interpolatePendingUniform); + } else { + InterpolationWorkerPool::instance().run( + s_pendingUniformInterpolations.size(), + [&](size_t index) { interpolatePendingUniform(s_pendingUniformInterpolations[index]); }); + } + } + s_hasInterpolatedFrame.store(eligible, std::memory_order_release); + s_pendingUniformInterpolations.clear(); + // The index maps are emptied node-preservingly by the next + // begin_frame_interpolation. + retire_frame_transforms(); +} + +void get_frame_interpolation_diagnostics(AuroraFrameInterpolationDiagnostics& diagnostics) noexcept { + diagnostics.targetFps = frame_interpolation_fps(); + diagnostics.targetSamples = std::min(s_interpolationSampleTarget.load(std::memory_order_acquire), + maximum_interpolation_samples()); + diagnostics.activeSamples = s_activeInterpolationSamples.load(std::memory_order_acquire); + diagnostics.candidates = s_diagCandidates.load(std::memory_order_relaxed); + diagnostics.matchable = s_diagMatchable.load(std::memory_order_relaxed); + diagnostics.matches = s_diagMatches.load(std::memory_order_relaxed); + diagnostics.eligible = s_diagEligible.load(std::memory_order_relaxed) ? 1 : 0; + diagnostics.replaySafe = s_diagReplaySafe.load(std::memory_order_relaxed) ? 1 : 0; + diagnostics.framesSealed = s_diagFramesSealed.load(std::memory_order_relaxed); + diagnostics.framesLowMatch = s_diagFramesLowMatch.load(std::memory_order_relaxed); + diagnostics.framesReplayUnsafe = s_diagFramesReplayUnsafe.load(std::memory_order_relaxed); + diagnostics.slotReductions = s_diagSlotReductions.load(std::memory_order_relaxed); + diagnostics.lateSealDrops = s_diagLateSealDrops.load(std::memory_order_relaxed); +} + +bool has_interpolated_frame() noexcept { + return s_hasInterpolatedFrame.load(std::memory_order_acquire); +} + +uint32_t interpolated_frame_count() noexcept { + if (!has_interpolated_frame()) { + return 0; + } + // The count latched when this frame began recording, not the configured maximum: + // build_uniform staged exactly this many ranges for every draw. + return s_activeInterpolationSamples.load(std::memory_order_acquire); +} + +void drop_pending_frame_interpolation_uniforms() noexcept { + // Pending tasks hold raw pointers into the mapped uniform staging range, so anything + // that unmaps or rotates that buffer first has to drop the tasks. + s_pendingUniformInterpolations.clear(); +} + +void mark_frame_interpolation_replay_unsafe() noexcept { + s_frameInterpolationReplaySafe.store(false, std::memory_order_release); + // Marking the frame unsafe rotates the staging buffer mid-frame, and the seal then + // duplicates slots instead of replaying, so drop the copies staged so far. + drop_pending_frame_interpolation_uniforms(); +} + +bool frame_interpolation_replay_safe() noexcept { + return s_frameInterpolationReplaySafe.load(std::memory_order_acquire); +} + +void extend_interpolation_draw(uint16_t usedPnMtxMask) noexcept { + if (s_currentFrameTransforms.empty()) { + return; + } + auto& snapshot = s_currentFrameTransforms.back().transform; + if (!snapshot.indexedMatrices) { + // One-matrix draws all read the current matrix index, which a merge cannot + // have changed either. + return; + } + const uint16_t addedSlots = static_cast(usedPnMtxMask & ~snapshot.usedMatrixMask); + if (addedSlots == 0) { + return; + } + for (size_t slot = 0; slot < MaxPnMtx; ++slot) { + if ((addedSlots & (1u << slot)) == 0) { + continue; + } + snapshot.indexedMatrices->position[slot] = g_gxState.pnMtx[slot].pos; + snapshot.indexedMatrices->normal[slot] = g_gxState.pnMtx[slot].nrm; + snapshot.indexedMatrices->slotHash[slot] = + xxh3_hash_s(&g_gxState.pnMtx[slot].pos, sizeof(Mat3x4), + xxh3_hash_s(&g_gxState.pnMtx[slot].nrm, sizeof(Mat3x4))); + } + snapshot.usedMatrixMask |= addedSlots; +} + +std::array record_interpolation_draw( + const FrameInterpolationDrawIdentity& identity, const Mat4x4& projection, + uint16_t usedPnMtxMask, const InterpolatedUniformLayout& uniformLayout) noexcept { + FrameTransformSnapshot snapshot{ + .projection = projection, + .usedMatrixMask = usedPnMtxMask, + }; + if (uniformLayout.indexedMatrices) { + snapshot.indexedMatrices = acquire_indexed_matrices(); + // Only the used slots are copied; consumers mask with usedMatrixMask, so the stale + // pool contents of unused slots are never read. + for (size_t i = 0; i < MaxPnMtx; ++i) { + if ((usedPnMtxMask & (1u << i)) == 0) { + continue; + } + snapshot.indexedMatrices->position[i] = g_gxState.pnMtx[i].pos; + snapshot.indexedMatrices->normal[i] = g_gxState.pnMtx[i].nrm; + snapshot.indexedMatrices->slotHash[i] = + xxh3_hash_s(&g_gxState.pnMtx[i].pos, sizeof(Mat3x4), + xxh3_hash_s(&g_gxState.pnMtx[i].nrm, sizeof(Mat3x4))); + } + } else { + const size_t currentMatrix = std::min(g_gxState.currentPnMtx, MaxPnMtx - 1); + snapshot.position = g_gxState.pnMtx[currentMatrix].pos; + snapshot.normal = g_gxState.pnMtx[currentMatrix].nrm; + } + + const size_t currentTransformIndex = s_currentFrameTransforms.size(); + s_currentFrameTransforms.emplace_back(FrameTransformEntry{ + .identity = identity, + .transform = std::move(snapshot), + }); + s_currentTransformIndices[identity.combined].push_back(currentTransformIndex); + const HashType stableIdentity = stable_identity(identity); + s_currentStableTransformIndices[stableIdentity].push_back(currentTransformIndex); + + std::array interpolatedRanges{}; + ++s_perspectiveCandidates; + const auto exactPrevious = s_previousTransformIndices.find(identity.combined); + const auto stablePrevious = s_previousStableTransformIndices.find(stableIdentity); + const bool hasPreviousPartner = + (exactPrevious != s_previousTransformIndices.end() && !exactPrevious->second.empty()) || + (stablePrevious != s_previousStableTransformIndices.end() && !stablePrevious->second.empty()); + if (hasPreviousPartner) { + ++s_perspectiveMatchable; + } + // A skinned draw with no identity partner can still borrow sibling transforms at + // seal time, so stage copies whenever the previous frame held any palette. + const bool stageInterpolation = + hasPreviousPartner || + (uniformLayout.indexedMatrices && s_previousFrameHasIndexedMatrices); + // A frame already split by a submitted prefix duplicates its slots instead of + // replaying, so staging copies for the resumed suffix would only waste space. + if (frame_interpolation_replay_safe() && stageInterpolation) { + const uint32_t sampleCount = s_activeInterpolationSamples.load(std::memory_order_acquire); + for (uint32_t sample = 0; sample < sampleCount; ++sample) { + auto [interpolatedBuffer, interpolatedRange] = gfx::map_uniform(uniformLayout.uniformSize); + s_pendingUniformInterpolations.push_back({ + .currentTransformIndex = currentTransformIndex, + .sourceUniformData = uniformLayout.sourceUniformData, + .uniformData = interpolatedBuffer.data(), + .uniformSize = uniformLayout.uniformSize, + .projectionOffset = uniformLayout.projectionOffset, + .positionOffset = uniformLayout.positionOffset, + .normalOffset = uniformLayout.normalOffset, + .currentMatrix = uniformLayout.currentMatrix, + .numerator = sample + 1, + .denominator = sampleCount + 1, + .indexedMatrices = uniformLayout.indexedMatrices, + }); + interpolatedRanges[sample] = interpolatedRange; + } + } + return interpolatedRanges; +} +} // namespace aurora::gx diff --git a/aurora-main/lib/gx/frame_interpolation.hpp b/aurora-main/lib/gx/frame_interpolation.hpp new file mode 100644 index 0000000..b38ffed --- /dev/null +++ b/aurora-main/lib/gx/frame_interpolation.hpp @@ -0,0 +1,89 @@ +#pragma once + +#include "gx.hpp" + +#include "aurora/gfx.h" + +#include + +// Frame interpolation: generates intermediate presentation frames between two consecutive 60 Hz +// guest frames by re-staging each perspective draw's uniform block with interpolated transforms. +namespace aurora::gx { +constexpr uint32_t MaxInterpolatedFrames = 3; + +// Identifies a draw across frames. `combined` includes the geometry signature and is exact, while +// `pipeline`/`texture` is the material-only fallback for meshes whose vertex data changes. +struct FrameInterpolationDrawIdentity { + HashType combined = 0; + HashType pipeline = 0; + HashType texture = 0; + // Hash of the per-vertex PNMTXIDX stream. Matrix values animate, but this topology decides which + // absolute palette slot each vertex reads, so it must match across a pair. + HashType matrixTopology = 0; +}; + +// Where the transforms live inside a draw's staged uniform block. Offsets are +// relative to the start of the mapped range. +struct InterpolatedUniformLayout { + const uint8_t* sourceUniformData = nullptr; + size_t uniformSize = 0; + size_t projectionOffset = 0; + size_t positionOffset = 0; + size_t normalOffset = 0; + // Slot the live matrix occupies; a compacted position region holds it at 0. + size_t currentMatrix = 0; + bool indexedMatrices = false; +}; + +namespace detail { +// Defined in frame_interpolation.cpp, exposed so the early-outs below stay inline: the shipped +// build compiles shards without LTO, so a cross-TU call would land on every draw in the frame. +extern std::atomic_uint32_t g_frameInterpolationFps; +} // namespace detail + +// 0 when interpolation is disabled; otherwise the configured target (120/180/240). +inline uint32_t frame_interpolation_fps() noexcept { + return detail::g_frameInterpolationFps.load(std::memory_order_acquire); +} +inline bool frame_interpolation_active() noexcept { return frame_interpolation_fps() != 0; } + +void set_frame_interpolation_fps(uint32_t targetFps) noexcept; + +// Feedback for the adaptive slot controller: whether the producer met its +// retrace boundary for the frame that just presented. +void report_producer_paced(bool paced) noexcept; + +void begin_frame_interpolation() noexcept; +void finalize_frame_interpolation() noexcept; +// Fills the observability snapshot behind aurora_get_frame_interpolation_diagnostics. +void get_frame_interpolation_diagnostics(AuroraFrameInterpolationDiagnostics& diagnostics) noexcept; +bool has_interpolated_frame() noexcept; +uint32_t interpolated_frame_count() noexcept; +void mark_frame_interpolation_replay_unsafe() noexcept; +// Drops the interpolation tasks staged into the currently mapped uniform range. Required of +// anything that unmaps or rotates that range before the frame is finalized. +void drop_pending_frame_interpolation_uniforms() noexcept; +bool frame_interpolation_replay_safe() noexcept; + +// Records one perspective draw and maps its intermediate uniform copies, returning the mapped +// range per slot (empty when the draw has no counterpart). Called by build_uniform. +std::array record_interpolation_draw( + const FrameInterpolationDrawIdentity& identity, const Mat4x4& projection, + uint16_t usedPnMtxMask, const InterpolatedUniformLayout& uniformLayout) noexcept; + +// Folds a merged draw back into the snapshot of the draw it joined. aurora renders merged +// primitives through the first one's uniform block, so without this the merged-in bones tear. +void extend_interpolation_draw(uint16_t usedPnMtxMask) noexcept; + +bool interpolate_transform(const Mat3x4& previous, const Mat3x4& current, + float weight, Mat3x4& output) noexcept; +bool interpolate_transform_midpoint(const Mat3x4& previous, const Mat3x4& current, + Mat3x4& output) noexcept; +// Matrix palettes are already-composed skinning transforms, so interpolating their coefficients +// keeps shared boundaries intact. Ordinary one-matrix draws keep the rigid TRS path above. +bool interpolate_indexed_transform(const Mat3x4& previous, + const Mat3x4& current, float weight, + Mat3x4& output) noexcept; +float transform_match_distance_squared(const Mat3x4& previous, + const Mat3x4& current) noexcept; +} // namespace aurora::gx diff --git a/aurora-main/lib/gx/gx.cpp b/aurora-main/lib/gx/gx.cpp new file mode 100644 index 0000000..bf9d696 --- /dev/null +++ b/aurora-main/lib/gx/gx.cpp @@ -0,0 +1,2149 @@ +#include "gx.hpp" + +#include "pipeline.hpp" +#include "texture_bind_group_cache_key.hpp" +#include "../dolphin/vi/vi_internal.hpp" +#include "../webgpu/gpu.hpp" +#include "../internal.hpp" +#include "../gfx/common.hpp" +#include "../gfx/tex_palette_conv.hpp" +#include "../gfx/texture.hpp" +#include "../gfx/texture_convert.hpp" +#include "../gfx/texture_replacement.hpp" +#include "gx_fmt.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static aurora::Module Log("aurora::gx"); + +namespace aurora::gx { +using webgpu::g_device; +using webgpu::g_graphicsConfig; + +namespace { + + + + + + +} // namespace + +static GXState make_default_gx_state() { + GXState state{}; + for (size_t i = 0; i < state.tcgs.size(); ++i) { + state.tcgs[i].src = static_cast(GX_TG_TEX0 + i); + } + return state; +} + +GXState g_gxState = make_default_gx_state(); + +static wgpu::Sampler sEmptySampler; +static wgpu::Texture sEmptyTexture; +static wgpu::TextureView sEmptyTextureView; +static std::mutex sBindGroupLayoutMutex; +static absl::flat_hash_map sUniformBindGroupLayouts; +static absl::flat_hash_map> sTextureBindGroupLayouts; +static wgpu::BindGroupLayout sTextureBindGroupLayout; +static wgpu::BindGroupLayout sSamplerBindGroupLayout; +static wgpu::PipelineLayout sPipelineLayout; +wgpu::BindGroup g_emptyTextureBindGroup; +static u64 s_copyTextureStateRevision = 1; +static u64 s_staticTextureCacheValidationRevision = 1; + +// GXInvalidateTexAll means "the guest may have rewritten texture memory, so re-check anything cached from it before trusting it again". +static u64 s_textureCacheStamps = 0; + +// Marks a cache artifact as reusable at the current validation revision. +static inline void note_texture_cache_stamp() noexcept { ++s_textureCacheStamps; } + +static std::array s_lastNoCopyResolveRevision{}; +static void mark_copy_texture_cache_changed() noexcept; +static size_t static_texture_source_cache_size() noexcept; +static size_t static_palette_texture_source_cache_size() noexcept; +static size_t texture_object_cache_size() noexcept; + +struct TextureResolveIdentity { + const void* data = nullptr; + u32 mode0 = 0; + u32 mode1 = 0; + u32 image0 = 0; + u32 image3 = 0; + u32 width = 0; + u32 height = 0; + u32 format = 0; + GXTlut tlut = GX_TLUT0; + u32 texObjId = 0; + u32 texDataVersion = 0; + u8 flags = 0; + + bool operator==(const TextureResolveIdentity& rhs) const = default; + template + friend H AbslHashValue(H h, const TextureResolveIdentity& identity) { + return H::combine(std::move(h), identity.data, identity.mode0, identity.mode1, identity.image0, + identity.image3, identity.width, identity.height, identity.format, identity.tlut, + identity.texObjId, identity.texDataVersion, identity.flags); + } +}; +static std::array s_lastTextureResolveIdentity{}; +static std::array s_lastTextureResolveIdentityValid{}; + +struct TextureResolveIdentityCacheEntry { + bool valid = false; + size_t hash = 0; + u64 copyTextureRevision = 0; + // Revision this binding was last proved good at. + u64 validationRevision = 0; + TextureResolveIdentity identity{}; + gfx::TextureBind binding{}; +}; + +// ponytail: A fixed direct-mapped cache avoids allocator/hash-table overhead. +static std::array s_textureResolveIdentityCache{}; + +static TextureResolveIdentity make_texture_resolve_identity(const GXTexObj_& obj) noexcept { + return { + .data = obj.data, + .mode0 = obj.mode0, + .mode1 = obj.mode1, + .image0 = obj.image0, + .image3 = obj.image3, + .width = obj.width(), + .height = obj.height(), + .format = obj.format(), + .tlut = obj.tlut, + .texObjId = obj.texObjId, + .texDataVersion = obj.texDataVersion, + .flags = obj.flags, + }; +} + +namespace { +struct DynamicPaletteKey { + const void* sourceIdentity = nullptr; + u32 width = 0; + u32 height = 0; + u32 format = 0; + + bool operator==(const DynamicPaletteKey& rhs) const = default; + template + friend H AbslHashValue(H h, const DynamicPaletteKey& key) { + return H::combine(std::move(h), key.sourceIdentity, key.width, key.height, key.format); + } +}; + +struct DynamicPaletteEntry { + gfx::TextureHandle handle; + u32 sourceRevision = 0; + u32 tlutDataVersion = 0; +}; + +struct CachedTextureEntry { + gfx::TextureHandle handle; + u32 texDataVersion = 0; + u32 tlutObjId = 0; + u32 tlutDataVersion = 0; + // Revision the underlying source bytes were last proved good at. + u64 validationRevision = 0; +}; + +struct StaticTextureKey { + const void* data = nullptr; + u32 width = 0; + u32 height = 0; + u32 mips = 0; + u32 format = 0; + u32 texDataVersion = 0; + + bool operator==(const StaticTextureKey& rhs) const = default; + template + friend H AbslHashValue(H h, const StaticTextureKey& key) { + return H::combine(std::move(h), key.data, key.width, key.height, key.mips, key.format, key.texDataVersion); + } +}; + +static std::array s_lastStaticSourceResolveKey{}; +static std::array s_lastStaticSourceResolveKeyValid{}; +static std::array s_lastStaticSourceNoCopyRevision{}; + +struct StaticPaletteTextureKey { + StaticTextureKey texture; + const void* tlutData = nullptr; + u32 tlutFormat = 0; + u32 tlutEntries = 0; + u32 tlutDataVersion = 0; + + bool operator==(const StaticPaletteTextureKey& rhs) const = default; + template + friend H AbslHashValue(H h, const StaticPaletteTextureKey& key) { + return H::combine(std::move(h), key.texture, key.tlutData, key.tlutFormat, key.tlutEntries, key.tlutDataVersion); + } +}; + +// Revalidation used to keep a full byte-for-byte copy of every cached source and memcmp against it. +struct StaticTextureSourceEntry { + gfx::TextureHandle handle; + u64 digest = 0; + u32 sourceSize = 0; + u64 validationRevision = 0; + u64 sourceGeneration = kGuestWriteUntracked; +}; + +struct StaticPaletteTextureSourceEntry { + gfx::TextureHandle handle; + u64 textureDigest = 0; + u64 tlutDigest = 0; + u32 textureSize = 0; + u32 tlutSize = 0; + u64 validationRevision = 0; + u64 textureGeneration = kGuestWriteUntracked; + u64 tlutGeneration = kGuestWriteUntracked; +}; + +struct CachedTlutTextureEntry { + gfx::TextureHandle handle; + u32 tlutDataVersion = 0; + u64 digest = 0; + u64 validationRevision = 0; + u64 generation = kGuestWriteUntracked; +}; + +struct TlutObjectCache { + CachedTlutTextureEntry tlutTexture; + absl::flat_hash_map dynamicPaletteTextures; + absl::flat_hash_set staticTextureUsers; +}; + +absl::flat_hash_map s_textureObjectCaches; +absl::flat_hash_map s_tlutObjectCaches; +absl::flat_hash_map s_staticTextureSourceCache; +absl::flat_hash_map s_staticPaletteTextureSourceCache; +absl::flat_hash_map s_texObjSourceKeys; +absl::flat_hash_map s_texObjPaletteSourceKeys; + +// A repeated bind of the same texture object re-derives the same source key and re-stores it into the map above on every resolve. +template +struct TexObjSourceKeyMemo { + struct Slot { + u32 texObjId = 0; + Key key{}; + }; + std::array slots{}; + + [[nodiscard]] bool matches(u32 texObjId, const Key& key) const { + const auto& slot = slots[texObjId & (slots.size() - 1)]; + return slot.texObjId == texObjId && slot.key == key; + } + void store(u32 texObjId, const Key& key) { + auto& slot = slots[texObjId & (slots.size() - 1)]; + slot.texObjId = texObjId; + slot.key = key; + } + void forget(u32 texObjId) { + auto& slot = slots[texObjId & (slots.size() - 1)]; + if (slot.texObjId == texObjId) { + slot.texObjId = 0; + } + } + void reset() { + for (auto& slot : slots) { + slot.texObjId = 0; + } + } +}; + +TexObjSourceKeyMemo s_texObjSourceKeyMemo; +TexObjSourceKeyMemo s_texObjPaletteSourceKeyMemo; + +void record_tex_obj_source_key(u32 texObjId, const StaticTextureKey& key) noexcept { + if (s_texObjSourceKeyMemo.matches(texObjId, key)) { + return; + } + s_texObjSourceKeys[texObjId] = key; + s_texObjSourceKeyMemo.store(texObjId, key); +} + +void record_tex_obj_palette_source_key(u32 texObjId, const StaticPaletteTextureKey& key) noexcept { + if (s_texObjPaletteSourceKeyMemo.matches(texObjId, key)) { + return; + } + s_texObjPaletteSourceKeys[texObjId] = key; + s_texObjPaletteSourceKeyMemo.store(texObjId, key); +} + +} // namespace + +static size_t static_texture_source_cache_size() noexcept { return s_staticTextureSourceCache.size(); } +static size_t static_palette_texture_source_cache_size() noexcept { return s_staticPaletteTextureSourceCache.size(); } +static size_t texture_object_cache_size() noexcept { return s_textureObjectCaches.size(); } + +namespace { + +struct StaticSourceFrontCacheEntry { + bool valid = false; + size_t hash = 0; + u64 validationRevision = 0; + StaticTextureKey key{}; + gfx::TextureHandle handle; +}; + +static std::array s_staticSourceFrontCache; + +static gfx::TextureHandle lookup_static_source_front_cache(const StaticTextureKey& key, size_t hash) noexcept { + const auto& entry = s_staticSourceFrontCache[hash & (s_staticSourceFrontCache.size() - 1)]; + if (entry.valid && entry.validationRevision == s_staticTextureCacheValidationRevision && entry.hash == hash && + entry.key == key) { + return entry.handle; + } + return {}; +} + +static void store_static_source_front_cache(const StaticTextureKey& key, size_t hash, + const gfx::TextureHandle& handle) noexcept { + auto& entry = s_staticSourceFrontCache[hash & (s_staticSourceFrontCache.size() - 1)]; + entry.valid = true; + entry.hash = hash; + entry.validationRevision = s_staticTextureCacheValidationRevision; + entry.key = key; + entry.handle = handle; + note_texture_cache_stamp(); +} + +static void clear_static_source_front_cache() noexcept { + for (auto& entry : s_staticSourceFrontCache) { + entry.valid = false; + entry.handle.reset(); + } +} + +static u32 static_texture_data_size(const StaticTextureKey& key) noexcept { + if (key.data == nullptr || key.width == 0 || key.height == 0 || key.mips == 0) { + return 0; + } + return GXGetTexBufferSize(static_cast(key.width), static_cast(key.height), key.format, + key.mips > 1 ? GX_TRUE : GX_FALSE, static_cast(key.mips - 1)); +} + +static u64 digest_source_bytes(const void* data, size_t size) noexcept { + if (data == nullptr || size == 0) { + return 0; + } + return static_cast(xxh3_hash_s(data, size)); +} + +static bool source_matches_digest(const void* data, size_t size, u64 digest, u32 expectedSize) noexcept { + if (data == nullptr || size != expectedSize) { + return false; + } + return digest_source_bytes(data, size) == digest; +} + +// A source whose generation still matches the one recorded beside its digest cannot have been written since, so the digest is skipped. +static bool validate_static_texture_source_entry(const StaticTextureKey& key, + StaticTextureSourceEntry& entry) noexcept { + if (entry.validationRevision == s_staticTextureCacheValidationRevision) { + return true; + } + const u32 dataSize = static_texture_data_size(key); + const u64 generation = guest_write_generation(key.data, dataSize); + if (dataSize == entry.sourceSize && guest_write_generation_matches(entry.sourceGeneration, generation)) { + entry.validationRevision = s_staticTextureCacheValidationRevision; + note_texture_cache_stamp(); + return true; + } + if (!source_matches_digest(key.data, dataSize, entry.digest, entry.sourceSize)) { + return false; + } + entry.sourceGeneration = generation; + entry.validationRevision = s_staticTextureCacheValidationRevision; + note_texture_cache_stamp(); + return true; +} + +static bool validate_static_palette_texture_source_entry(const StaticPaletteTextureKey& key, + StaticPaletteTextureSourceEntry& entry) noexcept { + if (entry.validationRevision == s_staticTextureCacheValidationRevision) { + return true; + } + const u32 textureDataSize = static_texture_data_size(key.texture); + const size_t tlutDataSize = static_cast(key.tlutEntries) * sizeof(u16); + const u64 textureGeneration = guest_write_generation(key.texture.data, textureDataSize); + const u64 tlutGeneration = guest_write_generation(key.tlutData, tlutDataSize); + const bool skippable = textureDataSize == entry.textureSize && tlutDataSize == entry.tlutSize && + guest_write_generation_matches(entry.textureGeneration, textureGeneration) && + guest_write_generation_matches(entry.tlutGeneration, tlutGeneration); + if (!skippable) { + const bool textureOk = + source_matches_digest(key.texture.data, textureDataSize, entry.textureDigest, entry.textureSize); + const bool tlutOk = source_matches_digest(key.tlutData, tlutDataSize, entry.tlutDigest, entry.tlutSize); + if (!textureOk || !tlutOk) { + return false; + } + entry.textureGeneration = textureGeneration; + entry.tlutGeneration = tlutGeneration; + } + entry.validationRevision = s_staticTextureCacheValidationRevision; + note_texture_cache_stamp(); + return true; +} + +static StaticTextureSourceEntry make_static_texture_source_entry(const StaticTextureKey& key, + const gfx::TextureHandle& handle) { + const u32 dataSize = static_texture_data_size(key); + const u64 generation = guest_write_generation(key.data, dataSize); + note_texture_cache_stamp(); + return { + .handle = handle, + .digest = digest_source_bytes(key.data, dataSize), + .sourceSize = dataSize, + .validationRevision = s_staticTextureCacheValidationRevision, + .sourceGeneration = generation, + }; +} + +static StaticPaletteTextureSourceEntry make_static_palette_texture_source_entry( + const StaticPaletteTextureKey& key, const gfx::TextureHandle& handle) { + const u32 textureDataSize = static_texture_data_size(key.texture); + const size_t tlutDataSize = static_cast(key.tlutEntries) * sizeof(u16); + const u64 textureGeneration = guest_write_generation(key.texture.data, textureDataSize); + const u64 tlutGeneration = guest_write_generation(key.tlutData, tlutDataSize); + note_texture_cache_stamp(); + return { + .handle = handle, + .textureDigest = digest_source_bytes(key.texture.data, textureDataSize), + .tlutDigest = digest_source_bytes(key.tlutData, tlutDataSize), + .textureSize = textureDataSize, + .tlutSize = static_cast(tlutDataSize), + .validationRevision = s_staticTextureCacheValidationRevision, + .textureGeneration = textureGeneration, + .tlutGeneration = tlutGeneration, + }; +} + +DynamicPaletteKey make_dynamic_palette_key(const GXTexObj_& obj, const GXState::CopyTextureRef& source) { + return { + .sourceIdentity = source.handle.get(), + .width = obj.width(), + .height = obj.height(), + .format = obj.format(), + }; +} + +StaticTextureKey make_static_texture_key(const GXTexObj_& obj) { + return { + .data = obj.data, + .width = obj.width(), + .height = obj.height(), + .mips = obj.mip_count(), + .format = obj.format(), + .texDataVersion = obj.texDataVersion, + }; +} + +StaticPaletteTextureKey make_static_palette_texture_key(const GXTexObj_& obj, const GXTlutObj_& tlut) { + return { + .texture = make_static_texture_key(obj), + .tlutData = tlut.data, + .tlutFormat = static_cast(tlut.format), + .tlutEntries = tlut.numEntries, + .tlutDataVersion = tlut.tlutDataVersion, + }; +} + +bool can_cache_static_texture_upload(const GXTexObj_& obj) noexcept { + // THP movie frames are decoded into reused guest buffers and uploaded as YUVA. + return obj.format() != GX_CTF_YUVA8; +} + +void clear_texture_dependency(u32 texObjId, u32 tlutObjId) { + if (texObjId == 0 || tlutObjId == 0) { + return; + } + if (auto it = s_tlutObjectCaches.find(tlutObjId); it != s_tlutObjectCaches.end()) { + it->second.staticTextureUsers.erase(texObjId); + if (!it->second.tlutTexture.handle && it->second.dynamicPaletteTextures.empty() && + it->second.staticTextureUsers.empty()) { + s_tlutObjectCaches.erase(it); + } + } +} + +void store_cached_texture(const GXTexObj_& obj, gfx::TextureHandle handle, u32 tlutObjId = 0, u32 tlutDataVersion = 0) { + if (obj.texObjId == 0) { + return; + } + + auto& entry = s_textureObjectCaches[obj.texObjId]; + if (entry.tlutObjId != tlutObjId) { + clear_texture_dependency(obj.texObjId, entry.tlutObjId); + } + + entry.handle = std::move(handle); + entry.texDataVersion = obj.texDataVersion; + entry.tlutObjId = tlutObjId; + entry.tlutDataVersion = tlutDataVersion; + entry.validationRevision = s_staticTextureCacheValidationRevision; + note_texture_cache_stamp(); + + if (tlutObjId != 0) { + s_tlutObjectCaches[tlutObjId].staticTextureUsers.insert(obj.texObjId); + } +} + +gfx::TextureHandle get_tlut_texture(const GXTlutObj_& tlut) { + const size_t tlutSize = static_cast(tlut.numEntries) * sizeof(u16); + if (tlut.tlutObjId != 0) { + auto& cache = s_tlutObjectCaches[tlut.tlutObjId]; + auto& cached = cache.tlutTexture; + if (cached.handle && cached.tlutDataVersion == tlut.tlutDataVersion) { + if (cached.validationRevision == s_staticTextureCacheValidationRevision) { + return cached.handle; + } + // GXInvalidateTexAll: the guest may have rewritten this palette in place without re-running GXInitTlutObj, which is the only thing that moves tlutDataVersion. + bool stillValid = true; + if (tlut.data != nullptr) { + const u64 generation = guest_write_generation(tlut.data, tlutSize); + if (!guest_write_generation_matches(cached.generation, generation)) { + stillValid = digest_source_bytes(tlut.data, tlutSize) == cached.digest; + cached.generation = generation; + } + } + if (stillValid) { + cached.validationRevision = s_staticTextureCacheValidationRevision; + note_texture_cache_stamp(); + return cached.handle; + } + } + cache.dynamicPaletteTextures.clear(); + for (const u32 texObjId : cache.staticTextureUsers) { + s_textureObjectCaches.erase(texObjId); + } + cache.staticTextureUsers.clear(); + } + + const auto handle = + gfx::new_static_texture_2d(tlut.numEntries, 1, 1, gfx::tlut_texture_format(tlut.format), + {static_cast(tlut.data), tlutSize}, true, "Loaded TLUT"); + if (tlut.tlutObjId != 0) { + auto& cache = s_tlutObjectCaches[tlut.tlutObjId]; + const u64 generation = guest_write_generation(tlut.data, tlutSize); + cache.tlutTexture.handle = handle; + cache.tlutTexture.tlutDataVersion = tlut.tlutDataVersion; + cache.tlutTexture.digest = digest_source_bytes(tlut.data, tlutSize); + cache.tlutTexture.generation = generation; + cache.tlutTexture.validationRevision = s_staticTextureCacheValidationRevision; + note_texture_cache_stamp(); + } + return handle; +} + +gfx::TextureHandle resolve_static_texture(const GXTexObj_& obj) { + ZoneScoped; + const bool canCacheUpload = !obj.no_cache() && can_cache_static_texture_upload(obj); + + if (canCacheUpload && obj.texObjId != 0) { + if (const auto it = s_textureObjectCaches.find(obj.texObjId); it != s_textureObjectCaches.end()) { + const auto& entry = it->second; + if (entry.handle && entry.texDataVersion == obj.texDataVersion && entry.tlutObjId == 0 && + entry.validationRevision == s_staticTextureCacheValidationRevision) { + return entry.handle; + } + } + } + + const bool canUseSourceCache = canCacheUpload && obj.data != nullptr; + const StaticTextureKey sourceKey = canUseSourceCache ? make_static_texture_key(obj) : StaticTextureKey{}; + const size_t sourceHash = canUseSourceCache ? absl::Hash{}(sourceKey) : 0; + if (canUseSourceCache) { + if (auto handle = lookup_static_source_front_cache(sourceKey, sourceHash)) { + if (obj.texObjId != 0) { + record_tex_obj_source_key(obj.texObjId, sourceKey); + } + return handle; + } + if (auto it = s_staticTextureSourceCache.find(sourceKey); it != s_staticTextureSourceCache.end()) { + if (!validate_static_texture_source_entry(sourceKey, it->second)) { + s_staticTextureSourceCache.erase(it); + clear_static_source_front_cache(); + } else { + const auto& handle = it->second.handle; + if (obj.texObjId != 0) { + record_tex_obj_source_key(obj.texObjId, sourceKey); + } + store_static_source_front_cache(sourceKey, sourceHash, handle); + return handle; + } + } + } + + gfx::TextureHandle handle; + if (const auto replacement = gfx::texture_replacement::find_replacement(obj); replacement.has_value()) { + handle = *replacement; + } else { +#if DEBUG + const auto name = gfx::texture_replacement::build_texture_replacement_name(obj); + const auto nameStr = name.c_str(); +#else + const auto nameStr = "GX Static Texture"; +#endif + const u32 dataSize = GXGetTexBufferSize(static_cast(obj.width()), static_cast(obj.height()), + obj.format(), obj.has_mips(), + obj.has_mips() ? static_cast(obj.mip_count() - 1) : 0); + handle = gfx::new_static_texture_2d(obj.width(), obj.height(), obj.mip_count(), obj.format(), + {static_cast(obj.data), dataSize}, false, nameStr); + } + if (canCacheUpload) { + store_cached_texture(obj, handle); + if (canUseSourceCache) { + if (s_staticTextureSourceCache.size() >= 512) { + absl::erase_if(s_staticTextureSourceCache, [](const auto& item) { return item.second.handle.use_count() <= 1; }); + clear_static_source_front_cache(); + } + s_staticTextureSourceCache[sourceKey] = make_static_texture_source_entry(sourceKey, handle); + store_static_source_front_cache(sourceKey, sourceHash, handle); + if (obj.texObjId != 0) { + record_tex_obj_source_key(obj.texObjId, sourceKey); + } + } + } + return handle; +} + +gfx::TextureHandle resolve_static_palette_texture(const GXTexObj_& obj, const GXTlutObj_& tlut) { + ZoneScoped; + + if (obj.texObjId != 0) { + if (const auto it = s_textureObjectCaches.find(obj.texObjId); it != s_textureObjectCaches.end()) { + const auto& entry = it->second; + if (entry.handle && entry.texDataVersion == obj.texDataVersion && entry.tlutObjId == tlut.tlutObjId && + entry.tlutDataVersion == tlut.tlutDataVersion && + entry.validationRevision == s_staticTextureCacheValidationRevision) { + return entry.handle; + } + } + } + + const bool canUseSourceCache = !obj.no_cache() && !tlut.no_cache() && obj.data != nullptr && tlut.data != nullptr; + const StaticPaletteTextureKey sourceKey = + canUseSourceCache ? make_static_palette_texture_key(obj, tlut) : StaticPaletteTextureKey{}; + if (canUseSourceCache) { + if (auto it = s_staticPaletteTextureSourceCache.find(sourceKey); + it != s_staticPaletteTextureSourceCache.end()) { + if (!validate_static_palette_texture_source_entry(sourceKey, it->second)) { + s_staticPaletteTextureSourceCache.erase(it); + } else { + const auto& handle = it->second.handle; + if (obj.texObjId != 0) { + store_cached_texture(obj, handle, tlut.tlutObjId, tlut.tlutDataVersion); + record_tex_obj_palette_source_key(obj.texObjId, sourceKey); + } + return handle; + } + } + } + + gfx::TextureHandle handle; + if (const auto replacement = gfx::texture_replacement::find_replacement(obj); replacement.has_value()) { + handle = *replacement; + } else { + const u32 dataSize = GXGetTexBufferSize(static_cast(obj.width()), static_cast(obj.height()), + obj.format(), obj.has_mips(), + obj.has_mips() ? static_cast(obj.mip_count() - 1) : 0); + auto converted = gfx::convert_texture_palette( + obj.format(), obj.width(), obj.height(), obj.mip_count(), {static_cast(obj.data), dataSize}, + tlut.format, tlut.numEntries, {static_cast(tlut.data), static_cast(tlut.numEntries) * 2}); + if (converted.data.empty()) { + return {}; + } + handle = + gfx::new_static_texture_2d(obj.width(), obj.height(), obj.mip_count(), GX_TF_RGBA8_PC, + {converted.data.data(), converted.data.size()}, false, "GX Static Palette Texture"); + handle->hasArbitraryMips = converted.hasArbitraryMips; + } + if (!obj.no_cache() && !tlut.no_cache()) { + store_cached_texture(obj, handle, tlut.tlutObjId, tlut.tlutDataVersion); + if (canUseSourceCache) { + if (s_staticPaletteTextureSourceCache.size() >= 256) { + absl::erase_if(s_staticPaletteTextureSourceCache, [](const auto& item) { return item.second.handle.use_count() <= 1; }); + } + s_staticPaletteTextureSourceCache[sourceKey] = make_static_palette_texture_source_entry(sourceKey, handle); + if (obj.texObjId != 0) { + record_tex_obj_palette_source_key(obj.texObjId, sourceKey); + } + } + } + return handle; +} + +gfx::TextureHandle resolve_dynamic_palette_texture(const GXTexObj_& obj, const GXState::CopyTextureRef& source, + const GXTlutObj_& tlut) { + ZoneScoped; + + const auto tlutHandle = get_tlut_texture(tlut); + auto& tlutCache = s_tlutObjectCaches[tlut.tlutObjId]; + if (tlutCache.dynamicPaletteTextures.size() >= 32) { + absl::erase_if(tlutCache.dynamicPaletteTextures, [](const auto& item) { return item.second.handle.use_count() <= 1; }); + } + auto& entry = tlutCache.dynamicPaletteTextures[make_dynamic_palette_key(obj, source)]; + if (!entry.handle) { + // Use source size instead of target (logical) size + entry.handle = gfx::new_conv_texture(source.handle->size.width, source.handle->size.height, GX_TF_RGBA8, + "GX Dynamic Palette Texture"); + } + if (entry.sourceRevision != source.revision || entry.tlutDataVersion != tlut.tlutDataVersion) { + gfx::queue_palette_conv({ + .variant = obj.format() == GX_TF_C4 ? gfx::tex_palette_conv::Variant::FromFloat4 + : gfx::tex_palette_conv::Variant::FromFloat8, + .src = source.handle, + .dst = entry.handle, + .tlut = tlutHandle, + }); + entry.sourceRevision = source.revision; + entry.tlutDataVersion = tlut.tlutDataVersion; + } + return entry.handle; +} + +u32 resolved_format_for_handle(const gfx::TextureHandle& handle) { + if (!handle) { + return GX_TF_RGBA8; + } + if (handle->gxFormat != gfx::InvalidTextureFormat) { + return handle->gxFormat; + } + return GX_TF_RGBA8_PC; +} +} // namespace + +Vec2 logical_fb_size() noexcept { + return gfx::is_offscreen() ? gfx::get_render_target_size() : vi::configured_fb_size(); +} + +namespace { +struct ScissorRange { + s32 offset = 0; + s32 start = 0; + s32 end = 0; +}; + +struct ScissorRect { + gfx::ClipRect rect{}; + s32 xOff = 0; + s32 yOff = 0; +}; + +std::array compute_scissor_ranges(s32 start, s32 end, s32 offset, s32 efbDim, u32& count) noexcept { + std::array ranges{}; + count = 0; + for (s32 extraOff = -4096; extraOff <= 4096; extraOff += 1024) { + const s32 newOff = offset + extraOff; + const s32 newStart = std::clamp(start - newOff, 0, efbDim); + const s32 newEnd = std::clamp(end - newOff + 1, 0, efbDim); + if (newStart < newEnd && count < ranges.size()) { + ranges[count++] = { + .offset = newOff, + .start = newStart, + .end = newEnd, + }; + } + } + return ranges; +} + +int scissor_viewport_area(const ScissorRect& rect, const gfx::Viewport& viewport) noexcept { + const float viewportLeft = std::min(viewport.left, viewport.left + viewport.width); + const float viewportRight = std::max(viewport.left, viewport.left + viewport.width); + const float viewportTop = std::min(viewport.top, viewport.top + viewport.height); + const float viewportBottom = std::max(viewport.top, viewport.top + viewport.height); + + const float left = std::clamp(static_cast(rect.rect.x + rect.xOff), viewportLeft, viewportRight); + const float right = + std::clamp(static_cast(rect.rect.x + rect.rect.width + rect.xOff), viewportLeft, viewportRight); + const float top = std::clamp(static_cast(rect.rect.y + rect.yOff), viewportTop, viewportBottom); + const float bottom = + std::clamp(static_cast(rect.rect.y + rect.rect.height + rect.yOff), viewportTop, viewportBottom); + + return static_cast(std::max(right - left, 0.0f) * std::max(bottom - top, 0.0f)); +} + +int scissor_area(const ScissorRect& rect) noexcept { + return rect.rect.width * rect.rect.height; +} + +ScissorRect best_scissor_rect(const gfx::ClipRect& logicalScissor, const gfx::Viewport& logicalViewport) noexcept { + if (logicalScissor.width <= 0 || logicalScissor.height <= 0) { + return { + .rect = {1000, 1000, 1, 1}, + .xOff = 0, + .yOff = 0, + }; + } + + const auto [logicalFbWidth, logicalFbHeight] = logical_fb_size(); + const s32 efbWidth = static_cast(logicalFbWidth); + const s32 efbHeight = static_cast(logicalFbHeight); + const s32 left = logicalScissor.x; + const s32 right = logicalScissor.x + logicalScissor.width - 1; + const s32 top = logicalScissor.y; + const s32 bottom = logicalScissor.y + logicalScissor.height - 1; + + u32 xCount = 0; + u32 yCount = 0; + const auto xRanges = compute_scissor_ranges(left, right, g_gxState.scissorOffsetX, efbWidth, xCount); + const auto yRanges = compute_scissor_ranges(top, bottom, g_gxState.scissorOffsetY, efbHeight, yCount); + if (xCount == 0 || yCount == 0) { + return { + .rect = {1000, 1000, 1, 1}, + .xOff = 0, + .yOff = 0, + }; + } + + std::optional best; + for (u32 x = 0; x < xCount; ++x) { + for (u32 y = 0; y < yCount; ++y) { + const ScissorRect candidate{ + .rect = + { + .x = xRanges[x].start, + .y = yRanges[y].start, + .width = xRanges[x].end - xRanges[x].start, + .height = yRanges[y].end - yRanges[y].start, + }, + .xOff = xRanges[x].offset, + .yOff = yRanges[y].offset, + }; + if (!best) { + best = candidate; + continue; + } + + const int candidateViewportArea = scissor_viewport_area(candidate, logicalViewport); + const int bestViewportArea = scissor_viewport_area(*best, logicalViewport); + if (candidateViewportArea > bestViewportArea || + (candidateViewportArea == bestViewportArea && scissor_area(candidate) > scissor_area(*best))) { + best = candidate; + } + } + } + + return *best; +} + +float logical_to_target_x(float value) noexcept { + const auto [logicalFbWidth, logicalFbHeight] = logical_fb_size(); + const auto [targetWidth, targetHeight] = gfx::get_render_target_size(); + if (logicalFbWidth == 0 || logicalFbHeight == 0 || targetWidth == 0 || targetHeight == 0) { + return value; + } + return value * static_cast(targetWidth) / static_cast(logicalFbWidth); +} + +float logical_to_target_y(float value) noexcept { + const auto [logicalFbWidth, logicalFbHeight] = logical_fb_size(); + const auto [targetWidth, targetHeight] = gfx::get_render_target_size(); + if (logicalFbWidth == 0 || logicalFbHeight == 0 || targetWidth == 0 || targetHeight == 0) { + return value; + } + return value * static_cast(targetHeight) / static_cast(logicalFbHeight); +} + +gfx::ClipRect scale_logical_scissor_rect(const gfx::ClipRect& logicalScissor) noexcept { + if (g_gxState.viewportPolicy == AURORA_VIEWPORT_NATIVE) { + return logicalScissor; + } + + const auto [targetWidth, targetHeight] = gfx::get_render_target_size(); + const float left = logical_to_target_x(static_cast(logicalScissor.x)); + const float top = logical_to_target_y(static_cast(logicalScissor.y)); + const float right = logical_to_target_x(static_cast(logicalScissor.x + logicalScissor.width)); + const float bottom = logical_to_target_y(static_cast(logicalScissor.y + logicalScissor.height)); + + const auto mappedLeft = std::clamp(static_cast(std::floor(left)), 0, static_cast(targetWidth)); + const auto mappedTop = std::clamp(static_cast(std::floor(top)), 0, static_cast(targetHeight)); + const auto mappedRight = + std::clamp(static_cast(std::ceil(right)), mappedLeft, static_cast(targetWidth)); + const auto mappedBottom = + std::clamp(static_cast(std::ceil(bottom)), mappedTop, static_cast(targetHeight)); + + return { + .x = mappedLeft, + .y = mappedTop, + .width = mappedRight - mappedLeft, + .height = mappedBottom - mappedTop, + }; +} + +} // namespace + +MappedRenderState map_logical_render_state() noexcept { + if (g_gxState.viewportPolicy == AURORA_VIEWPORT_NATIVE) { + return { + .viewport = g_gxState.logicalViewport, + .scissor = g_gxState.logicalScissor, + }; + } + + const auto bestScissor = best_scissor_rect(g_gxState.logicalScissor, g_gxState.logicalViewport); + const gfx::Viewport shiftedViewport{ + .left = g_gxState.logicalViewport.left - static_cast(bestScissor.xOff), + .top = g_gxState.logicalViewport.top - static_cast(bestScissor.yOff), + .width = g_gxState.logicalViewport.width, + .height = g_gxState.logicalViewport.height, + .znear = g_gxState.logicalViewport.znear, + .zfar = g_gxState.logicalViewport.zfar, + }; + + return { + .viewport = map_logical_viewport(shiftedViewport), + .scissor = scale_logical_scissor_rect(bestScissor.rect), + }; +} + +namespace { +void apply_logical_render_state() noexcept { + const auto mapped = map_logical_render_state(); + set_render_viewport(mapped.viewport); + set_render_scissor(mapped.scissor); +} +} // namespace + +gfx::Viewport map_logical_viewport(const gfx::Viewport& logicalViewport) noexcept { + if (g_gxState.viewportPolicy == AURORA_VIEWPORT_NATIVE) { + return logicalViewport; + } + + const auto [logicalFbWidth, logicalFbHeight] = logical_fb_size(); + const auto [targetWidth, targetHeight] = gfx::get_render_target_size(); + if (logicalFbWidth == 0 || logicalFbHeight == 0 || targetWidth == 0 || targetHeight == 0) { + return logicalViewport; + } + + const float scaleX = static_cast(targetWidth) / static_cast(logicalFbWidth); + const float scaleY = static_cast(targetHeight) / static_cast(logicalFbHeight); + return { + .left = logicalViewport.left * scaleX, + .top = logicalViewport.top * scaleY, + .width = logicalViewport.width * scaleX, + .height = logicalViewport.height * scaleY, + .znear = logicalViewport.znear, + .zfar = logicalViewport.zfar, + }; +} + +gfx::ClipRect map_logical_scissor(const gfx::ClipRect& logicalScissor) noexcept { + if (g_gxState.viewportPolicy == AURORA_VIEWPORT_NATIVE) { + return logicalScissor; + } + + const auto [logicalFbWidth, logicalFbHeight] = logical_fb_size(); + const auto [targetWidth, targetHeight] = gfx::get_render_target_size(); + if (logicalFbWidth == 0 || logicalFbHeight == 0 || targetWidth == 0 || targetHeight == 0) { + return logicalScissor; + } + + return scale_logical_scissor_rect(logicalScissor); +} + +void set_logical_viewport(const gfx::Viewport& viewport) noexcept { + const bool changed = viewport != g_gxState.logicalViewport; + g_gxState.logicalViewport = viewport; + g_gxState.stateDirty = g_gxState.stateDirty || changed; + apply_logical_render_state(); +} + +void set_render_viewport(const gfx::Viewport& viewport) noexcept { + if (viewport == g_gxState.renderViewport) { + return; + } + g_gxState.renderViewport = viewport; + g_gxState.stateDirty = true; + gfx::set_viewport(viewport); +} + +void set_logical_scissor(const gfx::ClipRect& scissor) noexcept { + g_gxState.logicalScissor = scissor; + apply_logical_render_state(); +} + +void set_render_scissor(const gfx::ClipRect& scissor) noexcept { + g_gxState.renderScissor = scissor; + gfx::set_scissor(scissor); +} + +const gfx::TextureBind& get_texture(GXTexMapID id) noexcept { return g_gxState.textures[static_cast(id)]; } + +void evict_texture_object(u32 texObjId) noexcept { + if (const auto it = s_textureObjectCaches.find(texObjId); it != s_textureObjectCaches.end()) { + clear_texture_dependency(texObjId, it->second.tlutObjId); + s_textureObjectCaches.erase(it); + } + if (const auto it = s_texObjSourceKeys.find(texObjId); it != s_texObjSourceKeys.end()) { + s_staticTextureSourceCache.erase(it->second); + s_texObjSourceKeys.erase(it); + s_texObjSourceKeyMemo.forget(texObjId); + clear_static_source_front_cache(); + } + if (const auto it = s_texObjPaletteSourceKeys.find(texObjId); it != s_texObjPaletteSourceKeys.end()) { + s_staticPaletteTextureSourceCache.erase(it->second); + s_texObjPaletteSourceKeys.erase(it); + s_texObjPaletteSourceKeyMemo.forget(texObjId); + } + // If there is a loaded slot with this ID, mark it as no_cache to avoid inserting it when it's resolved. + // This also handles the case where the texture was created, loaded, and immediately destroyed before we resolved it. + for (auto& obj : g_gxState.loadedTextures) { + if (obj.texObjId == texObjId) { + obj.set_no_cache(true); + } + } +} + +void evict_tlut_object(u32 tlutObjId) noexcept { + // Only the conversions that actually depend on this palette are dropped. + if (const auto it = s_tlutObjectCaches.find(tlutObjId); it != s_tlutObjectCaches.end()) { + for (const u32 texObjId : it->second.staticTextureUsers) { + s_textureObjectCaches.erase(texObjId); + if (const auto keyIt = s_texObjPaletteSourceKeys.find(texObjId); keyIt != s_texObjPaletteSourceKeys.end()) { + s_staticPaletteTextureSourceCache.erase(keyIt->second); + s_texObjPaletteSourceKeys.erase(keyIt); + s_texObjPaletteSourceKeyMemo.forget(texObjId); + } + } + s_tlutObjectCaches.erase(it); + } + // If there is a loaded slot with this ID, mark it as no_cache to avoid inserting it when it's resolved. + // This also handles the case where the texture was created, loaded, and immediately destroyed before we resolved it. + for (auto& obj : g_gxState.loadedTluts) { + if (obj.tlutObjId == tlutObjId) { + obj.set_no_cache(true); + } + } +} + +// GXInitTexObj mints a fresh object id per call and nothing ever emits DESTROY_TEXOBJ, so the texture object memo used to be bounded only by the fact that GXInvalidateTexAll dropped it six times a frame. +constexpr u64 kTextureCacheIdleRevisions = 128; +constexpr size_t kTextureObjectCacheSoftLimit = 1024; +constexpr size_t kTlutObjectCacheSoftLimit = 512; +constexpr size_t kTexObjSourceKeySoftLimit = 2048; + +static void prune_idle_texture_caches() noexcept { + const u64 revision = s_staticTextureCacheValidationRevision; + // An idle entry stays idle, so there is nothing to gain from sweeping on every bump (six a frame). + if (revision % (kTextureCacheIdleRevisions / 2) != 0) { + return; + } + if (s_textureObjectCaches.size() >= kTextureObjectCacheSoftLimit) { + std::vector> retired; + for (const auto& [texObjId, entry] : s_textureObjectCaches) { + if (revision - entry.validationRevision > kTextureCacheIdleRevisions) { + retired.emplace_back(texObjId, entry.tlutObjId); + } + } + for (const auto& [texObjId, tlutObjId] : retired) { + clear_texture_dependency(texObjId, tlutObjId); + s_textureObjectCaches.erase(texObjId); + } + } + + if (s_tlutObjectCaches.size() >= kTlutObjectCacheSoftLimit) { + // Only drop palettes nothing depends on: an entry with users still records which cached conversions have to be thrown away when its bytes change. + absl::erase_if(s_tlutObjectCaches, [revision](const auto& item) { + const auto& cache = item.second; + return cache.staticTextureUsers.empty() && cache.dynamicPaletteTextures.empty() && + (!cache.tlutTexture.handle || cache.tlutTexture.handle.use_count() <= 1) && + revision - cache.tlutTexture.validationRevision > kTextureCacheIdleRevisions; + }); + } + + // The source-key maps exist only so evict_texture_object can find the static source entry a texObjId last resolved to, and nothing else ever retires them -- with GXInitTexObj minting a fresh id per call they otherwise grow for the lifetime of the process. + if (s_texObjSourceKeys.size() >= kTexObjSourceKeySoftLimit) { + absl::erase_if(s_texObjSourceKeys, [](const auto& item) { return !s_textureObjectCaches.contains(item.first); }); + s_texObjSourceKeyMemo.reset(); + } + if (s_texObjPaletteSourceKeys.size() >= kTexObjSourceKeySoftLimit) { + absl::erase_if(s_texObjPaletteSourceKeys, + [](const auto& item) { return !s_textureObjectCaches.contains(item.first); }); + s_texObjPaletteSourceKeyMemo.reset(); + } +} + +void invalidate_static_texture_cache() noexcept { + // Nothing has been marked reusable since the last bump, so every entry in every tier is already stale at the current revision. + if (s_textureCacheStamps == 0) { + return; + } + + ++s_staticTextureCacheValidationRevision; + s_textureCacheStamps = 0; + if (s_staticTextureCacheValidationRevision == 0) { + // Unreachable with a 64-bit counter, but if it ever wraps, no stamped revision may survive into the reused revision. + s_staticTextureCacheValidationRevision = 1; + s_textureObjectCaches.clear(); + s_tlutObjectCaches.clear(); + s_staticTextureSourceCache.clear(); + s_staticPaletteTextureSourceCache.clear(); + s_texObjSourceKeys.clear(); + s_texObjPaletteSourceKeys.clear(); + s_texObjSourceKeyMemo.reset(); + s_texObjPaletteSourceKeyMemo.reset(); + clear_static_source_front_cache(); + for (auto& entry : s_textureResolveIdentityCache) { + entry.valid = false; + entry.binding.reset(); + } + } + + // Everything else is revision-gated: the texture object memo, the TLUT textures (re-proved by palette digest), the static source front cache and the resolve identity cache all miss on the new revision without being torn down, so the bump costs one increment instead of dropping GPU textures and rebuilding hash tables. + s_lastTextureResolveIdentityValid.fill(false); + s_lastNoCopyResolveRevision.fill(0); + s_lastStaticSourceResolveKeyValid.fill(false); + s_lastStaticSourceNoCopyRevision.fill(0); + prune_idle_texture_caches(); + g_gxState.stateDirty = true; +} + +void clear_copy_texture_cache() noexcept { + g_gxState.copyTextures.clear(); + g_gxState.copyTextureCache.clear(); + prune_copy_texture_pool(nullptr); + mark_copy_texture_cache_changed(); + clear_display_copy_cache(); + for (auto& [_, cache] : s_tlutObjectCaches) { + cache.dynamicPaletteTextures.clear(); + } +} + +void clear_display_copy_cache() noexcept { + g_gxState.displayCopyTexture.reset(); + g_gxState.displayCopyBindGroup = {}; + g_gxState.displayCopyWidth = 0; + g_gxState.displayCopyHeight = 0; +} + +void set_display_copy_present_source() noexcept { + if (!g_gxState.displayCopyTexture) { + return; + } + + g_gxState.displayCopyBindGroup = webgpu::create_copy_bind_group(g_gxState.displayCopyTexture->sampleTextureView, + webgpu::present_source().sampler); + webgpu::set_present_source_override(g_gxState.displayCopyBindGroup, g_gxState.displayCopyTexture->texture, + g_gxState.displayCopyTexture->size, g_gxState.displayCopyTexture->format); +} + +void evict_copy_texture(const void* dest) noexcept { + // Dynamic palette textures are keyed on the copy texture's raw TextureRef pointer; evicting the copy without them leaves entries that can match a recycled allocation at the same address and serve a stale conversion. + absl::flat_hash_set sourceIdentities; + bool changed = false; + if (const auto it = g_gxState.copyTextures.find(dest); it != g_gxState.copyTextures.end()) { + if (it->second.handle) { + sourceIdentities.insert(it->second.handle.get()); + } + g_gxState.copyTextures.erase(it); + changed = true; + } + for (auto it = g_gxState.copyTextureCache.begin(); it != g_gxState.copyTextureCache.end();) { + if (it->first.dest == dest) { + if (it->second.handle) { + sourceIdentities.insert(it->second.handle.get()); + } + g_gxState.copyTextureCache.erase(it++); + changed = true; + } else { + ++it; + } + } + if (!sourceIdentities.empty()) { + for (auto& [_, cache] : s_tlutObjectCaches) { + for (auto it = cache.dynamicPaletteTextures.begin(); it != cache.dynamicPaletteTextures.end();) { + if (sourceIdentities.contains(it->first.sourceIdentity)) { + cache.dynamicPaletteTextures.erase(it++); + } else { + ++it; + } + } + } + } + prune_copy_texture_pool(dest); + if (changed) { + mark_copy_texture_cache_changed(); + } +} + +static void mark_copy_texture_cache_changed() noexcept { + ++s_copyTextureStateRevision; + if (s_copyTextureStateRevision == 0) { + s_copyTextureStateRevision = 1; + s_lastNoCopyResolveRevision.fill(0); + } +} + +void mark_copy_texture_sampled(const void* copyDest, GXState::CopyTextureRef& copyRef) noexcept { + const auto frame = gfx::current_frame(); + copyRef.lastSampledFrame = frame; + copyRef.sampledThisFrame = true; + + if (auto it = g_gxState.copyTextures.find(copyDest); + it != g_gxState.copyTextures.end() && it->second.handle == copyRef.handle) { + it->second.lastSampledFrame = frame; + it->second.sampledThisFrame = true; + } + + const GXState::CopyTextureKey key{ + .dest = copyDest, + .width = copyRef.width, + .height = copyRef.height, + .format = copyRef.format, + }; + if (auto it = g_gxState.copyTextureCache.find(key); it != g_gxState.copyTextureCache.end() && + it->second.handle == copyRef.handle) { + it->second.lastSampledFrame = frame; + it->second.sampledThisFrame = true; + } + +} + +bool copy_ref_matches_texobj(const GXState::CopyTextureRef& copyRef, const GXTexObj_& obj) noexcept { + return copyRef && copyRef.width == obj.width() && copyRef.height == obj.height() && + copy_texture_format_compatible(copyRef.format, obj.format()); +} + +void notify_copy_texture_created() noexcept { + mark_copy_texture_cache_changed(); +} + + +GXState::CopyTextureRef* find_cached_copy_texture(const void* copyDest, const GXTexObj_& obj) noexcept { + const GXState::CopyTextureKey key{ + .dest = copyDest, + .width = obj.width(), + .height = obj.height(), + .format = static_cast(obj.format()), + }; + auto cached = g_gxState.copyTextureCache.find(key); + if (cached == g_gxState.copyTextureCache.end() || !cached->second) { + return nullptr; + } + mark_copy_texture_sampled(copyDest, cached->second); + return &cached->second; +} + +GXState::CopyTextureRef* find_copy_texture_for_texobj(const GXTexObj_& obj) noexcept { + if (obj.data == nullptr) { + return nullptr; + } + + if (auto* cached = find_cached_copy_texture(obj.data, obj)) { + return cached; + } + + const auto exact = g_gxState.copyTextures.find(obj.data); + if (exact != g_gxState.copyTextures.end() && copy_ref_matches_texobj(exact->second, obj)) { + mark_copy_texture_sampled(obj.data, exact->second); + return &exact->second; + } + // A guest allocation can be recycled while an old GPU-only EFB copy still exists. + return nullptr; +} + +void resolve_sampled_textures(const ShaderInfo& info) noexcept { + ZoneScoped; + + for (u32 i = 0; i < MaxTextures; ++i) { + if (!info.sampledTextures.test(i)) { + continue; + } + + GXTexObj_ obj = g_gxState.loadedTextures[i]; + auto& textureBind = g_gxState.textures[i]; + const TextureResolveIdentity identity = make_texture_resolve_identity(obj); + // obj is unchanged between here and the memo checks below, so the two former can_cache_static_texture_upload(obj) calls are one value. + const bool canCacheUpload = can_cache_static_texture_upload(obj); + const bool sameResolvedTexture = canCacheUpload && s_lastTextureResolveIdentityValid[i] && + s_lastTextureResolveIdentity[i] == identity; + // The primary per-texmap memo below consumes neither the source key nor its hash, so building them ahead of it wasted make_static_texture_key on the hottest path (same texture bound across consecutive draws). + if (sameResolvedTexture && s_lastNoCopyResolveRevision[i] == s_copyTextureStateRevision) { + continue; + } + const bool canUseStaticSourceKey = + !is_palette_format(obj.format()) && canCacheUpload && !obj.no_cache() && + obj.data != nullptr; + const StaticTextureKey staticSourceKey = + canUseStaticSourceKey ? make_static_texture_key(obj) : StaticTextureKey{}; + const size_t identityHash = canUseStaticSourceKey ? absl::Hash{}(identity) : 0; + if (canUseStaticSourceKey) { + const auto& entry = s_textureResolveIdentityCache[identityHash & (s_textureResolveIdentityCache.size() - 1)]; + if (entry.valid && entry.hash == identityHash && entry.copyTextureRevision == s_copyTextureStateRevision && + entry.validationRevision == s_staticTextureCacheValidationRevision && entry.identity == identity && + entry.binding) { + textureBind = entry.binding; + note_texture_cache_stamp(); + s_lastTextureResolveIdentity[i] = identity; + s_lastTextureResolveIdentityValid[i] = true; + s_lastNoCopyResolveRevision[i] = s_copyTextureStateRevision; + s_lastStaticSourceResolveKey[i] = staticSourceKey; + s_lastStaticSourceResolveKeyValid[i] = true; + s_lastStaticSourceNoCopyRevision[i] = s_copyTextureStateRevision; + continue; + } + } + if (canUseStaticSourceKey && textureBind.ref && s_lastStaticSourceResolveKeyValid[i] && + s_lastStaticSourceResolveKey[i] == staticSourceKey && + s_lastStaticSourceNoCopyRevision[i] == s_copyTextureStateRevision) { + obj.mFormat = resolved_format_for_handle(textureBind.ref); + textureBind.texObj = obj; + s_lastTextureResolveIdentity[i] = identity; + s_lastTextureResolveIdentityValid[i] = true; + s_lastNoCopyResolveRevision[i] = s_copyTextureStateRevision; + note_texture_cache_stamp(); + continue; + } + + GXState::CopyTextureRef* copyRef = find_copy_texture_for_texobj(obj); + if (sameResolvedTexture) { + if (copyRef == nullptr) { + s_lastNoCopyResolveRevision[i] = s_copyTextureStateRevision; + continue; + } + if (!is_palette_format(obj.format()) && textureBind.ref == copyRef->handle) { + s_lastNoCopyResolveRevision[i] = 0; + continue; + } + } + + gfx::TextureHandle handle; + if (is_palette_format(obj.format())) { + const auto tlutIdx = static_cast(obj.tlut); + if (tlutIdx < g_gxState.loadedTluts.size()) { + const auto& tlut = g_gxState.loadedTluts[tlutIdx]; + if (tlut.data != nullptr) { + if (copyRef != nullptr) { + handle = resolve_dynamic_palette_texture(obj, *copyRef, tlut); + } else if (obj.data != nullptr) { + handle = resolve_static_palette_texture(obj, tlut); + } + } + } + } else if (copyRef != nullptr) { + handle = copyRef->handle; + } else if (obj.data != nullptr) { + handle = resolve_static_texture(obj); + } + + obj.mFormat = resolved_format_for_handle(handle); + textureBind = gfx::TextureBind{obj, std::move(handle)}; + // The per-texmap memo below lets a later draw reuse this binding without touching guest memory again, including for no_cache textures that never reach any of the stamped caches above, so it has to count as a stamp. + note_texture_cache_stamp(); + s_lastTextureResolveIdentity[i] = identity; + s_lastTextureResolveIdentityValid[i] = true; + s_lastNoCopyResolveRevision[i] = (copyRef == nullptr) ? s_copyTextureStateRevision : 0; + s_lastStaticSourceResolveKeyValid[i] = canUseStaticSourceKey && copyRef == nullptr; + if (s_lastStaticSourceResolveKeyValid[i]) { + s_lastStaticSourceResolveKey[i] = staticSourceKey; + s_lastStaticSourceNoCopyRevision[i] = s_copyTextureStateRevision; + auto& entry = s_textureResolveIdentityCache[identityHash & (s_textureResolveIdentityCache.size() - 1)]; + entry.valid = true; + entry.hash = identityHash; + entry.copyTextureRevision = s_copyTextureStateRevision; + entry.validationRevision = s_staticTextureCacheValidationRevision; + entry.identity = identity; + entry.binding = textureBind; + note_texture_cache_stamp(); + } else { + s_lastStaticSourceNoCopyRevision[i] = 0; + } + } +} + +static inline wgpu::BlendFactor to_blend_factor(GXBlendFactor fac, bool isDst, bool alphaComponent) { + switch (fac) { + DEFAULT_FATAL("invalid blend factor {}", underlying(fac)); + case GX_BL_ZERO: + return wgpu::BlendFactor::Zero; + case GX_BL_ONE: + return wgpu::BlendFactor::One; + case GX_BL_SRCCLR: // + GX_BL_DSTCLR + if (alphaComponent) { + return isDst ? wgpu::BlendFactor::SrcAlpha : wgpu::BlendFactor::DstAlpha; + } + if (isDst) { + return wgpu::BlendFactor::Src; + } else { + return wgpu::BlendFactor::Dst; + } + case GX_BL_INVSRCCLR: // + GX_BL_INVDSTCLR + if (alphaComponent) { + return isDst ? wgpu::BlendFactor::OneMinusSrcAlpha : wgpu::BlendFactor::OneMinusDstAlpha; + } + if (isDst) { + return wgpu::BlendFactor::OneMinusSrc; + } else { + return wgpu::BlendFactor::OneMinusDst; + } + case GX_BL_SRCALPHA: + return wgpu::BlendFactor::SrcAlpha; + case GX_BL_INVSRCALPHA: + return wgpu::BlendFactor::OneMinusSrcAlpha; + case GX_BL_DSTALPHA: + return wgpu::BlendFactor::DstAlpha; + case GX_BL_INVDSTALPHA: + return wgpu::BlendFactor::OneMinusDstAlpha; + } +} + +static inline GXBlendFactor remove_dst_alpha_usage(GXBlendFactor fac) { + switch (fac) { + case GX_BL_DSTALPHA: + return GX_BL_ONE; + case GX_BL_INVDSTALPHA: + return GX_BL_ZERO; + default: + return fac; + } +} + +static inline wgpu::CompareFunction to_compare_function(GXCompare func) { + switch (func) { + DEFAULT_FATAL("invalid depth fn {}", underlying(func)); + case GX_NEVER: + return wgpu::CompareFunction::Never; + case GX_LESS: + return wgpu::CompareFunction::Less; + case GX_EQUAL: + return wgpu::CompareFunction::Equal; + case GX_LEQUAL: + return wgpu::CompareFunction::LessEqual; + case GX_GREATER: + return wgpu::CompareFunction::Greater; + case GX_NEQUAL: + return wgpu::CompareFunction::NotEqual; + case GX_GEQUAL: + return wgpu::CompareFunction::GreaterEqual; + case GX_ALWAYS: + return wgpu::CompareFunction::Always; + } +} + +static inline wgpu::BlendState to_blend_state(GXBlendMode mode, GXBlendFactor srcFac, GXBlendFactor dstFac, + GXLogicOp op, GXPixelFmt pixelFmt, u32 dstAlpha) { + wgpu::BlendComponent colorBlendComponent; + switch (mode) { + DEFAULT_FATAL("unsupported blend mode {}", underlying(mode)); + case GX_BM_NONE: + colorBlendComponent = { + .operation = wgpu::BlendOperation::Add, + .srcFactor = wgpu::BlendFactor::One, + .dstFactor = wgpu::BlendFactor::Zero, + }; + break; + case GX_BM_BLEND: + if (!render_target_has_alpha(pixelFmt)) { + srcFac = remove_dst_alpha_usage(srcFac); + dstFac = remove_dst_alpha_usage(dstFac); + } + colorBlendComponent = { + .operation = wgpu::BlendOperation::Add, + .srcFactor = to_blend_factor(srcFac, false, false), + .dstFactor = to_blend_factor(dstFac, true, false), + }; + break; + case GX_BM_SUBTRACT: + colorBlendComponent = { + .operation = wgpu::BlendOperation::ReverseSubtract, + .srcFactor = wgpu::BlendFactor::One, + .dstFactor = wgpu::BlendFactor::One, + }; + break; + case GX_BM_LOGIC: + switch (op) { + default: + // WebGPU exposes blend factors, but not fixed-function integer logic ops. + colorBlendComponent = { + .operation = wgpu::BlendOperation::Add, + .srcFactor = wgpu::BlendFactor::One, + .dstFactor = wgpu::BlendFactor::Zero, + }; + break; + case GX_LO_CLEAR: + colorBlendComponent = { + .operation = wgpu::BlendOperation::Add, + .srcFactor = wgpu::BlendFactor::Zero, + .dstFactor = wgpu::BlendFactor::Zero, + }; + break; + case GX_LO_AND: + case GX_LO_REVAND: + case GX_LO_XOR: + case GX_LO_NOR: + case GX_LO_EQUIV: + case GX_LO_INV: + case GX_LO_REVOR: + case GX_LO_INVCOPY: + case GX_LO_INVOR: + case GX_LO_NAND: + case GX_LO_COPY: + case GX_LO_SET: + colorBlendComponent = { + .operation = wgpu::BlendOperation::Add, + .srcFactor = wgpu::BlendFactor::One, + .dstFactor = wgpu::BlendFactor::Zero, + }; + break; + case GX_LO_INVAND: + colorBlendComponent = { + .operation = wgpu::BlendOperation::Add, + .srcFactor = wgpu::BlendFactor::Zero, + .dstFactor = wgpu::BlendFactor::OneMinusSrc, + }; + break; + case GX_LO_NOOP: + colorBlendComponent = { + .operation = wgpu::BlendOperation::Add, + .srcFactor = wgpu::BlendFactor::Zero, + .dstFactor = wgpu::BlendFactor::One, + }; + break; + case GX_LO_OR: + // WebGPU has no fixed-function integer logic op. + colorBlendComponent = { + .operation = wgpu::BlendOperation::Max, + .srcFactor = wgpu::BlendFactor::One, + .dstFactor = wgpu::BlendFactor::One, + }; + break; + } + break; + } + wgpu::BlendComponent alphaBlendComponent; + if (dstAlpha != UINT32_MAX) { + alphaBlendComponent = wgpu::BlendComponent{ + .operation = wgpu::BlendOperation::Add, + .srcFactor = wgpu::BlendFactor::Constant, + .dstFactor = wgpu::BlendFactor::Zero, + }; + } else if (mode == GX_BM_BLEND) { + alphaBlendComponent = wgpu::BlendComponent{ + .operation = wgpu::BlendOperation::Add, + .srcFactor = to_blend_factor(srcFac, false, true), + .dstFactor = to_blend_factor(dstFac, true, true), + }; + } else { + alphaBlendComponent = colorBlendComponent; + } + return { + .color = colorBlendComponent, + .alpha = alphaBlendComponent, + }; +} + +static inline bool effective_alpha_update(GXPixelFmt pixelFmt, bool alphaUpdate) { + return alphaUpdate && render_target_has_alpha(pixelFmt); +} + +static inline u32 effective_dst_alpha(GXPixelFmt pixelFmt, bool alphaUpdate, u32 dstAlpha) { + return effective_alpha_update(pixelFmt, alphaUpdate) ? dstAlpha : UINT32_MAX; +} + +static inline wgpu::ColorWriteMask to_write_mask(bool colorUpdate, bool alphaUpdate) { + wgpu::ColorWriteMask writeMask = wgpu::ColorWriteMask::None; + if (colorUpdate) { + writeMask |= wgpu::ColorWriteMask::Red | wgpu::ColorWriteMask::Green | wgpu::ColorWriteMask::Blue; + } + if (alphaUpdate) { + writeMask |= wgpu::ColorWriteMask::Alpha; + } + return writeMask; +} + +static inline wgpu::PrimitiveState to_primitive_state(GXCullMode gx_cullMode) { + auto cullMode = wgpu::CullMode::None; + switch (gx_cullMode) { + DEFAULT_FATAL("unsupported cull mode {}", underlying(gx_cullMode)); + case GX_CULL_FRONT: + cullMode = wgpu::CullMode::Front; + break; + case GX_CULL_BACK: + cullMode = wgpu::CullMode::Back; + break; + case GX_CULL_ALL: + // todo: remove? since all calls get dropped anyways + cullMode = wgpu::CullMode::Front; + break; + case GX_CULL_NONE: + break; + } + return { + .topology = wgpu::PrimitiveTopology::TriangleList, + .stripIndexFormat = wgpu::IndexFormat::Undefined, + .frontFace = wgpu::FrontFace::CW, + .cullMode = cullMode, + }; +} + +wgpu::RenderPipeline build_pipeline(const PipelineConfig& config, ArrayRef vtxBuffers, + wgpu::ShaderModule shader, const char* label) noexcept { + ZoneScoped; + const wgpu::DepthStencilState depthStencil{ + .format = g_graphicsConfig.depthFormat, + .depthWriteEnabled = config.depthUpdate, + .depthCompare = config.depthCompare ? to_compare_function(config.depthFunc) : wgpu::CompareFunction::Always, + }; + const auto blendState = to_blend_state(config.blendMode, config.blendFacSrc, config.blendFacDst, config.blendOp, + config.pixelFmt, config.dstAlpha); + const std::array colorTargets{wgpu::ColorTargetState{ + .format = g_graphicsConfig.surfaceConfiguration.format, + .blend = &blendState, + .writeMask = to_write_mask(config.colorUpdate, config.alphaUpdate), + }}; + const wgpu::FragmentState fragmentState{ + .module = shader, + .entryPoint = "fs_main", + .targetCount = colorTargets.size(), + .targets = colorTargets.data(), + }; + const wgpu::RenderPipelineDescriptor descriptor{ + .label = label, + .layout = sPipelineLayout, + .vertex = + { + .module = shader, + .entryPoint = "vs_main", + .bufferCount = static_cast(vtxBuffers.size()), + .buffers = vtxBuffers.data(), + }, + .primitive = to_primitive_state(config.cullMode), + .depthStencil = &depthStencil, + .multisample = + wgpu::MultisampleState{ + .count = config.msaaSamples, + }, + .fragment = &fragmentState, + }; + return g_device.CreateRenderPipeline(&descriptor); +} + +u8 comp_type_size(GXAttr attr, GXCompType type) noexcept { + switch (attr) { + case GX_VA_PNMTXIDX: + case GX_VA_TEX0MTXIDX: + case GX_VA_TEX1MTXIDX: + case GX_VA_TEX2MTXIDX: + case GX_VA_TEX3MTXIDX: + case GX_VA_TEX4MTXIDX: + case GX_VA_TEX5MTXIDX: + case GX_VA_TEX6MTXIDX: + case GX_VA_TEX7MTXIDX: + return 1; + case GX_VA_CLR0: + case GX_VA_CLR1: + switch (type) { + case GX_RGB565: + case GX_RGBA4: + return 2; + case GX_RGB8: + case GX_RGBA6: + return 3; + case GX_RGBX8: + case GX_RGBA8: + return 4; + } + default: + switch (type) { + case GX_U8: + case GX_S8: + return 1; + case GX_U16: + case GX_S16: + return 2; + case GX_F32: + return 4; + default: + Log.fatal("comp_type_size: Unsupported component type {}", type); + } + } +} + +u8 comp_cnt_count(GXAttr attr, GXCompCnt cnt) noexcept { + switch (attr) { + case GX_VA_PNMTXIDX: + case GX_VA_TEX0MTXIDX: + case GX_VA_TEX1MTXIDX: + case GX_VA_TEX2MTXIDX: + case GX_VA_TEX3MTXIDX: + case GX_VA_TEX4MTXIDX: + case GX_VA_TEX5MTXIDX: + case GX_VA_TEX6MTXIDX: + case GX_VA_TEX7MTXIDX: + return 1; + case GX_VA_POS: + switch (cnt) { + case GX_POS_XY: + return 2; + case GX_POS_XYZ: + return 3; + default: + break; + } + break; + case GX_VA_NRM: + switch (cnt) { + case GX_NRM_XYZ: + return 3; + case GX_NRM_NBT: + case GX_NRM_NBT3: + return 9; + default: + break; + } + break; + case GX_VA_CLR0: + case GX_VA_CLR1: + return 1; + case GX_VA_TEX0: + case GX_VA_TEX1: + case GX_VA_TEX2: + case GX_VA_TEX3: + case GX_VA_TEX4: + case GX_VA_TEX5: + case GX_VA_TEX6: + case GX_VA_TEX7: + switch (cnt) { + case GX_TEX_S: + return 1; + case GX_TEX_ST: + return 2; + default: + break; + } + break; + default: + break; + } + Log.fatal("comp_cnt_count: Unsupported attr/cnt {} {}", attr, cnt); +} + +static u8 index_attr_size(GXAttr attr, GXCompCnt cnt, GXAttrType type) noexcept { + const u8 indexSize = type == GX_INDEX16 ? 2 : 1; + if (attr == GX_VA_NRM && cnt == GX_NRM_NBT3) { + return indexSize * 3; + } + return indexSize; +} + +void populate_pipeline_config(PipelineConfig& config, GXPrimitive primitive, GXVtxFmt fmt) noexcept { + ZoneScoped; + + const auto& vtxFmt = g_gxState.vtxFmts[fmt]; + // GX applies integer logic ops in the PE after fog. + config.shaderConfig.fogType = effective_pipeline_fog_type( + g_gxState.fog.type, g_gxState.zTextureOp, g_gxState.zCompLocBeforeTex, + g_gxState.blendMode, g_gxState.blendOp); + config.shaderConfig.fogRangeAdjust = + config.shaderConfig.fogType != GX_FOG_NONE && (g_gxState.fogRange[0] & (1u << 10)) != 0; + u8 vtxOffset = 0; + for (int i = GX_VA_PNMTXIDX; i <= GX_VA_TEX7; ++i) { + const auto attr = static_cast(i); + const auto type = g_gxState.vtxDesc[i]; + auto& mapping = config.shaderConfig.attrs[i]; + if (type == GX_NONE) { + mapping = {}; + continue; + } + const auto& attrFmt = vtxFmt.attrs[i]; + const auto cnt = comp_cnt_count(attr, attrFmt.cnt); + mapping = AttrConfig{ + .attrType = static_cast(type), + .cnt = cnt, + .compType = static_cast(attrFmt.type), + .offset = vtxOffset, + .stride = 0, + .frac = attrFmt.frac, + .le = false, + .nrmIndexCount = static_cast(attr == GX_VA_NRM && attrFmt.cnt == GX_NRM_NBT3 ? 3 : 1), + }; + switch (type) { + case GX_DIRECT: { + vtxOffset += comp_type_size(attr, attrFmt.type) * cnt; + break; + } + case GX_INDEX8: + mapping.stride = g_gxState.arrays[i].stride; + mapping.le = g_gxState.arrays[i].le; + vtxOffset += index_attr_size(attr, attrFmt.cnt, type); + break; + case GX_INDEX16: + mapping.stride = g_gxState.arrays[i].stride; + mapping.le = g_gxState.arrays[i].le; + vtxOffset += index_attr_size(attr, attrFmt.cnt, type); + break; + default: + Log.fatal("populate_pipeline_config: Invalid vertex type {}", type); + } + } + config.shaderConfig.vtxStride = vtxOffset; + if (primitive == GX_LINES) { + config.shaderConfig.lineMode = 1; + } else if (primitive == GX_LINESTRIP) { + config.shaderConfig.lineMode = 2; + } else if (primitive == GX_POINTS) { + config.shaderConfig.lineMode = 3; + } else { + config.shaderConfig.lineMode = 0; + } + config.shaderConfig.dualTexEnabled = (g_gxState.dualTex & 1u) != 0; + config.shaderConfig.tevSwapTable = g_gxState.tevSwapTable; + for (u8 i = 0; i < g_gxState.numTevStages; ++i) { + config.shaderConfig.tevStages[i] = g_gxState.tevStages[i]; + } + config.shaderConfig.tevStageCount = g_gxState.numTevStages; + config.shaderConfig.numTexGens = g_gxState.numTexGens; + if (g_gxState.zTextureOp != GX_ZT_DISABLE && !g_gxState.zCompLocBeforeTex) { + config.shaderConfig.zTexture = (g_gxState.zTextureBias & 0x00FFFFFFu) | + ((static_cast(g_gxState.zTextureFmt) & 0x3u) << 24) | + ((static_cast(g_gxState.zTextureOp) & 0x3u) << 26); + } + for (u8 i = 0; i < g_gxState.numIndStages; ++i) { + config.shaderConfig.indStages[i] = g_gxState.indStages[i]; + } + config.shaderConfig.numIndStages = g_gxState.numIndStages; + for (u8 i = 0; i < MaxColorChannels; ++i) { + const auto& cc = g_gxState.colorChannelConfig[i]; + if (cc.lightingEnabled) { + config.shaderConfig.colorChannels[i] = cc; + } else { + // Only matSrc matters when lighting disabled + config.shaderConfig.colorChannels[i] = { + .matSrc = cc.matSrc, + }; + } + } + for (u8 i = 0; i < MaxTexCoord; ++i) { + config.shaderConfig.tcgs[i].src = static_cast(GX_TG_TEX0 + i); + } + for (u8 i = 0; i < g_gxState.numTexGens; ++i) { + config.shaderConfig.tcgs[i] = g_gxState.tcgs[i]; + } + config.shaderConfig.alphaCompare = {}; + if (g_gxState.alphaCompare) { + config.shaderConfig.alphaCompare = g_gxState.alphaCompare; + } + // The remaining fields are assigned in place. + config.version = GXPipelineConfigVersion; + config.msaaSamples = gfx::get_sample_count(); + config.depthFunc = g_gxState.depthFunc; + config.cullMode = config.shaderConfig.lineMode == 0 ? g_gxState.cullMode : GX_CULL_NONE; + config.blendMode = g_gxState.blendMode; + config.blendFacSrc = g_gxState.blendFacSrc; + config.blendFacDst = g_gxState.blendFacDst; + config.blendOp = g_gxState.blendOp; + config.pixelFmt = g_gxState.pixelFmt; + config.dstAlpha = effective_dst_alpha(g_gxState.pixelFmt, g_gxState.alphaUpdate, g_gxState.dstAlpha); + config.depthCompare = g_gxState.depthCompare; + config.depthUpdate = g_gxState.depthUpdate; + config.alphaUpdate = effective_alpha_update(g_gxState.pixelFmt, g_gxState.alphaUpdate); + config.colorUpdate = g_gxState.colorUpdate; +} + +static TextureBindGroupCacheKey make_texture_bind_group_cache_key(const ShaderInfo& info) noexcept { + TextureBindGroupCacheKey key{ + .sampledTextures = info.sampledTextures.to_ullong(), + .sampledIndTextures = info.sampledIndTextures.to_ullong(), + }; + for (u32 i = 0; i < MaxTextures; ++i) { + if (!info.sampledTextures[i] && !info.sampledIndTextures[i]) { + continue; + } + const auto& tex = g_gxState.textures[i]; + if (!tex) { + continue; + } + const u32 refState = (tex.ref->isReplacement ? 1u : 0u) | (tex.ref->hasArbitraryMips ? 2u : 0u) | + (tex.ref->mipCount << 8); + set_texture_bind_group_cache_slot(key, i, tex.ref->sampleTextureView.Get(), tex.texObj.mode0, + tex.texObj.mode1, refState); + } + return key; +} + +static HashType hash_texture_bind_group_cache_key(const TextureBindGroupCacheKey& key) noexcept { + // The key is a 176-byte POD with no padding, so one hash over the whole thing replaces the six chained XXH3 calls this used to make per draw. + static_assert(std::has_unique_object_representations_v); + return xxh3_hash(key); +} + +static GXBindGroups build_bind_groups_uncached(const ShaderInfo& info) noexcept { + ZoneScoped; + + if (!info.sampledTextures.any() && !info.sampledIndTextures.any()) { + // Don't bother re-binding anything + return {}; + } + + // Using C WGPU types instead of C++ wrappers to avoid destructor overhead + std::array textureEntries{}; + for (u32 i = 0; i < MaxTextures; ++i) { + const auto& tex = g_gxState.textures[i]; + WGPUBindGroupEntry& textureEntry = textureEntries[i * 2]; + WGPUBindGroupEntry& samplerEntry = textureEntries[i * 2 + 1]; + textureEntry.binding = i * 2; + samplerEntry.binding = i * 2 + 1; + if (tex && (info.sampledTextures[i] || info.sampledIndTextures[i])) { + textureEntry.textureView = tex.ref->sampleTextureView.Get(); + samplerEntry.sampler = gfx::sampler_ref(tex.get_descriptor()).Get(); + } else { + textureEntry.textureView = sEmptyTextureView.Get(); + samplerEntry.sampler = sEmptySampler.Get(); + } + } + const WGPUBindGroupDescriptor textureBindGroupDescriptor{ + .label = {"GX Texture Bind Group", WGPU_STRLEN}, + .layout = sTextureBindGroupLayout.Get(), + .entryCount = textureEntries.size(), + .entries = textureEntries.data(), + }; + const auto ref = gfx::bind_group_ref(textureBindGroupDescriptor); + return { + .textureBindGroup = ref, + .resolvedTextureBindGroup = gfx::find_bind_group(ref).Get(), + }; +} + +GXBindGroups build_bind_groups(const ShaderInfo& info) noexcept { + ZoneScoped; + + if (!info.sampledTextures.any() && !info.sampledIndTextures.any()) { + return {}; + } + + constexpr size_t CacheSize = 1024; + struct Entry { + bool valid = false; + u32 frame = 0; + HashType hash = 0; + TextureBindGroupCacheKey key{}; + GXBindGroups bindGroups{}; + }; + static std::array cache{}; + + const auto key = make_texture_bind_group_cache_key(info); + const HashType hash = hash_texture_bind_group_cache_key(key); + const u32 frame = gfx::current_frame(); + auto& entry = cache[hash & (CacheSize - 1)]; + if (entry.valid && entry.frame == frame && entry.hash == hash && entry.key == key) { + return entry.bindGroups; + } + + const auto bindGroups = build_bind_groups_uncached(info); + entry.valid = true; + entry.frame = frame; + entry.hash = hash; + entry.key = key; + entry.bindGroups = bindGroups; + return bindGroups; +} + +void initialize() noexcept { + { + std::array textureEntries; + for (u32 i = 0; i < MaxTextures; ++i) { + textureEntries[i * 2] = { + .binding = i * 2, + .visibility = wgpu::ShaderStage::Fragment, + .texture = + { + .sampleType = wgpu::TextureSampleType::Float, + .viewDimension = wgpu::TextureViewDimension::e2D, + }, + }; + textureEntries[i * 2 + 1] = { + .binding = i * 2 + 1, + .visibility = wgpu::ShaderStage::Fragment, + .sampler = {.type = wgpu::SamplerBindingType::Filtering}, + }; + } + const wgpu::BindGroupLayoutDescriptor descriptor{ + .label = "GX Texture Bind Group Layout", + .entryCount = textureEntries.size(), + .entries = textureEntries.data(), + }; + sTextureBindGroupLayout = g_device.CreateBindGroupLayout(&descriptor); + } + { + constexpr wgpu::SamplerDescriptor descriptor{.label = "Empty sampler"}; + sEmptySampler = gfx::sampler_ref(descriptor); + } + { + constexpr wgpu::TextureDescriptor descriptor{ + .label = "Empty texture", + .usage = wgpu::TextureUsage::TextureBinding, + .size = {1, 1}, + .format = wgpu::TextureFormat::RGBA8Unorm, + }; + sEmptyTexture = g_device.CreateTexture(&descriptor); + sEmptyTextureView = sEmptyTexture.CreateView(); + } + { + std::array entries; + for (u32 i = 0; i < MaxTextures; ++i) { + entries[i * 2] = { + .binding = i * 2, + .textureView = sEmptyTextureView, + }; + entries[i * 2 + 1] = { + .binding = i * 2 + 1, + .sampler = sEmptySampler, + }; + } + const wgpu::BindGroupDescriptor desc{ + .label = "GX Empty Texture Bind Group", + .layout = sTextureBindGroupLayout, + .entryCount = entries.size(), + .entries = entries.data(), + }; + g_emptyTextureBindGroup = g_device.CreateBindGroup(&desc); + } + { + const std::array layouts{ + gfx::g_staticBindGroupLayout, + gfx::g_uniformBindGroupLayout, + sTextureBindGroupLayout, + }; + const wgpu::PipelineLayoutDescriptor desc{ + .label = "GX Pipeline Layout", + .bindGroupLayoutCount = layouts.size(), + .bindGroupLayouts = layouts.data(), + }; + sPipelineLayout = g_device.CreatePipelineLayout(&desc); + } +} + +void shutdown() noexcept { + // TODO we should probably store this all in g_state.gx instead + sSamplerBindGroupLayout = {}; + sTextureBindGroupLayout = {}; + { + std::lock_guard lock{sBindGroupLayoutMutex}; + sUniformBindGroupLayouts.clear(); + sTextureBindGroupLayouts.clear(); + } + for (auto& item : g_gxState.textures) { + item.ref.reset(); + } + s_textureObjectCaches.clear(); + s_tlutObjectCaches.clear(); + s_staticTextureSourceCache.clear(); + s_staticPaletteTextureSourceCache.clear(); + s_texObjSourceKeys.clear(); + s_texObjPaletteSourceKeys.clear(); + s_texObjSourceKeyMemo.reset(); + s_texObjPaletteSourceKeyMemo.reset(); + clear_static_source_front_cache(); + // The resolve identity cache holds texture references too; it is revision gated rather than cleared during normal operation, so it has to be dropped explicitly here. + for (auto& entry : s_textureResolveIdentityCache) { + entry.valid = false; + entry.binding.reset(); + } + s_lastTextureResolveIdentityValid.fill(false); + s_lastStaticSourceResolveKeyValid.fill(false); + g_gxState.loadedTextures.fill({}); + g_gxState.loadedTluts.fill({}); + clear_copy_texture_cache(); +} +} // namespace aurora::gx + +static wgpu::AddressMode wgpu_address_mode(GXTexWrapMode mode) { + switch (mode) { + DEFAULT_FATAL("invalid wrap mode {}", underlying(mode)); + case GX_CLAMP: + return wgpu::AddressMode::ClampToEdge; + case GX_REPEAT: + return wgpu::AddressMode::Repeat; + case GX_MIRROR: + return wgpu::AddressMode::MirrorRepeat; + } +} + +static std::pair wgpu_filter_mode(GXTexFilter filter) { + switch (filter) { + DEFAULT_FATAL("invalid filter mode {}", static_cast(filter)); + case GX_NEAR: + return {wgpu::FilterMode::Nearest, wgpu::MipmapFilterMode::Undefined}; + case GX_LINEAR: + return {wgpu::FilterMode::Linear, wgpu::MipmapFilterMode::Undefined}; + case GX_NEAR_MIP_NEAR: + return {wgpu::FilterMode::Nearest, wgpu::MipmapFilterMode::Nearest}; + case GX_LIN_MIP_NEAR: + return {wgpu::FilterMode::Linear, wgpu::MipmapFilterMode::Nearest}; + case GX_NEAR_MIP_LIN: + return {wgpu::FilterMode::Nearest, wgpu::MipmapFilterMode::Linear}; + case GX_LIN_MIP_LIN: + return {wgpu::FilterMode::Linear, wgpu::MipmapFilterMode::Linear}; + } +} + +static u16 wgpu_aniso(GXAnisotropy aniso) { + switch (aniso) { + DEFAULT_FATAL("invalid aniso {}", static_cast(aniso)); + case GX_ANISO_1: + case GX_MAX_ANISOTROPY: + return 1; + case GX_ANISO_2: + return std::max(aurora::webgpu::g_graphicsConfig.textureAnisotropy / 2, 1); + case GX_ANISO_4: + return std::max(aurora::webgpu::g_graphicsConfig.textureAnisotropy, 1); + } +} + +static bool supports_wgpu_aniso(wgpu::FilterMode magFilter, wgpu::FilterMode minFilter, + wgpu::MipmapFilterMode mipFilter) { + return magFilter == wgpu::FilterMode::Linear && minFilter == wgpu::FilterMode::Linear && + mipFilter == wgpu::MipmapFilterMode::Linear; +} + +wgpu::SamplerDescriptor aurora::gfx::TextureBind::get_descriptor() const noexcept { + auto [minFilter, mipFilter] = wgpu_filter_mode(texObj.min_filter()); + const auto [magFilter, _] = wgpu_filter_mode(texObj.mag_filter()); + float minLod = texObj.min_lod(); + float maxLod = texObj.max_lod(); + if (ref && ref->isReplacement) { + minFilter = wgpu::FilterMode::Linear; + mipFilter = wgpu::MipmapFilterMode::Linear; + minLod = 0.f; + maxLod = static_cast(std::max(ref->mipCount, 1u) - 1u); + } else if (ref && ref->hasArbitraryMips && mipFilter != wgpu::MipmapFilterMode::Undefined) { + mipFilter = wgpu::MipmapFilterMode::Linear; + } else if (mipFilter == wgpu::MipmapFilterMode::Undefined) { + minLod = 0.f; + maxLod = 0.f; + } + u16 maxAnisotropy = wgpu_aniso(texObj.max_aniso()); + if (maxAnisotropy > 1 && !supports_wgpu_aniso(magFilter, minFilter, mipFilter)) { + maxAnisotropy = 1; + } + return { + .label = "Generated Filtering Sampler", + .addressModeU = wgpu_address_mode(texObj.wrap_s()), + .addressModeV = wgpu_address_mode(texObj.wrap_t()), + .addressModeW = wgpu::AddressMode::Repeat, + .magFilter = magFilter, + .minFilter = minFilter, + .mipmapFilter = mipFilter, + .lodMinClamp = minLod, + .lodMaxClamp = maxLod, + .maxAnisotropy = maxAnisotropy, + }; +} // namespace aurora::gx diff --git a/aurora-main/lib/gx/gx.hpp b/aurora-main/lib/gx/gx.hpp new file mode 100644 index 0000000..185e0851 --- /dev/null +++ b/aurora-main/lib/gx/gx.hpp @@ -0,0 +1,767 @@ +#pragma once +#include +#include + +#include "../internal.hpp" +#include "../gfx/common.hpp" +#include "../gfx/texture.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define M_PIF 3.14159265358979323846f + +namespace GX { +constexpr u8 MaxLights = 8; +using LightMask = std::bitset; +} // namespace GX + +struct GXLightObj_ { + GXColor color; + float a0 = 1.f; + float a1 = 0.f; + float a2 = 0.f; + float k0 = 1.f; + float k1 = 0.f; + float k2 = 0.f; + float px = 0.f; + float py = 0.f; + float pz = 0.f; + float nx = 0.f; + float ny = 0.f; + float nz = 0.f; +}; +static_assert(sizeof(GXLightObj_) <= sizeof(GXLightObj), "GXLightObj too small!"); + +#if GX_IS_WII +constexpr float GX_LARGE_NUMBER = -1.0e+18f; +#else +constexpr float GX_LARGE_NUMBER = -1048576.0f; +#endif + +namespace aurora::gx { +constexpr bool UsePerPixelLighting = false; +constexpr bool UseReversedZ = true; + +constexpr u32 MaxTextures = GX_MAX_TEXMAP; +constexpr u32 MaxTluts = 20; +constexpr u32 MaxTevStages = GX_MAX_TEVSTAGE; +constexpr u32 MaxColorChannels = 4; +constexpr u32 MaxTevRegs = 4; // TEVPREV, TEVREG0-2 +constexpr u32 MaxKColors = GX_MAX_KCOLOR; +constexpr u32 MaxTexMtx = 10; +constexpr u32 MaxPTTexMtx = 20; +constexpr u32 MaxTexCoord = GX_MAX_TEXCOORD; +constexpr u32 MaxVtxAttr = GX_VA_MAX_ATTR; +constexpr u32 MaxTevSwap = GX_MAX_TEVSWAP; +constexpr u32 MaxIndStages = GX_MAX_INDTEXSTAGE; +constexpr u32 MaxIndTexMtxs = 3; +constexpr u32 MaxVtxFmt = GX_MAX_VTXFMT; +constexpr u32 MaxPnMtx = (GX_PNMTX9 / 3) + 1; +// Position and texture matrices share one shader array (`ubuf.postex_mtx`), mirroring XF matrix memory: rows 0..29 (slots 0..9) are position matrices and rows 30..59 (slots 10..19) are texture matrices. +constexpr u32 MaxPostexMtx = MaxPnMtx + MaxTexMtx; +constexpr u32 MaxIndexAttr = 12; // VA_POS -> VA_TEX7 +constexpr u32 MaxUniformSize = 3840; + +extern wgpu::BindGroup g_emptyTextureBindGroup; + +template +struct TevPass { + Arg a = Default; + Arg b = Default; + Arg c = Default; + Arg d = Default; + + bool operator==(const TevPass& rhs) const { return memcmp(this, &rhs, sizeof(*this)) == 0; } + bool operator!=(const TevPass& rhs) const { return !(*this == rhs); } +}; +static_assert(std::has_unique_object_representations_v>); +static_assert(std::has_unique_object_representations_v>); +struct TevOp { + GXTevOp op = GX_TEV_ADD; + GXTevBias bias = GX_TB_ZERO; + GXTevScale scale = GX_CS_SCALE_1; + GXTevRegID outReg = GX_TEVPREV; + bool clamp = true; + u8 _p1 = 0; + u8 _p2 = 0; + u8 _p3 = 0; + + bool operator==(const TevOp& rhs) const { return memcmp(this, &rhs, sizeof(*this)) == 0; } + bool operator!=(const TevOp& rhs) const { return !(*this == rhs); } +}; +static_assert(std::has_unique_object_representations_v); +struct TevStage { + TevPass colorPass; + TevPass alphaPass; + TevOp colorOp; + TevOp alphaOp; + GXTevKColorSel kcSel = GX_TEV_KCSEL_1; + GXTevKAlphaSel kaSel = GX_TEV_KASEL_1; + GXTexCoordID texCoordId = GX_TEXCOORD_NULL; + GXTexMapID texMapId = GX_TEXMAP_NULL; + GXChannelID channelId = GX_COLOR_NULL; + GXTevSwapSel tevSwapRas = GX_TEV_SWAP0; + GXTevSwapSel tevSwapTex = GX_TEV_SWAP0; + GXIndTexStageID indTexStage = GX_INDTEXSTAGE0; + GXIndTexFormat indTexFormat = GX_ITF_8; + GXIndTexBiasSel indTexBiasSel = GX_ITB_NONE; + GXIndTexAlphaSel indTexAlphaSel = GX_ITBA_OFF; + GXIndTexMtxID indTexMtxId = GX_ITM_OFF; + GXIndTexWrap indTexWrapS = GX_ITW_OFF; + GXIndTexWrap indTexWrapT = GX_ITW_OFF; + bool indTexUseOrigLOD = false; + bool indTexAddPrev = false; + u8 _p1 = 0; + u8 _p2 = 0; + + bool operator==(const TevStage& rhs) const { return memcmp(this, &rhs, sizeof(*this)) == 0; } + bool operator!=(const TevStage& rhs) const { return !(*this == rhs); } +}; +static_assert(std::has_unique_object_representations_v); +struct IndStage { + GXTexCoordID texCoordId; + GXTexMapID texMapId; + GXIndTexScale scaleS; + GXIndTexScale scaleT; +}; +static_assert(std::has_unique_object_representations_v); +// For shader generation +struct ColorChannelConfig { + GXColorSrc matSrc = GX_SRC_REG; + GXColorSrc ambSrc = GX_SRC_REG; + GXDiffuseFn diffFn = GX_DF_NONE; + GXAttnFn attnFn = GX_AF_NONE; + bool lightingEnabled = false; + u8 _p1 = 0; + u8 _p2 = 0; + u8 _p3 = 0; + + bool operator==(const ColorChannelConfig& rhs) const { return memcmp(this, &rhs, sizeof(*this)) == 0; } + bool operator!=(const ColorChannelConfig& rhs) const { return !(*this == rhs); } +}; +static_assert(std::has_unique_object_representations_v); +// For uniform generation +struct ColorChannelState { + Vec4 matColor; + Vec4 ambColor; + GX::LightMask lightMask; +}; +struct TcgConfig { + GXTexGenType type = GX_TG_MTX2x4; + GXTexGenSrc src = GX_MAX_TEXGENSRC; + GXTexMtx mtx = GX_IDENTITY; + GXPTTexMtx postMtx = GX_PTIDENTITY; + bool normalize = false; + bool inputFormAB11 = false; + u8 _p2 = 0; + u8 _p3 = 0; + + bool operator==(const TcgConfig& rhs) const { return memcmp(this, &rhs, sizeof(*this)) == 0; } + bool operator!=(const TcgConfig& rhs) const { return !(*this == rhs); } +}; +static_assert(std::has_unique_object_representations_v); +struct FogState { + GXFogType type = GX_FOG_NONE; + float a = 0.f; + float b = 0.5f; + float c = 0.f; + float aRaw = 0.f; + Vec4 color; + // Raw encoded register values for A/B reconstruction across separate BP writes + u32 fog0Raw = 0; // 0xEE: encoded A parameter + u32 fog1Raw = 0; // 0xEF: B mantissa + u32 fog2Raw = 0; // 0xF0: B shift + u32 bMagnitude = 1; + u32 bShift = 1; + + bool operator==(const FogState& rhs) const { + return type == rhs.type && a == rhs.a && b == rhs.b && c == rhs.c && aRaw == rhs.aRaw && color == rhs.color && + bMagnitude == rhs.bMagnitude && bShift == rhs.bShift; + } + bool operator!=(const FogState& rhs) const { return !(*this == rhs); } +}; +struct TevSwap { + GXTevColorChan red = GX_CH_RED; + GXTevColorChan green = GX_CH_GREEN; + GXTevColorChan blue = GX_CH_BLUE; + GXTevColorChan alpha = GX_CH_ALPHA; + + bool operator==(const TevSwap& rhs) const { return memcmp(this, &rhs, sizeof(*this)) == 0; } + bool operator!=(const TevSwap& rhs) const { return !(*this == rhs); } + explicit operator bool() const { return !(*this == TevSwap{}); } +}; +static_assert(std::has_unique_object_representations_v); +struct AlphaCompare { + GXCompare comp0 = GX_ALWAYS; + u32 ref0 = 0; // would be u8 but extended to avoid padding bytes + GXAlphaOp op = GX_AOP_AND; + GXCompare comp1 = GX_ALWAYS; + u32 ref1 = 0; + + bool operator==(const AlphaCompare& rhs) const { return memcmp(this, &rhs, sizeof(*this)) == 0; } + bool operator!=(const AlphaCompare& rhs) const { return !(*this == rhs); } + explicit operator bool() const { return comp0 != GX_ALWAYS || comp1 != GX_ALWAYS; } +}; +static_assert(std::has_unique_object_representations_v); +struct IndTexMtxInfo { + Mat3x2 mtx; + s8 scaleExp = 0; + // Accumulated adjScale bits from BP registers (2 bits per row, 3 rows) + u8 adjScaleRaw = 0; + + bool operator==(const IndTexMtxInfo& rhs) const { return mtx == rhs.mtx && scaleExp == rhs.scaleExp; } + bool operator!=(const IndTexMtxInfo& rhs) const { return !(*this == rhs); } +}; +struct TexCoordScale { + u16 scaleS = 0; // texture width - 1 + u16 scaleT = 0; // texture height - 1 + bool biasS = false; + bool biasT = false; + bool cylWrapS = false; + bool cylWrapT = false; + bool lineOffset = false; + bool pointOffset = false; + + bool operator==(const TexCoordScale& rhs) const { + return scaleS == rhs.scaleS && scaleT == rhs.scaleT && biasS == rhs.biasS && biasT == rhs.biasT && + cylWrapS == rhs.cylWrapS && cylWrapT == rhs.cylWrapT && lineOffset == rhs.lineOffset && + pointOffset == rhs.pointOffset; + } + bool operator!=(const TexCoordScale& rhs) const { return !(*this == rhs); } +}; +struct VtxAttrFmt { + GXCompCnt cnt; + GXCompType type; + u8 frac; + u8 _p1 = 0; + u8 _p2 = 0; + u8 _p3 = 0; +}; +static_assert(std::has_unique_object_representations_v); +struct VtxFmt { + std::array attrs; +}; +static_assert(std::has_unique_object_representations_v); +struct PnMtx { + Mat3x4 pos; + Mat3x4 nrm; +}; +static_assert(sizeof(PnMtx) == sizeof(Mat3x4) * 2); +struct Light { + Vec4 pos{0.f, 0.f, 0.f}; + Vec4 dir{0.f, 0.f, 0.f}; + Vec4 color{0.f, 0.f, 0.f, 0.f}; + Vec4 cosAtt{0.f, 0.f, 0.f}; + Vec4 distAtt{0.f, 0.f, 0.f}; + + bool operator==(const Light& rhs) const { + return pos == rhs.pos && dir == rhs.dir && color == rhs.color && cosAtt == rhs.cosAtt && distAtt == rhs.distAtt; + } + bool operator!=(const Light& rhs) const { return !(*this == rhs); } +}; +static_assert(sizeof(Light) == 80); +struct Fog { + Vec4 color; + float a = 0.f; + float b = 0.5f; + float c = 0.f; + float pad = FLT_MAX; + Vec4 rangeBase; + std::array, 3> rangeK; +}; +static_assert(sizeof(Fog) == 96); +struct AttrArray { + const void* data; + u32 size; + u8 stride; + bool le = true; + gfx::Range cachedRange; +}; +inline bool operator==(const AttrArray& lhs, const AttrArray& rhs) { + return lhs.data == rhs.data && lhs.size == rhs.size && lhs.stride == rhs.stride && lhs.le == rhs.le; +} +inline bool operator!=(const AttrArray& lhs, const AttrArray& rhs) { return !(lhs == rhs); } + +// Stamps GXState::pipelineStateGeneration, both when a GXState is constructed and on every pipeline-state write. +inline u32 next_gx_state_epoch() noexcept { + static u32 epoch = 0; + return ++epoch; +} + +struct GXState { + struct CopyTextureRef { + gfx::TextureHandle handle; + u32 revision = 0; + u32 lastProducedFrame = 0; + u32 lastSampledFrame = 0; + u32 width = 0; + u32 height = 0; + u32 dataSize = 0; + GXTexFmt format = GX_TF_I4; + bool sampledThisFrame = false; + + operator bool() const noexcept { return handle.operator bool(); } + }; + std::array pnMtx; + u32 currentPnMtx; + Mat4x4 proj; + GXProjectionType projType; // for GXGetProjectionv + FogState fog; + GXCullMode cullMode = GX_CULL_BACK; + u8 lineWidth = 0; + u8 pointSize = 0; + GXTexOffset lineTexOffset = GX_TO_ZERO; + GXTexOffset pointTexOffset = GX_TO_ZERO; + bool lineHalfAspect = false; + GXBlendMode blendMode = GX_BM_NONE; + GXBlendFactor blendFacSrc = GX_BL_SRCALPHA; + GXBlendFactor blendFacDst = GX_BL_INVSRCALPHA; + GXLogicOp blendOp = GX_LO_CLEAR; + GXCompare depthFunc = GX_LEQUAL; + Vec4 clearColor{0.f, 0.f, 0.f, 1.f}; + u32 clearDepth = 0xFFFFFF; + GXPixelFmt pixelFmt = GX_PF_RGB8_Z24; + GXZFmt16 zFmt = GX_ZC_LINEAR; + bool zCompLocBeforeTex = false; + GXZTexOp zTextureOp = GX_ZT_DISABLE; + u8 zTextureFmt = 0; + u32 zTextureBias = 0; + u32 dstAlpha; // u8; UINT32_MAX = disabled + AlphaCompare alphaCompare; + std::array, MaxTevRegs> colorRegs; + std::array, GX_MAX_KCOLOR> kcolors; + std::array colorChannelConfig; + std::array colorChannelState; + std::array lights; + // Light direction sanitization/normalization is independent of the draw. + std::array preparedLights; + bool preparedLightsDirty = true; + std::array tevStages; + std::array textures; + std::array loadedTextures; + std::array loadedTluts; + AuroraViewportPolicy viewportPolicy = AURORA_VIEWPORT_FIT; + gfx::Viewport logicalViewport{0.f, 0.f, 640.f, 480.f, 0.f, 1.f}; + gfx::Viewport renderViewport{0.f, 0.f, 640.f, 480.f, 0.f, 1.f}; + gfx::ClipRect logicalScissor{0, 0, 640, 480}; + gfx::ClipRect renderScissor{0, 0, 640, 480}; + std::array xfViewport{320.f, -240.f, 16777215.f, 660.f, 580.f, 16777215.f}; + std::array xfProjection{}; + std::array, MaxTexMtx> texMtxs; + std::array, MaxPTTexMtx> ptTexMtxs; + std::array tcgs; + std::array texCoordScales; + u16 lastVtxSize = 0; + GXVtxFmt lastVtxFmt = GX_MAX_VTXFMT; + std::array vtxDesc; + std::array sourceVtxDesc; + std::array vtxFmts; + std::array tevSwapTable{ + TevSwap{}, + TevSwap{GX_CH_RED, GX_CH_RED, GX_CH_RED, GX_CH_ALPHA}, + TevSwap{GX_CH_GREEN, GX_CH_GREEN, GX_CH_GREEN, GX_CH_ALPHA}, + TevSwap{GX_CH_BLUE, GX_CH_BLUE, GX_CH_BLUE, GX_CH_ALPHA}, + }; + std::array indStages; + std::array indTexMtxs; + u8 indTexMask = 0; + std::array boundingBox{1023, 0, 1023, 0}; + std::array arrays; + gfx::ClipRect texCopySrc; + gfx::ClipRect dispCopySrc; + bool texCopySrcRenderSpace = false; + GXTexFmt texCopyFmt; + u16 texCopyDstWidth = 0; + u16 texCopyDstHeight = 0; + bool texCopyHalfScale = false; + u16 dispCopyDstWidth = 640; + u16 dispCopyDstHeight = 480; + f32 dispCopyYScale = 1.f; + GXGamma dispCopyGamma = GX_GM_1_0; + GXBool copyFilterAa = GX_FALSE; + GXBool copyFilterVf = GX_FALSE; + std::array, 12> copyFilterSamplePattern{}; + std::array copyFilterVFilter{}; + GXFBClamp copyClamp = GX_CLAMP_NONE; + u32 dispCopyFrame2Field = 0; + u32 fieldMask = 0; + u32 fieldMode = 0; + u32 revBits = 0; + std::array fogRange{}; + u32 xfError = 0; + u32 dualTex = 0; + float zScale = 1.f; + float zOffset = 0.f; + s32 scissorOffsetX = 0; + s32 scissorOffsetY = 0; + struct CopyTextureKey { + const void* dest = nullptr; + u32 width = 0; + u32 height = 0; + GXTexFmt format = GX_TF_I4; + + bool operator==(const CopyTextureKey& rhs) const { + return dest == rhs.dest && width == rhs.width && height == rhs.height && format == rhs.format; + } + + template + friend H AbslHashValue(H h, const CopyTextureKey& key) { + return H::combine(std::move(h), key.dest, key.width, key.height, key.format); + } + }; + absl::flat_hash_map copyTextures; + absl::flat_hash_map copyTextureCache; + gfx::TextureHandle displayCopyTexture; + wgpu::BindGroup displayCopyBindGroup; + u32 displayCopyWidth = 0; + u32 displayCopyHeight = 0; + bool depthCompare = true; + bool depthUpdate = true; + bool colorUpdate = true; + bool alphaUpdate = true; + u8 numChans = 0; + u8 numIndStages = 0; + u8 numTevStages = 0; + u8 numTexGens = 0; + bool stateDirty = true; + // Bumped by the decoded register writes that feed populate_pipeline_config, and by nothing else. + u32 pipelineStateGeneration = next_gx_state_epoch(); + std::array bpRegCache = [] { + std::array regs{}; + regs[0xFE] = 0x00FFFFFF; + return regs; + }(); + // Covers XF 0x00-0x5F: the scalar bank, viewport/projection, and the TexGen and post-transform registers that every material's display list re-emits. + std::array xfRegCache{}; + std::array xfRegCacheValid{}; + + [[nodiscard]] bool xfRegMatches(u32 reg, u32 val) const { + return (xfRegCacheValid[reg >> 6] >> (reg & 63) & 1) != 0 && xfRegCache[reg] == val; + } + void storeXfReg(u32 reg, u32 val) { + xfRegCache[reg] = val; + xfRegCacheValid[reg >> 6] |= u64{1} << (reg & 63); + } + // For state that another register bank writes behind the XF cache's back. + void invalidateXfReg(u32 reg) { xfRegCacheValid[reg >> 6] &= ~(u64{1} << (reg & 63)); } + + void clearVtxSizeCache() { lastVtxFmt = GX_MAX_VTXFMT; } +}; +extern GXState g_gxState; +struct ShaderInfo; +struct MappedRenderState { + gfx::Viewport viewport{}; + gfx::ClipRect scissor{}; +}; + +void initialize() noexcept; +void shutdown() noexcept; +void clear_copy_texture_cache() noexcept; +void clear_display_copy_cache() noexcept; +void set_display_copy_present_source() noexcept; +void evict_copy_texture(const void* dest) noexcept; +// Drops retired GX copy targets so they cannot outlive the destination they were recycled for. +void prune_copy_texture_pool(const void* dest) noexcept; +void evict_texture_object(u32 texObjId) noexcept; +void evict_tlut_object(u32 tlutObjId) noexcept; +void invalidate_static_texture_cache() noexcept; +Vec2 logical_fb_size() noexcept; +MappedRenderState map_logical_render_state() noexcept; +gfx::Viewport map_logical_viewport(const gfx::Viewport& logicalViewport) noexcept; +gfx::ClipRect map_logical_scissor(const gfx::ClipRect& logicalScissor) noexcept; +void set_logical_viewport(const gfx::Viewport& viewport) noexcept; +void set_render_viewport(const gfx::Viewport& viewport) noexcept; +void set_logical_scissor(const gfx::ClipRect& scissor) noexcept; +void set_render_scissor(const gfx::ClipRect& scissor) noexcept; +const gfx::TextureBind& get_texture(GXTexMapID id) noexcept; +void resolve_sampled_textures(const ShaderInfo& info) noexcept; + +inline float clear_depth_value() { + return std::min(static_cast(g_gxState.clearDepth) / 16777216.f, 16777215.f / 16777216.f); +} + +inline bool render_target_has_alpha(GXPixelFmt pixelFmt) noexcept { return pixelFmt == GX_PF_RGBA6_Z24; } + +inline float indirect_matrix_scale_multiplier(s8 scaleExp) { + return std::exp2f(static_cast(scaleExp)); +} + +inline s32 indirect_matrix_mantissa(float value) noexcept { + return static_cast(std::lround(value * 1024.0f)); +} + +inline s32 indirect_matrix_shift(s8 scaleExp) noexcept { return -static_cast(scaleExp); } + +static inline bool is_palette_format(u32 fmt) { return fmt == GX_TF_C4 || fmt == GX_TF_C8 || fmt == GX_TF_C14X2; } +static inline bool is_depth_format(u32 fmt) { + return fmt == GX_TF_Z8 || fmt == GX_TF_Z16 || fmt == GX_TF_Z24X8 || fmt == GX_CTF_Z4 || fmt == GX_CTF_Z8M || + fmt == GX_CTF_Z8L || fmt == GX_CTF_Z16L; +} +static inline bool copy_texture_format_compatible(GXTexFmt copyFmt, u32 sampleFmt) noexcept { + if (static_cast(copyFmt) == sampleFmt) { + return true; + } + + switch (copyFmt) { + case GX_CTF_R4: + return sampleFmt == GX_TF_I4; + case GX_CTF_R8: + case GX_CTF_G8: + case GX_CTF_B8: + case GX_CTF_A8: + return sampleFmt == GX_TF_I8 || sampleFmt == GX_TF_A8; + case GX_CTF_RA4: + return sampleFmt == GX_TF_IA4; + case GX_CTF_RA8: + case GX_CTF_RG8: + case GX_CTF_GB8: + case GX_TF_Z16: + return sampleFmt == GX_TF_IA8; + case GX_CTF_YUVA8: + return sampleFmt == GX_TF_RGBA8; + case GX_TF_Z24X8: + return sampleFmt == GX_TF_RGBA8; + default: + return false; + } +} + +struct AttrConfig { + u8 attrType = GX_NONE; // GXAttrType + u8 cnt = 0xFF; // Actual count; not GXCompCnt + u8 compType = 0xFF; // GXCompType + u8 offset = 0; // Offset within vertex + u8 stride = 0; // Array stride + u8 frac = 0; + bool le = true; + u8 nrmIndexCount = 0; // GX_NRM_NBT3 stores three separate normal/tangent/binormal indices. +}; +struct ShaderConfig { + u8 fogType = GX_FOG_NONE; + u8 vtxStride = 0; + u8 lineMode : 2 = 0; // 1 = GX_LINES, 2 = GX_LINESTRIP, 3 = GX_POINTS + u8 dualTexEnabled : 1 = 0; + u8 fogRangeAdjust : 1 = 0; + u8 pad1 : 4 = 0; + u8 numTexGens = 0; + u32 zTexture = 0; // bias[0:23], format[24:25], op[26:27]; 0 disables shader depth output. + std::array attrs; + std::array tevSwapTable; + std::array tevStages; + u32 tevStageCount = 0; + std::array colorChannels; + std::array tcgs; + AlphaCompare alphaCompare; + std::array indStages{}; + u32 numIndStages = 0; + + bool operator==(const ShaderConfig& rhs) const { return memcmp(this, &rhs, sizeof(*this)) == 0; } +}; +static_assert(std::has_unique_object_representations_v); + +// GX supplies an opaque white color when a draw omits vertex colors. +inline int shader_vertex_color_attr(const ShaderConfig& config, u32 channel) noexcept { + const bool hasColor0 = config.attrs[GX_VA_CLR0].attrType != GX_NONE; + const bool hasColor1 = config.attrs[GX_VA_CLR1].attrType != GX_NONE; + if (channel == 0) { + if (hasColor0) { + return GX_VA_CLR0; + } + if (hasColor1) { + return GX_VA_CLR1; + } + } else if (channel == 1 && hasColor0 && hasColor1) { + return GX_VA_CLR1; + } + return -1; +} + +// The indirect TEV coordinate is persistent across stages. +inline bool tev_stage_needs_fixed_texcoord_state(const ShaderConfig& config, u32 stageIdx) noexcept { + if (stageIdx >= config.tevStageCount) { + return false; + } + + const auto& stage = config.tevStages[stageIdx]; + const bool hasCoordOperation = stage.indTexMtxId != GX_ITM_OFF || stage.indTexWrapS != GX_ITW_OFF || + stage.indTexWrapT != GX_ITW_OFF || stage.indTexAddPrev; + const bool feedsNextAddPrev = + stageIdx + 1 < config.tevStageCount && config.tevStages[stageIdx + 1].indTexAddPrev; + return hasCoordOperation || feedsNextAddPrev; +} + +inline bool tev_stage_has_texture_map(const TevStage& stage) noexcept { + return stage.texMapId != GX_TEXMAP_NULL && stage.texMapId < MaxTextures; +} + +// Returns -1 for GX_ITW_OFF, an integer fixed-point mask for the wrapping modes, and 0 for GX_ITW_0. +inline int tev_indirect_wrap_mask(GXIndTexWrap wrap) noexcept { + switch (wrap) { + case GX_ITW_OFF: + return -1; + case GX_ITW_256: + return (256 << 7) - 1; + case GX_ITW_128: + return (128 << 7) - 1; + case GX_ITW_64: + return (64 << 7) - 1; + case GX_ITW_32: + return (32 << 7) - 1; + case GX_ITW_16: + return (16 << 7) - 1; + case GX_ITW_0: + return 0; + default: + return underlying(wrap) >= underlying(GX_ITW_0) ? 0 : -2; + } +} + +inline constexpr s32 tev_s24_wrap(s32 value) noexcept { + const u32 wrapped = static_cast(value) & 0x00ffffffu; + return (wrapped & 0x00800000u) != 0 ? static_cast(wrapped) - 0x01000000 + : static_cast(wrapped); +} + +// GX falls back to texcoord 0 when a TEV order names GX_TEXCOORD_NULL or a coordinate beyond the configured texgen count. +inline int tev_effective_texcoord(const ShaderConfig& config, GXTexCoordID texCoordId) noexcept { + if (config.numTexGens == 0) { + return -1; + } + + const u32 rawTexCoord = underlying(texCoordId); + return rawTexCoord < config.numTexGens && rawTexCoord < MaxTexCoord ? static_cast(rawTexCoord) : 0; +} + +inline bool tev_stage_combiner_uses_texture(const TevStage& stage) noexcept { + const auto& color = stage.colorPass; + const auto& alpha = stage.alphaPass; + return color.a == GX_CC_TEXC || color.a == GX_CC_TEXA || color.b == GX_CC_TEXC || + color.b == GX_CC_TEXA || color.c == GX_CC_TEXC || color.c == GX_CC_TEXA || + color.d == GX_CC_TEXC || color.d == GX_CC_TEXA || alpha.a == GX_CA_TEXA || + alpha.b == GX_CA_TEXA || alpha.c == GX_CA_TEXA || alpha.d == GX_CA_TEXA; +} + +struct TevStageTextureDependency { + int texCoordId = -1; + int texMapId = -1; + bool needsFixedTexcoordState = false; + bool combinerUsesTexture = false; + bool canSampleTexture = false; +}; + +inline bool tev_texture_sample_enabled(const TevStageTextureDependency& dependency, + bool sampleRequested) noexcept { + return sampleRequested && dependency.canSampleTexture; +} + +// Pure TEV order/dependency analysis used by both ShaderInfo and WGSL generation. +inline TevStageTextureDependency tev_stage_texture_dependency(const ShaderConfig& config, + u32 stageIdx) noexcept { + if (stageIdx >= config.tevStageCount) { + return {}; + } + + const auto& stage = config.tevStages[stageIdx]; + TevStageTextureDependency dependency{ + .texCoordId = tev_effective_texcoord(config, stage.texCoordId), + .texMapId = tev_stage_has_texture_map(stage) ? static_cast(underlying(stage.texMapId)) : -1, + .needsFixedTexcoordState = tev_stage_needs_fixed_texcoord_state(config, stageIdx), + .combinerUsesTexture = tev_stage_combiner_uses_texture(stage), + }; + dependency.canSampleTexture = dependency.texCoordId >= 0 && dependency.texMapId >= 0; + return dependency; +} + +inline bool shader_uses_fixed_texcoord_state(const ShaderConfig& config) noexcept { + for (u32 i = 0; i < config.tevStageCount; ++i) { + if (tev_stage_texture_dependency(config, i).needsFixedTexcoordState) { + return true; + } + } + return false; +} + +inline bool tev_z_texture_enabled(const ShaderConfig& config) noexcept { + return ((config.zTexture >> 26) & 0x3u) != GX_ZT_DISABLE; +} + +inline int tev_z_texture_stage(const ShaderConfig& config) noexcept { + if (!tev_z_texture_enabled(config)) { + return -1; + } + for (int i = static_cast(config.tevStageCount) - 1; i >= 0; --i) { + if (tev_stage_texture_dependency(config, static_cast(i)).canSampleTexture) { + return i; + } + } + return -1; +} + +struct PipelineConfig; + +struct GXBindGroups { + gfx::BindGroupRef textureBindGroup; + // Bind group resolved at draw-build time so that gx::render does not have to hash-map the ref again for every draw. + WGPUBindGroup resolvedTextureBindGroup = nullptr; +}; +// Which matrix-memory slots a generated shader can actually read, and where each one lives in the compacted uniform arrays. +struct UniformMatrixLayout { + // `postexSlots`/`nrmSlots` entry meaning "the matrix selected by the current matrix index", which is only known per draw. + static constexpr u8 kCurrentPnMtx = 0xFE; + // `postexRemap` entry for a slot that this shader never reads. + static constexpr u8 kAbsent = 0xFF; + + // Compact index -> absolute postex slot (or kCurrentPnMtx). + std::array postexSlots{}; + // Absolute postex slot -> compact index (or kAbsent). + std::array postexRemap{}; + // Compact index -> absolute position slot (or kCurrentPnMtx). + std::array nrmSlots{}; + u8 postexCount = 0; + u8 nrmCount = 0; + // Position slots 0..MaxPnMtx-1 are uploaded 1:1 at compact indices 0..9, so `current_pnmtx` and any per-vertex index need no remapping. + bool absolutePosRegion = false; +}; +// Output info from shader generation +struct ShaderInfo { + std::bitset sampledTexCoords; + std::bitset sampledTextures; + std::bitset sampledKColors; + std::bitset sampledColorChannels; + // TEV color and alpha outputs are independently addressable. + std::bitset loadsTevRegRgb; + std::bitset loadsTevRegAlpha; + std::bitset writesTevRegRgb; + std::bitset writesTevRegAlpha; + std::bitset usesPTTexMtx; + std::bitset indexAttr; + std::bitset usedIndStages; + std::bitset sampledIndTextures; + std::bitset usedIndTexMtxs; + UniformMatrixLayout matrixLayout; + u32 uniformSize = 0; + bool usesFog : 1 = false; + bool lightingEnabled : 1 = false; + u8 lineMode : 2 = 0; +}; +struct BindGroupRanges { + std::array vaRanges{}; +}; +void populate_pipeline_config(PipelineConfig& config, GXPrimitive primitive, GXVtxFmt fmt) noexcept; +wgpu::RenderPipeline build_pipeline(const PipelineConfig& config, ArrayRef vtxBuffers, + wgpu::ShaderModule shader, const char* label) noexcept; +wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept; +GXBindGroups build_bind_groups(const ShaderInfo& info) noexcept; +void notify_copy_texture_created() noexcept; + +u8 comp_type_size(GXAttr attr, GXCompType type) noexcept; +u8 comp_cnt_count(GXAttr attr, GXCompCnt cnt) noexcept; +} // namespace aurora::gx diff --git a/aurora-main/lib/gx/gx_fmt.hpp b/aurora-main/lib/gx/gx_fmt.hpp new file mode 100644 index 0000000..ca1fb33 --- /dev/null +++ b/aurora-main/lib/gx/gx_fmt.hpp @@ -0,0 +1,952 @@ +#pragma once + +#include "../internal.hpp" + +#include +#include +#include + +inline std::string format_as(const GXTevOp& op) { + switch (op) { + case GX_TEV_ADD: + return "GX_TEV_ADD"; + case GX_TEV_SUB: + return "GX_TEV_SUB"; + case GX_TEV_COMP_R8_GT: + return "GX_TEV_COMP_R8_GT"; + case GX_TEV_COMP_R8_EQ: + return "GX_TEV_COMP_R8_EQ"; + case GX_TEV_COMP_GR16_GT: + return "GX_TEV_COMP_GR16_GT"; + case GX_TEV_COMP_GR16_EQ: + return "GX_TEV_COMP_GR16_EQ"; + case GX_TEV_COMP_BGR24_GT: + return "GX_TEV_COMP_BGR24_GT"; + case GX_TEV_COMP_BGR24_EQ: + return "GX_TEV_COMP_BGR24_EQ"; + case GX_TEV_COMP_RGB8_GT: + return "GX_TEV_COMP_RGB8_GT"; + case GX_TEV_COMP_RGB8_EQ: + return "GX_TEV_COMP_RGB8_EQ"; + default: + return fmt::format("GXTevOp({})", underlying(op)); + } +} + +inline std::string format_as(const GXTevColorArg& arg) { + switch (arg) { + case GX_CC_CPREV: + return "GX_CC_CPREV"; + case GX_CC_APREV: + return "GX_CC_APREV"; + case GX_CC_C0: + return "GX_CC_C0"; + case GX_CC_A0: + return "GX_CC_A0"; + case GX_CC_C1: + return "GX_CC_C1"; + case GX_CC_A1: + return "GX_CC_A1"; + case GX_CC_C2: + return "GX_CC_C2"; + case GX_CC_A2: + return "GX_CC_A2"; + case GX_CC_TEXC: + return "GX_CC_TEXC"; + case GX_CC_TEXA: + return "GX_CC_TEXA"; + case GX_CC_RASC: + return "GX_CC_RASC"; + case GX_CC_RASA: + return "GX_CC_RASA"; + case GX_CC_ONE: + return "GX_CC_ONE"; + case GX_CC_HALF: + return "GX_CC_HALF"; + case GX_CC_KONST: + return "GX_CC_KONST"; + case GX_CC_ZERO: + return "GX_CC_ZERO"; + default: + return fmt::format("GXTevColorArg({})", underlying(arg)); + } +} + +inline std::string format_as(const GXTevAlphaArg& arg) { + switch (arg) { + case GX_CA_APREV: + return "GX_CA_APREV"; + case GX_CA_A0: + return "GX_CA_A0"; + case GX_CA_A1: + return "GX_CA_A1"; + case GX_CA_A2: + return "GX_CA_A2"; + case GX_CA_TEXA: + return "GX_CA_TEXA"; + case GX_CA_RASA: + return "GX_CA_RASA"; + case GX_CA_KONST: + return "GX_CA_KONST"; + case GX_CA_ZERO: + return "GX_CA_ZERO"; + default: + return fmt::format("GXTevAlphaArg({})", underlying(arg)); + } +} + +inline std::string format_as(const GXTexGenSrc& src) { + switch (src) { + case GX_TG_POS: + return "GX_TG_POS"; + case GX_TG_NRM: + return "GX_TG_NRM"; + case GX_TG_BINRM: + return "GX_TG_BINRM"; + case GX_TG_TANGENT: + return "GX_TG_TANGENT"; + case GX_TG_TEX0: + return "GX_TG_TEX0"; + case GX_TG_TEX1: + return "GX_TG_TEX1"; + case GX_TG_TEX2: + return "GX_TG_TEX2"; + case GX_TG_TEX3: + return "GX_TG_TEX3"; + case GX_TG_TEX4: + return "GX_TG_TEX4"; + case GX_TG_TEX5: + return "GX_TG_TEX5"; + case GX_TG_TEX6: + return "GX_TG_TEX6"; + case GX_TG_TEX7: + return "GX_TG_TEX7"; + case GX_TG_TEXCOORD0: + return "GX_TG_TEXCOORD0"; + case GX_TG_TEXCOORD1: + return "GX_TG_TEXCOORD1"; + case GX_TG_TEXCOORD2: + return "GX_TG_TEXCOORD2"; + case GX_TG_TEXCOORD3: + return "GX_TG_TEXCOORD3"; + case GX_TG_TEXCOORD4: + return "GX_TG_TEXCOORD4"; + case GX_TG_TEXCOORD5: + return "GX_TG_TEXCOORD5"; + case GX_TG_TEXCOORD6: + return "GX_TG_TEXCOORD6"; + case GX_TG_COLOR0: + return "GX_TG_COLOR0"; + case GX_TG_COLOR1: + return "GX_TG_COLOR1"; + default: + return fmt::format("GXTexGenSrc({})", underlying(src)); + } +} + +inline std::string format_as(const GXTexGenType& type) { + switch (type) { + case GX_TG_MTX2x4: + return "GX_TG_MTX2x4"; + case GX_TG_MTX3x4: + return "GX_TG_MTX3x4"; + case GX_TG_BUMP0: + return "GX_TG_BUMP0"; + case GX_TG_BUMP1: + return "GX_TG_BUMP1"; + case GX_TG_BUMP2: + return "GX_TG_BUMP2"; + case GX_TG_BUMP3: + return "GX_TG_BUMP3"; + case GX_TG_BUMP4: + return "GX_TG_BUMP4"; + case GX_TG_BUMP5: + return "GX_TG_BUMP5"; + case GX_TG_BUMP6: + return "GX_TG_BUMP6"; + case GX_TG_BUMP7: + return "GX_TG_BUMP7"; + case GX_TG_SRTG: + return "GX_TG_SRTG"; + default: + return fmt::format("GXTexGenType({})", underlying(type)); + } +} + +inline std::string format_as(const GXTevBias& bias) { + switch (bias) { + case GX_TB_ZERO: + return "GX_TB_ZERO"; + case GX_TB_ADDHALF: + return "GX_TB_ADDHALF"; + case GX_TB_SUBHALF: + return "GX_TB_SUBHALF"; + default: + return fmt::format("GXTevBias({})", underlying(bias)); + } +} + +inline std::string format_as(const GXTevScale& scale) { + switch (scale) { + case GX_CS_SCALE_1: + return "GX_CS_SCALE_1"; + case GX_CS_SCALE_2: + return "GX_CS_SCALE_2"; + case GX_CS_SCALE_4: + return "GX_CS_SCALE_4"; + case GX_CS_DIVIDE_2: + return "GX_CS_DIVIDE_2"; + default: + return fmt::format("GXTevScale({})", underlying(scale)); + } +} + +inline std::string format_as(const GXTevRegID& reg) { + switch (reg) { + case GX_TEVPREV: + return "GX_TEVPREV"; + case GX_TEVREG0: + return "GX_TEVREG0"; + case GX_TEVREG1: + return "GX_TEVREG1"; + case GX_TEVREG2: + return "GX_TEVREG2"; + default: + return fmt::format("GXTevRegID({})", underlying(reg)); + } +} + +inline std::string format_as(const GXTevKColorSel& sel) { + switch (sel) { + case GX_TEV_KCSEL_8_8: + return "GX_TEV_KCSEL_8_8"; + case GX_TEV_KCSEL_7_8: + return "GX_TEV_KCSEL_7_8"; + case GX_TEV_KCSEL_6_8: + return "GX_TEV_KCSEL_6_8"; + case GX_TEV_KCSEL_5_8: + return "GX_TEV_KCSEL_5_8"; + case GX_TEV_KCSEL_4_8: + return "GX_TEV_KCSEL_4_8"; + case GX_TEV_KCSEL_3_8: + return "GX_TEV_KCSEL_3_8"; + case GX_TEV_KCSEL_2_8: + return "GX_TEV_KCSEL_2_8"; + case GX_TEV_KCSEL_1_8: + return "GX_TEV_KCSEL_1_8"; + case GX_TEV_KCSEL_K0: + return "GX_TEV_KCSEL_K0"; + case GX_TEV_KCSEL_K1: + return "GX_TEV_KCSEL_K1"; + case GX_TEV_KCSEL_K2: + return "GX_TEV_KCSEL_K2"; + case GX_TEV_KCSEL_K3: + return "GX_TEV_KCSEL_K3"; + case GX_TEV_KCSEL_K0_R: + return "GX_TEV_KCSEL_K0_R"; + case GX_TEV_KCSEL_K1_R: + return "GX_TEV_KCSEL_K1_R"; + case GX_TEV_KCSEL_K2_R: + return "GX_TEV_KCSEL_K2_R"; + case GX_TEV_KCSEL_K3_R: + return "GX_TEV_KCSEL_K3_R"; + case GX_TEV_KCSEL_K0_G: + return "GX_TEV_KCSEL_K0_G"; + case GX_TEV_KCSEL_K1_G: + return "GX_TEV_KCSEL_K1_G"; + case GX_TEV_KCSEL_K2_G: + return "GX_TEV_KCSEL_K2_G"; + case GX_TEV_KCSEL_K3_G: + return "GX_TEV_KCSEL_K3_G"; + case GX_TEV_KCSEL_K0_B: + return "GX_TEV_KCSEL_K0_B"; + case GX_TEV_KCSEL_K1_B: + return "GX_TEV_KCSEL_K1_B"; + case GX_TEV_KCSEL_K2_B: + return "GX_TEV_KCSEL_K2_B"; + case GX_TEV_KCSEL_K3_B: + return "GX_TEV_KCSEL_K3_B"; + case GX_TEV_KCSEL_K0_A: + return "GX_TEV_KCSEL_K0_A"; + case GX_TEV_KCSEL_K1_A: + return "GX_TEV_KCSEL_K1_A"; + case GX_TEV_KCSEL_K2_A: + return "GX_TEV_KCSEL_K2_A"; + case GX_TEV_KCSEL_K3_A: + return "GX_TEV_KCSEL_K3_A"; + default: + return fmt::format("GXTevKColorSel({})", underlying(sel)); + } +} + +inline std::string format_as(const GXTevKAlphaSel& sel) { + switch (sel) { + case GX_TEV_KASEL_8_8: + return "GX_TEV_KASEL_8_8"; + case GX_TEV_KASEL_7_8: + return "GX_TEV_KASEL_7_8"; + case GX_TEV_KASEL_6_8: + return "GX_TEV_KASEL_6_8"; + case GX_TEV_KASEL_5_8: + return "GX_TEV_KASEL_5_8"; + case GX_TEV_KASEL_4_8: + return "GX_TEV_KASEL_4_8"; + case GX_TEV_KASEL_3_8: + return "GX_TEV_KASEL_3_8"; + case GX_TEV_KASEL_2_8: + return "GX_TEV_KASEL_2_8"; + case GX_TEV_KASEL_1_8: + return "GX_TEV_KASEL_1_8"; + case GX_TEV_KASEL_K0_R: + return "GX_TEV_KASEL_K0_R"; + case GX_TEV_KASEL_K1_R: + return "GX_TEV_KASEL_K1_R"; + case GX_TEV_KASEL_K2_R: + return "GX_TEV_KASEL_K2_R"; + case GX_TEV_KASEL_K3_R: + return "GX_TEV_KASEL_K3_R"; + case GX_TEV_KASEL_K0_G: + return "GX_TEV_KASEL_K0_G"; + case GX_TEV_KASEL_K1_G: + return "GX_TEV_KASEL_K1_G"; + case GX_TEV_KASEL_K2_G: + return "GX_TEV_KASEL_K2_G"; + case GX_TEV_KASEL_K3_G: + return "GX_TEV_KASEL_K3_G"; + case GX_TEV_KASEL_K0_B: + return "GX_TEV_KASEL_K0_B"; + case GX_TEV_KASEL_K1_B: + return "GX_TEV_KASEL_K1_B"; + case GX_TEV_KASEL_K2_B: + return "GX_TEV_KASEL_K2_B"; + case GX_TEV_KASEL_K3_B: + return "GX_TEV_KASEL_K3_B"; + case GX_TEV_KASEL_K0_A: + return "GX_TEV_KASEL_K0_A"; + case GX_TEV_KASEL_K1_A: + return "GX_TEV_KASEL_K1_A"; + case GX_TEV_KASEL_K2_A: + return "GX_TEV_KASEL_K2_A"; + case GX_TEV_KASEL_K3_A: + return "GX_TEV_KASEL_K3_A"; + default: + return fmt::format("GXTevKAlphaSel({})", underlying(sel)); + } +} + +inline std::string format_as(const GXTevSwapSel& sel) { + switch (sel) { + case GX_TEV_SWAP0: + return "GX_TEV_SWAP0"; + case GX_TEV_SWAP1: + return "GX_TEV_SWAP1"; + case GX_TEV_SWAP2: + return "GX_TEV_SWAP2"; + case GX_TEV_SWAP3: + return "GX_TEV_SWAP3"; + default: + return fmt::format("GXTevSwapSel({})", underlying(sel)); + } +} + +inline std::string format_as(const GXTexMapID& id) { + switch (id) { + case GX_TEXMAP0: + return "GX_TEXMAP0"; + case GX_TEXMAP1: + return "GX_TEXMAP1"; + case GX_TEXMAP2: + return "GX_TEXMAP2"; + case GX_TEXMAP3: + return "GX_TEXMAP3"; + case GX_TEXMAP4: + return "GX_TEXMAP4"; + case GX_TEXMAP5: + return "GX_TEXMAP5"; + case GX_TEXMAP6: + return "GX_TEXMAP6"; + case GX_TEXMAP7: + return "GX_TEXMAP7"; + case GX_TEXMAP_NULL: + return "GX_TEXMAP_NULL"; + case GX_TEX_DISABLE: + return "GX_TEX_DISABLE"; + default: + return fmt::format("GXTexMapID({})", underlying(id)); + } +} + +inline std::string format_as(const GXChannelID& id) { + switch (id) { + case GX_COLOR0: + return "GX_COLOR0"; + case GX_COLOR1: + return "GX_COLOR1"; + case GX_ALPHA0: + return "GX_ALPHA0"; + case GX_ALPHA1: + return "GX_ALPHA1"; + case GX_COLOR0A0: + return "GX_COLOR0A0"; + case GX_COLOR1A1: + return "GX_COLOR1A1"; + case GX_COLOR_ZERO: + return "GX_COLOR_ZERO"; + case GX_ALPHA_BUMP: + return "GX_ALPHA_BUMP"; + case GX_ALPHA_BUMPN: + return "GX_ALPHA_BUMPN"; + case GX_COLOR_NULL: + return "GX_COLOR_NULL"; + default: + return fmt::format("GXChannelID({})", underlying(id)); + } +} + +inline std::string format_as(const GXColorSrc& src) { + switch (src) { + case GX_SRC_REG: + return "GX_SRC_REG"; + case GX_SRC_VTX: + return "GX_SRC_VTX"; + default: + return fmt::format("GXColorSrc({})", underlying(src)); + } +} + +inline std::string format_as(const GXTexMtx& mtx) { + switch (mtx) { + case GX_TEXMTX0: + return "GX_TEXMTX0"; + case GX_TEXMTX1: + return "GX_TEXMTX1"; + case GX_TEXMTX2: + return "GX_TEXMTX2"; + case GX_TEXMTX3: + return "GX_TEXMTX3"; + case GX_TEXMTX4: + return "GX_TEXMTX4"; + case GX_TEXMTX5: + return "GX_TEXMTX5"; + case GX_TEXMTX6: + return "GX_TEXMTX6"; + case GX_TEXMTX7: + return "GX_TEXMTX7"; + case GX_TEXMTX8: + return "GX_TEXMTX8"; + case GX_TEXMTX9: + return "GX_TEXMTX9"; + case GX_IDENTITY: + return "GX_IDENTITY"; + default: + return fmt::format("GXTexMtx({})", underlying(mtx)); + } +} + +inline std::string format_as(const GXPTTexMtx& mtx) { + switch (mtx) { + case GX_PTTEXMTX0: + return "GX_PTTEXMTX0"; + case GX_PTTEXMTX1: + return "GX_PTTEXMTX1"; + case GX_PTTEXMTX2: + return "GX_PTTEXMTX2"; + case GX_PTTEXMTX3: + return "GX_PTTEXMTX3"; + case GX_PTTEXMTX4: + return "GX_PTTEXMTX4"; + case GX_PTTEXMTX5: + return "GX_PTTEXMTX5"; + case GX_PTTEXMTX6: + return "GX_PTTEXMTX6"; + case GX_PTTEXMTX7: + return "GX_PTTEXMTX7"; + case GX_PTTEXMTX8: + return "GX_PTTEXMTX8"; + case GX_PTTEXMTX9: + return "GX_PTTEXMTX9"; + case GX_PTTEXMTX10: + return "GX_PTTEXMTX10"; + case GX_PTTEXMTX11: + return "GX_PTTEXMTX11"; + case GX_PTTEXMTX12: + return "GX_PTTEXMTX12"; + case GX_PTTEXMTX13: + return "GX_PTTEXMTX13"; + case GX_PTTEXMTX14: + return "GX_PTTEXMTX14"; + case GX_PTTEXMTX15: + return "GX_PTTEXMTX15"; + case GX_PTTEXMTX16: + return "GX_PTTEXMTX16"; + case GX_PTTEXMTX17: + return "GX_PTTEXMTX17"; + case GX_PTTEXMTX18: + return "GX_PTTEXMTX18"; + case GX_PTTEXMTX19: + return "GX_PTTEXMTX19"; + case GX_PTIDENTITY: + return "GX_PTIDENTITY"; + default: + return fmt::format("GXPTTexMtx({})", underlying(mtx)); + } +} + +inline std::string format_as(const GXCompare& comp) { + switch (comp) { + case GX_NEVER: + return "GX_NEVER"; + case GX_LESS: + return "GX_LESS"; + case GX_EQUAL: + return "GX_EQUAL"; + case GX_LEQUAL: + return "GX_LEQUAL"; + case GX_GREATER: + return "GX_GREATER"; + case GX_NEQUAL: + return "GX_NEQUAL"; + case GX_GEQUAL: + return "GX_GEQUAL"; + case GX_ALWAYS: + return "GX_ALWAYS"; + default: + return fmt::format("GXCompare({})", underlying(comp)); + } +} + +inline std::string format_as(const GXAlphaOp& op) { + switch (op) { + case GX_AOP_AND: + return "GX_AOP_AND"; + case GX_AOP_OR: + return "GX_AOP_OR"; + case GX_AOP_XOR: + return "GX_AOP_XOR"; + case GX_AOP_XNOR: + return "GX_AOP_XNOR"; + default: + return fmt::format("GXAlphaOp({})", underlying(op)); + } +} + +inline std::string format_as(const GXFogType& type) { + switch (type) { + case GX_FOG_NONE: + return "GX_FOG_NONE"; + case GX_FOG_PERSP_LIN: + return "GX_FOG_PERSP_LIN"; + case GX_FOG_PERSP_EXP: + return "GX_FOG_PERSP_EXP"; + case GX_FOG_PERSP_EXP2: + return "GX_FOG_PERSP_EXP2"; + case GX_FOG_PERSP_REVEXP: + return "GX_FOG_PERSP_REVEXP"; + case GX_FOG_PERSP_REVEXP2: + return "GX_FOG_PERSP_REVEXP2"; + case GX_FOG_ORTHO_LIN: + return "GX_FOG_ORTHO_LIN"; + case GX_FOG_ORTHO_EXP: + return "GX_FOG_ORTHO_EXP"; + case GX_FOG_ORTHO_EXP2: + return "GX_FOG_ORTHO_EXP2"; + case GX_FOG_ORTHO_REVEXP: + return "GX_FOG_ORTHO_REVEXP"; + case GX_FOG_ORTHO_REVEXP2: + return "GX_FOG_ORTHO_REVEXP2"; + default: + return fmt::format("GXFogType({})", underlying(type)); + } +} + +inline std::string format_as(const GXTexCoordID& id) { + switch (id) { + case GX_TEXCOORD0: + return "GX_TEXCOORD0"; + case GX_TEXCOORD1: + return "GX_TEXCOORD1"; + case GX_TEXCOORD2: + return "GX_TEXCOORD2"; + case GX_TEXCOORD3: + return "GX_TEXCOORD3"; + case GX_TEXCOORD4: + return "GX_TEXCOORD4"; + case GX_TEXCOORD5: + return "GX_TEXCOORD5"; + case GX_TEXCOORD6: + return "GX_TEXCOORD6"; + case GX_TEXCOORD7: + return "GX_TEXCOORD7"; + case GX_TEXCOORD_NULL: + return "GX_TEXCOORD_NULL"; + default: + return fmt::format("GXTexCoordID({})", underlying(id)); + } +} + +inline std::string format_as(const GXIndTexStageID& id) { + switch (id) { + case GX_INDTEXSTAGE0: + return "GX_INDTEXSTAGE0"; + case GX_INDTEXSTAGE1: + return "GX_INDTEXSTAGE1"; + case GX_INDTEXSTAGE2: + return "GX_INDTEXSTAGE2"; + case GX_INDTEXSTAGE3: + return "GX_INDTEXSTAGE3"; + default: + return fmt::format("GXIndTexStageID({})", underlying(id)); + } +} + +inline std::string format_as(const GXIndTexFormat& fmt_) { + switch (fmt_) { + case GX_ITF_8: + return "GX_ITF_8"; + case GX_ITF_5: + return "GX_ITF_5"; + case GX_ITF_4: + return "GX_ITF_4"; + case GX_ITF_3: + return "GX_ITF_3"; + default: + return fmt::format("GXIndTexFormat({})", underlying(fmt_)); + } +} + +inline std::string format_as(const GXIndTexBiasSel& sel) { + switch (sel) { + case GX_ITB_NONE: + return "GX_ITB_NONE"; + case GX_ITB_S: + return "GX_ITB_S"; + case GX_ITB_T: + return "GX_ITB_T"; + case GX_ITB_ST: + return "GX_ITB_ST"; + case GX_ITB_U: + return "GX_ITB_U"; + case GX_ITB_SU: + return "GX_ITB_SU"; + case GX_ITB_TU: + return "GX_ITB_TU"; + case GX_ITB_STU: + return "GX_ITB_STU"; + default: + return fmt::format("GXIndTexBiasSel({})", underlying(sel)); + } +} + +inline std::string format_as(const GXIndTexAlphaSel& sel) { + switch (sel) { + case GX_ITBA_OFF: + return "GX_ITBA_OFF"; + case GX_ITBA_S: + return "GX_ITBA_S"; + case GX_ITBA_T: + return "GX_ITBA_T"; + case GX_ITBA_U: + return "GX_ITBA_U"; + default: + return fmt::format("GXIndTexAlphaSel({})", underlying(sel)); + } +} + +inline std::string format_as(const GXIndTexMtxID& id) { + switch (id) { + case GX_ITM_OFF: + return "GX_ITM_OFF"; + case GX_ITM_0: + return "GX_ITM_0"; + case GX_ITM_1: + return "GX_ITM_1"; + case GX_ITM_2: + return "GX_ITM_2"; + case GX_ITM_S0: + return "GX_ITM_S0"; + case GX_ITM_S1: + return "GX_ITM_S1"; + case GX_ITM_S2: + return "GX_ITM_S2"; + case GX_ITM_T0: + return "GX_ITM_T0"; + case GX_ITM_T1: + return "GX_ITM_T1"; + case GX_ITM_T2: + return "GX_ITM_T2"; + default: + return fmt::format("GXIndTexMtxID({})", underlying(id)); + } +} + +inline std::string format_as(const GXIndTexWrap& wrap) { + switch (wrap) { + case GX_ITW_OFF: + return "GX_ITW_OFF"; + case GX_ITW_256: + return "GX_ITW_256"; + case GX_ITW_128: + return "GX_ITW_128"; + case GX_ITW_64: + return "GX_ITW_64"; + case GX_ITW_32: + return "GX_ITW_32"; + case GX_ITW_16: + return "GX_ITW_16"; + case GX_ITW_0: + return "GX_ITW_0"; + default: + return fmt::format("GXIndTexWrap({})", underlying(wrap)); + } +} + +inline std::string format_as(const GXIndTexScale& scale) { + switch (scale) { + case GX_ITS_1: + return "GX_ITS_1"; + case GX_ITS_2: + return "GX_ITS_2"; + case GX_ITS_4: + return "GX_ITS_4"; + case GX_ITS_8: + return "GX_ITS_8"; + case GX_ITS_16: + return "GX_ITS_16"; + case GX_ITS_32: + return "GX_ITS_32"; + case GX_ITS_64: + return "GX_ITS_64"; + case GX_ITS_128: + return "GX_ITS_128"; + case GX_ITS_256: + return "GX_ITS_256"; + default: + return fmt::format("GXIndTexScale({})", underlying(scale)); + } +} + +inline std::string format_as(const GXPrimitive& prim) { + switch (prim) { + case GX_QUADS: + return "GX_QUADS"; + case GX_TRIANGLES: + return "GX_TRIANGLES"; + case GX_TRIANGLESTRIP: + return "GX_TRIANGLESTRIP"; + case GX_TRIANGLEFAN: + return "GX_TRIANGLEFAN"; + case GX_LINES: + return "GX_LINES"; + case GX_LINESTRIP: + return "GX_LINESTRIP"; + case GX_POINTS: + return "GX_POINTS"; + default: + return fmt::format("GXPrimitive({})", underlying(prim)); + } +} + +inline std::string format_as(const GXAttr& attr) { + switch (attr) { + case GX_VA_PNMTXIDX: + return "GX_VA_PNMTXIDX"; + case GX_VA_TEX0MTXIDX: + return "GX_VA_TEX0MTXIDX"; + case GX_VA_TEX1MTXIDX: + return "GX_VA_TEX1MTXIDX"; + case GX_VA_TEX2MTXIDX: + return "GX_VA_TEX2MTXIDX"; + case GX_VA_TEX3MTXIDX: + return "GX_VA_TEX3MTXIDX"; + case GX_VA_TEX4MTXIDX: + return "GX_VA_TEX4MTXIDX"; + case GX_VA_TEX5MTXIDX: + return "GX_VA_TEX5MTXIDX"; + case GX_VA_TEX6MTXIDX: + return "GX_VA_TEX6MTXIDX"; + case GX_VA_TEX7MTXIDX: + return "GX_VA_TEX7MTXIDX"; + case GX_VA_POS: + return "GX_VA_POS"; + case GX_VA_NRM: + return "GX_VA_NRM"; + case GX_VA_CLR0: + return "GX_VA_CLR0"; + case GX_VA_CLR1: + return "GX_VA_CLR1"; + case GX_VA_TEX0: + return "GX_VA_TEX0"; + case GX_VA_TEX1: + return "GX_VA_TEX1"; + case GX_VA_TEX2: + return "GX_VA_TEX2"; + case GX_VA_TEX3: + return "GX_VA_TEX3"; + case GX_VA_TEX4: + return "GX_VA_TEX4"; + case GX_VA_TEX5: + return "GX_VA_TEX5"; + case GX_VA_TEX6: + return "GX_VA_TEX6"; + case GX_VA_TEX7: + return "GX_VA_TEX7"; + case GX_POS_MTX_ARRAY: + return "GX_POS_MTX_ARRAY"; + case GX_NRM_MTX_ARRAY: + return "GX_NRM_MTX_ARRAY"; + case GX_TEX_MTX_ARRAY: + return "GX_TEX_MTX_ARRAY"; + case GX_LIGHT_ARRAY: + return "GX_LIGHT_ARRAY"; + case GX_VA_NBT: + return "GX_VA_NBT"; + case GX_VA_NULL: + return "GX_VA_NULL"; + default: + return fmt::format("GXAttr({})", underlying(attr)); + } +} + +inline std::string format_as(const GXCompCnt& cnt) { + switch (cnt) { + case GX_POS_XY: + return "GX_POS_XY|GX_NRM_XYZ|GX_CLR_RGB|GX_TEX_S"; + case GX_POS_XYZ: + return "GX_POS_XYZ|GX_NRM_NBT|GX_CLR_RGBA|GX_TEX_ST"; + case GX_NRM_NBT3: + return "GX_NRM_NBT3"; + default: + return fmt::format("GXCompCnt({})", underlying(cnt)); + } +} + +inline std::string format_as(const GXCompType& type) { + switch (type) { + case GX_U8: + return "GX_U8|GX_RGB565"; + case GX_S8: + return "GX_S8|GX_RGB8"; + case GX_U16: + return "GX_U16|GX_RGBX8"; + case GX_S16: + return "GX_S16|GX_RGBA4"; + case GX_F32: + return "GX_F32|GX_RGBA6"; + case GX_RGBA8: + return "GX_RGBA8"; + default: + return fmt::format("GXCompType({})", underlying(type)); + } +} + +inline std::string format_as(const GXAttrType& type) { + switch (type) { + case GX_NONE: + return "GX_NONE"; + case GX_DIRECT: + return "GX_DIRECT"; + case GX_INDEX8: + return "GX_INDEX8"; + case GX_INDEX16: + return "GX_INDEX16"; + default: + return fmt::format("GXAttrType({})", underlying(type)); + } +} + +inline std::string format_as(const GXVtxFmt& fmt) { + switch (fmt) { + case GX_VTXFMT0: + return "GX_VTXFMT0"; + case GX_VTXFMT1: + return "GX_VTXFMT1"; + case GX_VTXFMT2: + return "GX_VTXFMT2"; + case GX_VTXFMT3: + return "GX_VTXFMT3"; + case GX_VTXFMT4: + return "GX_VTXFMT4"; + case GX_VTXFMT5: + return "GX_VTXFMT5"; + case GX_VTXFMT6: + return "GX_VTXFMT6"; + case GX_VTXFMT7: + return "GX_VTXFMT7"; + default: + return fmt::format("GXVtxFmt({})", underlying(fmt)); + } +} + +inline std::string format_as(const GXTexFmt& fmt) { + switch (fmt) { + case GX_TF_I4: + return "GX_TF_I4"; + case GX_TF_I8: + return "GX_TF_I8"; + case GX_TF_IA4: + return "GX_TF_IA4"; + case GX_TF_IA8: + return "GX_TF_IA8"; + case GX_TF_RGB565: + return "GX_TF_RGB565"; + case GX_TF_RGB5A3: + return "GX_TF_RGB5A3"; + case GX_TF_RGBA8: + return "GX_TF_RGBA8"; + case GX_TF_CMPR: + return "GX_TF_CMPR"; + case GX_CTF_R4: + return "GX_CTF_R4"; + case GX_CTF_RA4: + return "GX_CTF_RA4"; + case GX_CTF_RA8: + return "GX_CTF_RA8"; + case GX_CTF_YUVA8: + return "GX_CTF_YUVA8"; + case GX_CTF_A8: + return "GX_CTF_A8"; + case GX_CTF_R8: + return "GX_CTF_R8"; + case GX_CTF_G8: + return "GX_CTF_G8"; + case GX_CTF_B8: + return "GX_CTF_B8"; + case GX_CTF_RG8: + return "GX_CTF_RG8"; + case GX_CTF_GB8: + return "GX_CTF_GB8"; + case GX_TF_Z8: + return "GX_TF_Z8"; + case GX_TF_Z16: + return "GX_TF_Z16"; + case GX_TF_Z24X8: + return "GX_TF_Z24X8"; + case GX_CTF_Z4: + return "GX_CTF_Z4"; + case GX_CTF_Z8M: + return "GX_CTF_Z8M"; + case GX_CTF_Z8L: + return "GX_CTF_Z8L"; + case GX_CTF_Z16L: + return "GX_CTF_Z16L"; + case GX_TF_R8_PC: + return "GX_TF_R8_PC"; + case GX_TF_RGBA8_PC: + return "GX_TF_RGBA8_PC"; + default: + return fmt::format("GXTexFmt({})", underlying(fmt)); + } +} + +inline std::string format_as(const GXTlutFmt& fmt) { + switch (fmt) { + case GX_TL_IA8: + return "GX_TL_IA8"; + case GX_TL_RGB565: + return "GX_TL_RGB565"; + case GX_TL_RGB5A3: + return "GX_TL_RGB5A3"; + default: + return fmt::format("GXTlutFmt({})", underlying(fmt)); + } +} diff --git a/aurora-main/lib/gx/pipeline.cpp b/aurora-main/lib/gx/pipeline.cpp new file mode 100644 index 0000000..9b1571e --- /dev/null +++ b/aurora-main/lib/gx/pipeline.cpp @@ -0,0 +1,96 @@ +#include "pipeline.hpp" + +#include "../webgpu/gpu.hpp" +#include "gx_fmt.hpp" +#include "shader_info.hpp" +#include "tracy/Tracy.hpp" + +#include +#include +#include + +#include + +namespace aurora::gx { +static Module Log("aurora::gx"); + +namespace { +struct ShaderConfigHash { + size_t operator()(const ShaderConfig& config) const noexcept { return static_cast(xxh3_hash(config)); } +}; + +struct CachedShaderModule { + std::condition_variable ready; + wgpu::ShaderModule module; + bool compiling = true; +}; + +std::mutex sShaderModuleCacheMutex; +absl::flat_hash_map, ShaderConfigHash> sShaderModuleCache; + +wgpu::ShaderModule cached_shader_module(const ShaderConfig& config) { + std::shared_ptr entry; + { + std::unique_lock lock{sShaderModuleCacheMutex}; + const auto it = sShaderModuleCache.find(config); + if (it == sShaderModuleCache.end()) { + entry = std::make_shared(); + sShaderModuleCache.emplace(config, entry); + } else { + entry = it->second; + entry->ready.wait(lock, [&] { return !entry->compiling; }); + return entry->module; + } + } + + auto module = build_shader(config); + { + std::lock_guard lock{sShaderModuleCacheMutex}; + entry->module = module; + entry->compiling = false; + } + entry->ready.notify_all(); + return module; +} +} // namespace + +wgpu::RenderPipeline create_pipeline(const PipelineConfig& config) { + ZoneScoped; + const auto shader = cached_shader_module(config.shaderConfig); + return build_pipeline(config, {}, shader, "GX Pipeline"); +} + +void clear_shader_module_cache() { + std::lock_guard lock{sShaderModuleCacheMutex}; + sShaderModuleCache.clear(); +} + +void render(const DrawData& data, const wgpu::RenderPassEncoder& pass, DrawEncodeState& state, + bool requireReadyPipeline, const gfx::Range* uniformRangeOverride) { + if (!gfx::bind_pipeline(data.pipeline, pass, state.currentPipeline, requireReadyPipeline)) { + return; + } + + // An interpolated presentation slot re-encodes the identical draw with only this range replaced; overriding here avoids copying the whole DrawData per draw per slot. + const gfx::Range& uniformRange = uniformRangeOverride != nullptr ? *uniformRangeOverride : data.uniformRange; + const std::array offsets{uniformRange.offset}; + pass.SetBindGroup(1, gfx::g_uniformBindGroup, offsets.size(), offsets.data()); + // Resolved when the draw was recorded; see GXBindGroups. + if (data.bindGroups.resolvedTextureBindGroup != nullptr && + data.bindGroups.resolvedTextureBindGroup != state.boundTextureBindGroup) { + wgpuRenderPassEncoderSetBindGroup(pass.Get(), 2, data.bindGroups.resolvedTextureBindGroup, 0, nullptr); + state.boundTextureBindGroup = data.bindGroups.resolvedTextureBindGroup; + } + if (data.dstAlpha != UINT32_MAX) { + const wgpu::Color color{0.f, 0.f, 0.f, data.dstAlpha / 255.f}; + pass.SetBlendConstant(&color); + } + if (!state.indexBufferBound) { + // Bound once for the pass; draws select their range with firstIndex below. + pass.SetIndexBuffer(gfx::g_indexBuffer, wgpu::IndexFormat::Uint16, 0, wgpu::kWholeSize); + state.indexBufferBound = true; + } + pass.DrawIndexed(data.indexCount, data.instanceCount, + static_cast(data.idxRange.offset / sizeof(uint16_t))); +} +} // namespace aurora::gx diff --git a/aurora-main/lib/gx/pipeline.hpp b/aurora-main/lib/gx/pipeline.hpp new file mode 100644 index 0000000..92a9e4d --- /dev/null +++ b/aurora-main/lib/gx/pipeline.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include "../gfx/common.hpp" +#include "shader_info.hpp" + +namespace aurora::gx { +struct DrawData { + gfx::PipelineRef pipeline; + gfx::Range vertRange; + gfx::Range idxRange; + gfx::Range uniformRange; + std::array interpolatedUniformRanges; + uint32_t vtxCount; + uint32_t indexCount; + uint32_t instanceCount; + GXBindGroups bindGroups; + uint32_t dstAlpha; +}; + +constexpr uint32_t GXPipelineConfigVersion = 19; + +constexpr GXFogType effective_pipeline_fog_type(GXFogType fogType, GXZTexOp zTextureOp, + bool zCompLocBeforeTex, GXBlendMode blendMode, + GXLogicOp logicOp) noexcept { + const bool usesLateZTexture = zTextureOp != GX_ZT_DISABLE && !zCompLocBeforeTex; + const bool usesUnsupportedLogicFog = + blendMode == GX_BM_LOGIC && logicOp != GX_LO_OR; + return usesLateZTexture && usesUnsupportedLogicFog ? GX_FOG_NONE : fogType; +} + +struct PipelineConfig { + uint32_t version = GXPipelineConfigVersion; + uint32_t msaaSamples = 1; + ShaderConfig shaderConfig; + GXCompare depthFunc; + GXCullMode cullMode; + GXBlendMode blendMode; + GXBlendFactor blendFacSrc, blendFacDst; + GXLogicOp blendOp; + GXPixelFmt pixelFmt; + uint32_t dstAlpha; + bool depthCompare, depthUpdate, alphaUpdate, colorUpdate; +}; +static_assert(std::has_unique_object_representations_v); + +inline bool valid_pipeline_config(const PipelineConfig& config) noexcept { + const auto in_range = [](auto value, auto maximum) { + using Value = decltype(value); + return static_cast>(value) >= 0 && + static_cast>(value) <= + static_cast>(maximum); + }; + const bool validSamples = config.msaaSamples == 1 || config.msaaSamples == 2 || + config.msaaSamples == 4 || config.msaaSamples == 8; + return config.version == GXPipelineConfigVersion && validSamples && + in_range(config.depthFunc, GX_ALWAYS) && in_range(config.cullMode, GX_CULL_ALL) && + in_range(config.blendMode, GX_BM_SUBTRACT) && + in_range(config.blendFacSrc, GX_BL_INVDSTALPHA) && + in_range(config.blendFacDst, GX_BL_INVDSTALPHA) && + in_range(config.blendOp, GX_LO_SET) && in_range(config.pixelFmt, GX_PF_YUV420); +} + +wgpu::RenderPipeline create_pipeline([[maybe_unused]] const PipelineConfig& config); +void clear_shader_module_cache(); + +// Per-pass encoder state carried across the draws of one render pass, so the replay loop can elide Dawn calls that would re-bind what is already bound. +struct DrawEncodeState { + gfx::PipelineRef currentPipeline = UINTPTR_MAX; + // The bind group currently occupying slot 2. + WGPUBindGroup boundTextureBindGroup = nullptr; + // The pass-wide index buffer binding is established lazily by the first indexed draw; every draw then addresses it with firstIndex instead of a per-draw SetIndexBuffer. + bool indexBufferBound = false; +}; + +void render(const DrawData& data, const wgpu::RenderPassEncoder& pass, DrawEncodeState& state, + bool requireReadyPipeline, const gfx::Range* uniformRangeOverride = nullptr); + +void queue_surface(const u8* dlStart, uint32_t dlSize, bool bigEndian) noexcept; +} // namespace aurora::gx diff --git a/aurora-main/lib/gx/shader.cpp b/aurora-main/lib/gx/shader.cpp new file mode 100644 index 0000000..4806f1f --- /dev/null +++ b/aurora-main/lib/gx/shader.cpp @@ -0,0 +1,2064 @@ +#include "../gfx/common.hpp" + +#include "../internal.hpp" +#include "../webgpu/gpu.hpp" +#include "gx.hpp" +#include "gx_fmt.hpp" +#include "shader_info.hpp" + +#include + +#include +#include +#include +#include + +#include "tracy/Tracy.hpp" + +namespace aurora::gx { +using namespace fmt::literals; +using namespace std::string_literals; +using namespace std::string_view_literals; + +static Module Log("aurora::gfx::gx"); + + +static inline std::string_view chan_comp(GXTevColorChan chan) noexcept { + switch (chan) { + case GX_CH_RED: + return "r"; + case GX_CH_GREEN: + return "g"; + case GX_CH_BLUE: + return "b"; + case GX_CH_ALPHA: + return "a"; + default: + return "?"; + } +} + +static bool is_alpha_bump_channel(GXChannelID id) noexcept { return id == GX_ALPHA_BUMP || id == GX_ALPHA_BUMPN; } + +static std::string_view disabled_texture_color(const ShaderConfig& config) noexcept { + return config.numTexGens == 0 ? "vec3f(0.0)"sv : "vec3f(1.0)"sv; +} + +static std::string_view disabled_texture_alpha(const ShaderConfig& config) noexcept { + return config.numTexGens == 0 ? "0.0"sv : "1.0"sv; +} + +static std::string tev_mask_expr(const std::string& value, u32 mask) { + // t_IndTexCoord is already expanded into the 0..255 indirect sample domain. + return fmt::format("(f32(u32({}) & 0x{:X}u) / 255.0)", value, mask); +} + +static std::string alpha_bump_sel(size_t stageIdx, const ShaderConfig& config, const TevStage& stage) { + if (stage.indTexStage >= config.numIndStages || stage.indTexAlphaSel == GX_ITBA_OFF) { + return "0.0"; + } + + std::string baseCoord; + switch (stage.indTexAlphaSel) { + DEFAULT_FATAL("invalid indTexAlphaSel {} for stage {}", underlying(stage.indTexAlphaSel), stageIdx); + case GX_ITBA_S: + baseCoord = fmt::format("t_IndTexCoord{}.x", underlying(stage.indTexStage)); + break; + case GX_ITBA_T: + baseCoord = fmt::format("t_IndTexCoord{}.y", underlying(stage.indTexStage)); + break; + case GX_ITBA_U: + baseCoord = fmt::format("t_IndTexCoord{}.z", underlying(stage.indTexStage)); + break; + case GX_ITBA_OFF: + return "0.0"; + } + + switch (stage.indTexFormat) { + DEFAULT_FATAL("invalid indirect format {} for stage {}", underlying(stage.indTexFormat), stageIdx); + case GX_ITF_8: + return tev_mask_expr(baseCoord, 0xF8u); + case GX_ITF_5: + return tev_mask_expr(baseCoord, 0xE0u); + case GX_ITF_4: + return tev_mask_expr(baseCoord, 0xF0u); + case GX_ITF_3: + return tev_mask_expr(baseCoord, 0xF8u); + } +} + +u8 color_channel(GXChannelID id) noexcept { + switch (id) { + DEFAULT_FATAL("unimplemented color channel {}", id); + case GX_COLOR0: + case GX_ALPHA0: + case GX_COLOR0A0: + return 0; + case GX_COLOR1: + case GX_ALPHA1: + case GX_COLOR1A1: + return 1; + } +} + +static std::string color_arg_reg(GXTevColorArg arg, size_t stageIdx, const ShaderConfig& config, + const TevStage& stage) { + switch (arg) { + DEFAULT_FATAL("invalid color arg {}", underlying(arg)); + case GX_CC_CPREV: + return "prev.rgb"; + case GX_CC_APREV: + return "vec3f(prev.a)"; + case GX_CC_C0: + return "tevreg0.rgb"; + case GX_CC_A0: + return "vec3f(tevreg0.a)"; + case GX_CC_C1: + return "tevreg1.rgb"; + case GX_CC_A1: + return "vec3f(tevreg1.a)"; + case GX_CC_C2: + return "tevreg2.rgb"; + case GX_CC_A2: + return "vec3f(tevreg2.a)"; + case GX_CC_TEXC: { + if (!tev_stage_texture_dependency(config, static_cast(stageIdx)).canSampleTexture) { + return std::string(disabled_texture_color(config)); + } + const auto& swap = config.tevSwapTable[stage.tevSwapTex]; + return fmt::format("sampled{}.{}{}{}", stageIdx, chan_comp(swap.red), chan_comp(swap.green), chan_comp(swap.blue)); + } + case GX_CC_TEXA: { + if (!tev_stage_texture_dependency(config, static_cast(stageIdx)).canSampleTexture) { + return fmt::format("vec3f({})", disabled_texture_alpha(config)); + } + const auto& swap = config.tevSwapTable[stage.tevSwapTex]; + return fmt::format("vec3f(sampled{}.{})", stageIdx, chan_comp(swap.alpha)); + } + case GX_CC_RASC: { + // CHECK(stage.channelId != GX_COLOR_NULL, "unmapped color channel for stage {}", stageIdx); + if (stage.channelId == GX_COLOR_ZERO || stage.channelId == GX_COLOR_NULL) { + return "vec3f(0.0)"; + } + if (is_alpha_bump_channel(stage.channelId)) { + std::string alpha = alpha_bump_sel(stageIdx, config, stage); + if (stage.channelId == GX_ALPHA_BUMPN) { + alpha = fmt::format("({} * (255.0 / 248.0))", alpha); + } + return fmt::format("vec3f({})", alpha); + } + u32 idx = color_channel(stage.channelId); + const auto& swap = config.tevSwapTable[stage.tevSwapRas]; + return fmt::format("rast{}.{}{}{}", idx, chan_comp(swap.red), chan_comp(swap.green), chan_comp(swap.blue)); + } + case GX_CC_RASA: { + // CHECK(stage.channelId != GX_COLOR_NULL, "unmapped color channel for stage {}", stageIdx); + if (stage.channelId == GX_COLOR_ZERO || stage.channelId == GX_COLOR_NULL) { + return "vec3f(0.0)"; + } + if (is_alpha_bump_channel(stage.channelId)) { + std::string alpha = alpha_bump_sel(stageIdx, config, stage); + if (stage.channelId == GX_ALPHA_BUMPN) { + alpha = fmt::format("({} * (255.0 / 248.0))", alpha); + } + return fmt::format("vec3f({})", alpha); + } + u32 idx = color_channel(stage.channelId); + const auto& swap = config.tevSwapTable[stage.tevSwapRas]; + return fmt::format("vec3f(rast{}.{})", idx, chan_comp(swap.alpha)); + } + case GX_CC_ONE: + return "vec3f(1.0)"; + case GX_CC_HALF: + return "vec3f(0.5)"; + case GX_CC_KONST: { + switch (stage.kcSel) { + DEFAULT_FATAL("invalid kcSel {}", underlying(stage.kcSel)); + case GX_TEV_KCSEL_8_8: + return "vec3f(1.0)"; + case GX_TEV_KCSEL_7_8: + return "vec3f(7.0/8.0)"; + case GX_TEV_KCSEL_6_8: + return "vec3f(6.0/8.0)"; + case GX_TEV_KCSEL_5_8: + return "vec3f(5.0/8.0)"; + case GX_TEV_KCSEL_4_8: + return "vec3f(4.0/8.0)"; + case GX_TEV_KCSEL_3_8: + return "vec3f(3.0/8.0)"; + case GX_TEV_KCSEL_2_8: + return "vec3f(2.0/8.0)"; + case GX_TEV_KCSEL_1_8: + return "vec3f(1.0/8.0)"; + case GX_TEV_KCSEL_K0: + return "ubuf.kcolor0.rgb"; + case GX_TEV_KCSEL_K1: + return "ubuf.kcolor1.rgb"; + case GX_TEV_KCSEL_K2: + return "ubuf.kcolor2.rgb"; + case GX_TEV_KCSEL_K3: + return "ubuf.kcolor3.rgb"; + case GX_TEV_KCSEL_K0_R: + return "vec3f(ubuf.kcolor0.r)"; + case GX_TEV_KCSEL_K1_R: + return "vec3f(ubuf.kcolor1.r)"; + case GX_TEV_KCSEL_K2_R: + return "vec3f(ubuf.kcolor2.r)"; + case GX_TEV_KCSEL_K3_R: + return "vec3f(ubuf.kcolor3.r)"; + case GX_TEV_KCSEL_K0_G: + return "vec3f(ubuf.kcolor0.g)"; + case GX_TEV_KCSEL_K1_G: + return "vec3f(ubuf.kcolor1.g)"; + case GX_TEV_KCSEL_K2_G: + return "vec3f(ubuf.kcolor2.g)"; + case GX_TEV_KCSEL_K3_G: + return "vec3f(ubuf.kcolor3.g)"; + case GX_TEV_KCSEL_K0_B: + return "vec3f(ubuf.kcolor0.b)"; + case GX_TEV_KCSEL_K1_B: + return "vec3f(ubuf.kcolor1.b)"; + case GX_TEV_KCSEL_K2_B: + return "vec3f(ubuf.kcolor2.b)"; + case GX_TEV_KCSEL_K3_B: + return "vec3f(ubuf.kcolor3.b)"; + case GX_TEV_KCSEL_K0_A: + return "vec3f(ubuf.kcolor0.a)"; + case GX_TEV_KCSEL_K1_A: + return "vec3f(ubuf.kcolor1.a)"; + case GX_TEV_KCSEL_K2_A: + return "vec3f(ubuf.kcolor2.a)"; + case GX_TEV_KCSEL_K3_A: + return "vec3f(ubuf.kcolor3.a)"; + } + } + case GX_CC_ZERO: + return "vec3f(0.0)"; + } +} + +static std::string alpha_arg_reg(GXTevAlphaArg arg, size_t stageIdx, const ShaderConfig& config, + const TevStage& stage) { + switch (arg) { + DEFAULT_FATAL("invalid alpha arg {}", underlying(arg)); + case GX_CA_APREV: + return "prev.a"; + case GX_CA_A0: + return "tevreg0.a"; + case GX_CA_A1: + return "tevreg1.a"; + case GX_CA_A2: + return "tevreg2.a"; + case GX_CA_TEXA: { + if (!tev_stage_texture_dependency(config, static_cast(stageIdx)).canSampleTexture) { + return std::string(disabled_texture_alpha(config)); + } + const auto& swap = config.tevSwapTable[stage.tevSwapTex]; + return fmt::format("sampled{}.{}", stageIdx, chan_comp(swap.alpha)); + } + case GX_CA_RASA: { + // CHECK(stage.channelId != GX_COLOR_NULL, "unmapped color channel for stage {}", stageIdx); + if (stage.channelId == GX_COLOR_ZERO || stage.channelId == GX_COLOR_NULL) { + return "0.0"; + } + if (is_alpha_bump_channel(stage.channelId)) { + std::string alpha = alpha_bump_sel(stageIdx, config, stage); + if (stage.channelId == GX_ALPHA_BUMPN) { + alpha = fmt::format("({} * (255.0 / 248.0))", alpha); + } + return alpha; + } + u32 idx = color_channel(stage.channelId); + const auto& swap = config.tevSwapTable[stage.tevSwapRas]; + return fmt::format("rast{}.{}", idx, chan_comp(swap.alpha)); + } + case GX_CA_KONST: { + switch (stage.kaSel) { + DEFAULT_FATAL("invalid kaSel {}", underlying(stage.kaSel)); + case GX_TEV_KASEL_8_8: + return "1.0"; + case GX_TEV_KASEL_7_8: + return "(7.0/8.0)"; + case GX_TEV_KASEL_6_8: + return "(6.0/8.0)"; + case GX_TEV_KASEL_5_8: + return "(5.0/8.0)"; + case GX_TEV_KASEL_4_8: + return "(4.0/8.0)"; + case GX_TEV_KASEL_3_8: + return "(3.0/8.0)"; + case GX_TEV_KASEL_2_8: + return "(2.0/8.0)"; + case GX_TEV_KASEL_1_8: + return "(1.0/8.0)"; + case GX_TEV_KASEL_K0_R: + return "ubuf.kcolor0.r"; + case GX_TEV_KASEL_K1_R: + return "ubuf.kcolor1.r"; + case GX_TEV_KASEL_K2_R: + return "ubuf.kcolor2.r"; + case GX_TEV_KASEL_K3_R: + return "ubuf.kcolor3.r"; + case GX_TEV_KASEL_K0_G: + return "ubuf.kcolor0.g"; + case GX_TEV_KASEL_K1_G: + return "ubuf.kcolor1.g"; + case GX_TEV_KASEL_K2_G: + return "ubuf.kcolor2.g"; + case GX_TEV_KASEL_K3_G: + return "ubuf.kcolor3.g"; + case GX_TEV_KASEL_K0_B: + return "ubuf.kcolor0.b"; + case GX_TEV_KASEL_K1_B: + return "ubuf.kcolor1.b"; + case GX_TEV_KASEL_K2_B: + return "ubuf.kcolor2.b"; + case GX_TEV_KASEL_K3_B: + return "ubuf.kcolor3.b"; + case GX_TEV_KASEL_K0_A: + return "ubuf.kcolor0.a"; + case GX_TEV_KASEL_K1_A: + return "ubuf.kcolor1.a"; + case GX_TEV_KASEL_K2_A: + return "ubuf.kcolor2.a"; + case GX_TEV_KASEL_K3_A: + return "ubuf.kcolor3.a"; + } + } + case GX_CA_ZERO: + return "0.0"; + } +} + +static std::string tev_bias_i32(GXTevBias bias) { + switch (bias) { + DEFAULT_FATAL("invalid tev bias {}", underlying(bias)); + case GX_TB_ZERO: + return "0"; + case GX_TB_ADDHALF: + return "128"; + case GX_TB_SUBHALF: + return "-128"; + } +} + +static std::string tev_scale_index(GXTevScale scale) { + switch (scale) { + DEFAULT_FATAL("invalid tev scale {}", underlying(scale)); + case GX_CS_SCALE_1: + return "0u"; + case GX_CS_SCALE_2: + return "1u"; + case GX_CS_SCALE_4: + return "2u"; + case GX_CS_DIVIDE_2: + return "3u"; + } +} + +static std::string tev_regular_op(GXTevOp op, GXTevBias bias, GXTevScale scale, std::string_view a, + std::string_view b, std::string_view c, std::string_view d, + std::string_view suffix) { + CHECK(op == GX_TEV_ADD || op == GX_TEV_SUB, "invalid regular tev op {}", underlying(op)); + return fmt::format("tev_regular_{4}({0}, {1}, {2}, {3}, {5}, {6}, {7})", a, b, c, d, suffix, + tev_bias_i32(bias), tev_scale_index(scale), op == GX_TEV_SUB ? "true" : "false"); +} + +static std::string tev_op(GXTevOp op, GXTevBias bias, GXTevScale scale, std::string_view a, std::string_view b, + std::string_view c, std::string_view d, std::string_view zero, + std::string_view suffix) { + switch (op) { + DEFAULT_FATAL("unimplemented tev op {}", underlying(op)); + case GX_TEV_ADD: + case GX_TEV_SUB: + return tev_regular_op(op, bias, scale, a, b, c, d, suffix); + case GX_TEV_COMP_R8_GT: + return fmt::format("select({3}, {2}, round({0}.r * 255.0) > round({1}.r * 255.0)) + {4}", a, b, c, zero, d); + case GX_TEV_COMP_R8_EQ: + return fmt::format("select({3}, {2}, round({0}.r * 255.0) == round({1}.r * 255.0)) + {4}", a, b, c, zero, d); + case GX_TEV_COMP_GR16_GT: + return fmt::format( + "select({3}, {2}, round(dot({0}.rg * 255.0, vec2(1.0, 256.0))) > round(dot({1}.rg * 255.0, vec2(1.0, 256.0))))" + " + {4}", + a, b, c, zero, d); + case GX_TEV_COMP_GR16_EQ: + return fmt::format( + "select({3}, {2}, round(dot({0}.rg * 255.0, vec2(1.0, 256.0))) == round(dot({1}.rg * 255.0, vec2(1.0, 256.0))))" + " + {4}", + a, b, c, zero, d); + case GX_TEV_COMP_BGR24_GT: + return fmt::format( + "select({3}, {2}, round(dot({0}.rgb * 255.0, vec3(1.0, 256.0, 65536.0))) > round(dot({1}.rgb * 255.0, " + "vec3(1.0, 256.0, 65536.0)))) + {4}", + a, b, c, zero, d); + case GX_TEV_COMP_BGR24_EQ: + return fmt::format( + "select({3}, {2}, round(dot({0}.rgb * 255.0, vec3(1.0, 256.0, 65536.0))) == round(dot({1}.rgb * 255.0, " + "vec3(1.0, 256.0, 65536.0)))) + {4}", + a, b, c, zero, d); + case GX_TEV_COMP_RGB8_GT: + return fmt::format("select({3}, {2}, round({0} * 255.0) > round({1} * 255.0)) + {4}", a, b, c, zero, d); + case GX_TEV_COMP_RGB8_EQ: + return fmt::format("select({3}, {2}, round({0} * 255.0) == round({1} * 255.0)) + {4}", a, b, c, zero, d); + } +} + +static std::string tev_color_op(GXTevOp op, GXTevBias bias, GXTevScale scale, bool clamp, + std::string_view a, std::string_view b, std::string_view c, std::string_view d) { + const auto overflow = [](std::string_view reg) { return fmt::format("tev_overflow_vec3f({})", reg); }; + std::string expr = tev_op(op, bias, scale, overflow(a), overflow(b), overflow(c), d, "vec3(0)"sv, "vec3f"sv); + return clamp ? fmt::format("clamp({}, vec3f(0.0), vec3f(1.0))", expr) + : fmt::format("clamp({}, vec3f(-1024.0 / 255.0), vec3f(1023.0 / 255.0))", expr); +} + +static bool alpha_compare_uses_color_inputs(GXTevOp op) noexcept { + switch (op) { + case GX_TEV_COMP_R8_GT: + case GX_TEV_COMP_R8_EQ: + case GX_TEV_COMP_GR16_GT: + case GX_TEV_COMP_GR16_EQ: + case GX_TEV_COMP_BGR24_GT: + case GX_TEV_COMP_BGR24_EQ: + return true; + default: + return false; + } +} + +static std::string tev_alpha_op(GXTevOp op, GXTevBias bias, GXTevScale scale, bool clamp, + std::string_view a, std::string_view b, std::string_view c, std::string_view d, + std::string_view colorA, std::string_view colorB) { + const auto scalarOverflow = [](std::string_view reg) { return fmt::format("tev_overflow_f32({})", reg); }; + std::string expr; + if (alpha_compare_uses_color_inputs(op)) { + const auto colorOverflow = [](std::string_view reg) { return fmt::format("tev_overflow_vec3f({})", reg); }; + // GX alpha compares R8/GR16/BGR24 using the color combiner's A/B inputs, while C and D remain the scalar alpha-combiner inputs. + expr = tev_op(op, bias, scale, colorOverflow(colorA), colorOverflow(colorB), scalarOverflow(c), d, "0.0"sv, + "f32"sv); + } else { + // The numeric RGB8 op aliases are the alpha combiner's A8 compares and therefore continue to compare scalar alpha A/B inputs here. + expr = tev_op(op, bias, scale, scalarOverflow(a), scalarOverflow(b), scalarOverflow(c), d, "0.0"sv, "f32"sv); + } + return clamp ? fmt::format("clamp({}, 0.0, 1.0)", expr) : fmt::format("clamp({}, -1024.0 / 255.0, 1023.0 / 255.0)", expr); +} + +struct AlphaCompareExpr { + std::string expr; + int constant = -1; +}; + +static AlphaCompareExpr alpha_compare_const(bool value) { return {value ? "true"s : "false"s, value ? 1 : 0}; } + +static AlphaCompareExpr alpha_compare_not(const AlphaCompareExpr& expr) { + if (expr.constant != -1) { + return alpha_compare_const(expr.constant == 0); + } + return {fmt::format("!{}", expr.expr), -1}; +} + +static AlphaCompareExpr alpha_compare_and(const AlphaCompareExpr& lhs, const AlphaCompareExpr& rhs) { + if (lhs.constant == 0 || rhs.constant == 0) { + return alpha_compare_const(false); + } + if (lhs.constant == 1) { + return rhs; + } + if (rhs.constant == 1) { + return lhs; + } + return {fmt::format("({} && {})", lhs.expr, rhs.expr), -1}; +} + +static AlphaCompareExpr alpha_compare_or(const AlphaCompareExpr& lhs, const AlphaCompareExpr& rhs) { + if (lhs.constant == 1 || rhs.constant == 1) { + return alpha_compare_const(true); + } + if (lhs.constant == 0) { + return rhs; + } + if (rhs.constant == 0) { + return lhs; + } + return {fmt::format("({} || {})", lhs.expr, rhs.expr), -1}; +} + +static AlphaCompareExpr alpha_compare_xor(const AlphaCompareExpr& lhs, const AlphaCompareExpr& rhs) { + if (lhs.constant != -1 && rhs.constant != -1) { + return alpha_compare_const(lhs.constant != rhs.constant); + } + if (lhs.constant == 0) { + return rhs; + } + if (rhs.constant == 0) { + return lhs; + } + if (lhs.constant == 1) { + return alpha_compare_not(rhs); + } + if (rhs.constant == 1) { + return alpha_compare_not(lhs); + } + return {fmt::format("({} != {})", lhs.expr, rhs.expr), -1}; +} + +static AlphaCompareExpr alpha_compare_xnor(const AlphaCompareExpr& lhs, const AlphaCompareExpr& rhs) { + if (lhs.constant != -1 && rhs.constant != -1) { + return alpha_compare_const(lhs.constant == rhs.constant); + } + if (lhs.constant == 0) { + return alpha_compare_not(rhs); + } + if (rhs.constant == 0) { + return alpha_compare_not(lhs); + } + if (lhs.constant == 1) { + return rhs; + } + if (rhs.constant == 1) { + return lhs; + } + return {fmt::format("({} == {})", lhs.expr, rhs.expr), -1}; +} + +static AlphaCompareExpr alpha_compare(GXCompare comp, u8 ref) { + const auto iref = static_cast(ref); + switch (comp) { + default: + return alpha_compare_const(true); + case GX_NEVER: + return alpha_compare_const(false); + case GX_LESS: + if (ref == 0) { + return alpha_compare_const(false); + } + return {fmt::format("(alphaCompare < {}u)", iref), -1}; + case GX_LEQUAL: + if (ref == 255) { + return alpha_compare_const(true); + } + return {fmt::format("(alphaCompare <= {}u)", iref), -1}; + case GX_EQUAL: + return {fmt::format("(alphaCompare == {}u)", iref), -1}; + case GX_NEQUAL: + return {fmt::format("(alphaCompare != {}u)", iref), -1}; + case GX_GEQUAL: + if (ref == 0) { + return alpha_compare_const(true); + } + return {fmt::format("(alphaCompare >= {}u)", iref), -1}; + case GX_GREATER: + if (ref == 255) { + return alpha_compare_const(false); + } + return {fmt::format("(alphaCompare > {}u)", iref), -1}; + case GX_ALWAYS: + return alpha_compare_const(true); + } +} + +static inline std::string vtx_attr(const ShaderConfig& config, GXAttr attr) { + const auto type = config.attrs[attr].attrType; + if (type == GX_NONE) { + if (attr == GX_VA_PNMTXIDX) { + return "ubuf.current_pnmtx"; + } + if (attr == GX_VA_NRM) { + // Default normal + return "vec3f(1.0, 0.0, 0.0)"s; + } + if (attr == GX_VA_CLR0 || attr == GX_VA_CLR1) { + return "vec4f(1.0)"s; + } + if (attr >= GX_VA_TEX0 && attr <= GX_VA_TEX7) { + return "vec2f(0.0, 0.0)"s; + } + UNLIKELY FATAL("unmapped vtx attr {}", underlying(attr)); + } + if (attr == GX_VA_POS) { + return "in_pos"s; + } + if (attr == GX_VA_NRM) { + return "in_nrm"s; + } + if (attr == GX_VA_CLR0 || attr == GX_VA_CLR1) { + const auto idx = attr - GX_VA_CLR0; + return fmt::format("in_clr{}", idx); + } + if (attr >= GX_VA_TEX0 && attr <= GX_VA_TEX7) { + const auto idx = attr - GX_VA_TEX0; + return fmt::format("in_tex{}_uv", idx); + } + if (attr == GX_VA_PNMTXIDX) { + return "in_pnmtxidx"s; + } + if (attr >= GX_VA_TEX0MTXIDX && attr <= GX_VA_TEX7MTXIDX) { + const auto idx = attr - GX_VA_TEX0MTXIDX; + return fmt::format("in_texmtxidx{}", idx); + } + UNLIKELY FATAL("unhandled vtx attr {}", underlying(attr)); +} + +static std::string vertex_color_attr(const ShaderConfig& config, u32 channel) { + const int attr = shader_vertex_color_attr(config, channel); + return attr >= 0 ? vtx_attr(config, static_cast(attr)) : "vec4f(1.0)"s; +} + +constexpr std::array TevColorArgNames{ + "CPREV"sv, "APREV"sv, "C0"sv, "A0"sv, "C1"sv, "A1"sv, "C2"sv, "A2"sv, + "TEXC"sv, "TEXA"sv, "RASC"sv, "RASA"sv, "ONE"sv, "HALF"sv, "KONST"sv, "ZERO"sv, +}; +constexpr std::array TevAlphaArgNames{ + "APREV"sv, "A0"sv, "A1"sv, "A2"sv, "TEXA"sv, "RASA"sv, "KONST"sv, "ZERO"sv, +}; + +auto fetch_attr(const AttrConfig& mapping, std::string_view buf, std::string_view offs, bool le, u8 cntOverride = 0) -> std::string { + const u8 cnt = cntOverride != 0 ? cntOverride : mapping.cnt; + switch (mapping.compType) { + case GX_U8: + return fmt::format("fetch_u8_{}(&{}, {}, {}, {})", cnt, buf, offs, mapping.frac, le); + case GX_S8: + return fmt::format("fetch_s8_{}(&{}, {}, {}, {})", cnt, buf, offs, mapping.frac, le); + case GX_U16: + return fmt::format("fetch_u16_{}(&{}, {}, {}, {})", cnt, buf, offs, mapping.frac, le); + case GX_S16: + return fmt::format("fetch_s16_{}(&{}, {}, {}, {})", cnt, buf, offs, mapping.frac, le); + case GX_F32: + return fmt::format("fetch_f32_{}(&{}, {}, {})", cnt, buf, offs, le); + case GX_RGBA8: + return fmt::format("unpack4x8unorm(load_u32_raw(&{}, {}))", buf, offs); + default: + Log.fatal("fetch_attr: Unimplemented {}", static_cast(mapping.compType)); + } +} + +auto fetch_color_attr(const AttrConfig& mapping, std::string_view buf, std::string_view offs, bool le) -> std::string { + switch (mapping.compType) { + case GX_RGB565: + return fmt::format("fetch_rgb565(&{}, {}, {})", buf, offs, le); + case GX_RGB8: + return fmt::format("fetch_rgb8(&{}, {}, {})", buf, offs, le); + case GX_RGBX8: + return fmt::format("fetch_rgbx8(&{}, {}, {})", buf, offs, le); + case GX_RGBA4: + return fmt::format("fetch_rgba4(&{}, {}, {})", buf, offs, le); + case GX_RGBA6: + return fmt::format("fetch_rgba6(&{}, {}, {})", buf, offs, le); + case GX_RGBA8: + return fmt::format("fetch_rgba8(&{}, {}, {})", buf, offs, le); + default: + Log.fatal("fetch_color_attr: Unimplemented {}", static_cast(mapping.compType)); + } +} + +auto attr_load(const ShaderConfig& config, GXAttr attr, std::string_view vidx) -> std::string; + +static u32 attr_comp_type_size(u8 compType) noexcept { + switch (static_cast(compType)) { + case GX_U8: + case GX_S8: + return 1; + case GX_U16: + case GX_S16: + return 2; + case GX_F32: + return 4; + default: + Log.fatal("attr_comp_type_size: Unimplemented {}", static_cast(compType)); + } +} + +static std::string offset_plus(std::string_view offs, u32 add) { + if (add == 0) { + return std::string(offs); + } + return fmt::format("({} + {}u)", offs, add); +} + +auto normal_group_load(const ShaderConfig& config, u8 group, std::string_view vidx) -> std::string { + const auto& mapping = config.attrs[GX_VA_NRM]; + if (mapping.attrType == GX_NONE || mapping.cnt != 9) { + return attr_load(config, GX_VA_NRM, vidx); + } + + auto buf = "vbuf"sv; + auto offs = fmt::format("ubuf.vtx_start + {} * {}u + {}u", vidx, config.vtxStride, mapping.offset); + auto le = false; // Vertex buffer is always big endian (for now) + const bool indexedNbt3 = + mapping.nrmIndexCount == 3 && (mapping.attrType == GX_INDEX8 || mapping.attrType == GX_INDEX16); + const u32 groupOffset = indexedNbt3 ? 0 : group * 3u * attr_comp_type_size(mapping.compType); + + if (mapping.attrType == GX_INDEX8) { + const std::string indexOffs = offset_plus(offs, mapping.nrmIndexCount == 3 ? group : 0); + offs = fmt::format("ubuf.array_start[{}] + raw_fetch_u8_1(&{}, {}) * {}u + {}u", GX_VA_NRM - GX_VA_POS, buf, + indexOffs, mapping.stride, groupOffset); + buf = "abuf"sv; + le = mapping.le; + } else if (mapping.attrType == GX_INDEX16) { + const std::string indexOffs = offset_plus(offs, mapping.nrmIndexCount == 3 ? group * 2u : 0); + offs = fmt::format("ubuf.array_start[{}] + raw_fetch_u16_1(&{}, {}, {}) * {}u + {}u", GX_VA_NRM - GX_VA_POS, + buf, indexOffs, le, mapping.stride, groupOffset); + buf = "abuf"sv; + le = mapping.le; + } else { + offs = offset_plus(offs, groupOffset); + } + + return fetch_attr(mapping, buf, offs, le, 3); +} + +auto attr_load(const ShaderConfig& config, GXAttr attr, std::string_view vidx) -> std::string { + const auto& mapping = config.attrs[attr]; + if (mapping.attrType == GX_NONE) { + return vtx_attr(config, attr); + } + auto buf = "vbuf"sv; + auto offs = fmt::format("ubuf.vtx_start + {} * {}u + {}u", vidx, config.vtxStride, mapping.offset); + auto le = false; // Vertex buffer is always big endian (for now) + if (mapping.attrType == GX_INDEX8) { + offs = fmt::format("ubuf.array_start[{}] + raw_fetch_u8_1(&{}, {}) * {}u", attr - GX_VA_POS, buf, offs, + mapping.stride); + buf = "abuf"sv; + le = mapping.le; + } else if (mapping.attrType == GX_INDEX16) { + offs = fmt::format("ubuf.array_start[{}] + raw_fetch_u16_1(&{}, {}, {}) * {}u", attr - GX_VA_POS, buf, offs, le, + mapping.stride); + buf = "abuf"sv; + le = mapping.le; + } + switch (attr) { + case GX_VA_PNMTXIDX: + return fmt::format("(raw_fetch_u8_1(&{}, {}) / 3u)", buf, offs); + case GX_VA_TEX0MTXIDX: + case GX_VA_TEX1MTXIDX: + case GX_VA_TEX2MTXIDX: + case GX_VA_TEX3MTXIDX: + case GX_VA_TEX4MTXIDX: + case GX_VA_TEX5MTXIDX: + case GX_VA_TEX6MTXIDX: + case GX_VA_TEX7MTXIDX: + return fmt::format("raw_fetch_u8_1(&{}, {})", buf, offs); + case GX_VA_POS: { + const auto posLoad = fetch_attr(mapping, buf, offs, le); + if (mapping.cnt == 2) { + return fmt::format("vec3f({}, 0.0)", posLoad); + } + return posLoad; + } + case GX_VA_NRM: + return fetch_attr(mapping, buf, offs, le, mapping.cnt == 9 ? 3 : 0); + case GX_VA_CLR0: + case GX_VA_CLR1: + return fetch_color_attr(mapping, buf, offs, le); + case GX_VA_TEX0: + case GX_VA_TEX1: + case GX_VA_TEX2: + case GX_VA_TEX3: + case GX_VA_TEX4: + case GX_VA_TEX5: + case GX_VA_TEX6: + case GX_VA_TEX7: { + const auto texLoad = fetch_attr(mapping, buf, offs, le); + if (mapping.cnt == 1) { + return fmt::format("vec2f({}, 0.0)", texLoad); + } + return texLoad; + } + default: + Log.fatal("attr_load: Unimplemented {}", attr); + } +} + +auto lighting_func(const ShaderConfig& config, const ColorChannelConfig& cc, u8 i, bool alpha) -> std::string { + std::string_view swizzle = alpha ? ".a"sv : ".rgb"sv; + std::string_view intType = alpha ? "i32"sv : "vec3i"sv; + std::string_view floatType = alpha ? "f32"sv : "vec3f"sv; + std::string_view intZero = alpha ? "0"sv : "vec3i(0)"sv; + std::string_view intMax = alpha ? "255"sv : "vec3i(255)"sv; + std::string_view shift7 = alpha ? "7u"sv : "vec3u(7u)"sv; + std::string_view shift8 = alpha ? "8u"sv : "vec3u(8u)"sv; + std::string outVar; + std::string_view posVar; + if (UsePerPixelLighting) { + outVar = fmt::format("rast{}", i); + posVar = "in.mv_pos"sv; + } else { + outVar = fmt::format("out.cc{}", i); + posVar = "mv_pos"sv; + } + std::string ambSrc, matSrc; + if (cc.ambSrc == GX_SRC_VTX) { + if (UsePerPixelLighting) { + ambSrc = fmt::format("in.clr{}", i); + } else { + ambSrc = vertex_color_attr(config, i); + } + } else if (cc.ambSrc == GX_SRC_REG) { + ambSrc = fmt::format("ubuf.cc{0}{1}_amb", i, alpha ? "a"sv : ""sv); + } + if (cc.matSrc == GX_SRC_VTX) { + if (UsePerPixelLighting) { + matSrc = fmt::format("in.clr{}", i); + } else { + matSrc = vertex_color_attr(config, i); + } + } else if (cc.matSrc == GX_SRC_REG) { + matSrc = fmt::format("ubuf.cc{0}{1}_mat", i, alpha ? "a"sv : ""sv); + } + if (!cc.lightingEnabled) { + // The XF lighting block consumes and produces 8-bit channel values even when lighting is disabled. + if (alpha) { + return fmt::format("\n {0}.a = f32(i32(round({1}.a * 255.0))) / 255.0;", outVar, matSrc); + } + return fmt::format("\n {0} = vec4f(vec3f(vec3i(round({1}.rgb * 255.0))) / 255.0, 1.0);", outVar, + matSrc); + } + GXDiffuseFn diffFn = cc.diffFn; + std::string lightAttnFn; + if (cc.attnFn == GX_AF_NONE) { + lightAttnFn = "attn = 1.0;"s; + } else if (cc.attnFn == GX_AF_SPOT) { + lightAttnFn = fmt::format(R"""( + var cosine = max(0.0, dot(ldir, light.dir)); + var cos_attn = dot(light.cos_att, vec3f(1.0, cosine, cosine * cosine)); + var dist_attn = dot(light.dist_att, vec3f(1.0, dist, dist2)); + attn = max(0.0, cos_attn / dist_attn);)"""); + } else if (cc.attnFn == GX_AF_SPEC) { + std::string_view normal = UsePerPixelLighting ? "in.mv_nrm"sv : "mv_nrm"sv; + std::string dist_attn = diffFn != GX_DF_NONE + ? "dot(normalize(light.dist_att), vec3f(1.0, attn, attn * attn))" + : "dot(light.dist_att, vec3f(1.0, attn, attn * attn))"; + lightAttnFn = fmt::format(R"""( + attn = select(0.0, max(0.0, dot({0}, light.dir)), dot({0}, ldir) >= 0.0); + var cos_attn = dot(light.cos_att, vec3f(1.0, attn, attn * attn)); + var dist_attn = {1}; + attn = select(select(0.0, 1.0, cos_attn > 0.0), max(0.0, cos_attn / dist_attn), dist_attn != 0.0);)""", + normal, dist_attn); + } + std::string_view normal = UsePerPixelLighting ? "in.mv_nrm"sv : "mv_nrm"sv; + std::string lightDirSetup; + if (cc.attnFn == GX_AF_NONE) { + lightDirSetup = fmt::format(R"""( + var ldir = light.pos - {0}; + var dist2 = dot(ldir, ldir); + var dist = sqrt(dist2); + ldir = select({1}, ldir / max(dist, 1e-20), dist > 0.0);)""", + posVar, normal); + } else { + lightDirSetup = fmt::format(R"""( + var ldir = light.pos - {0}; + var dist2 = dot(ldir, ldir); + var dist = sqrt(dist2); + ldir = ldir / dist;)""", + posVar); + } + std::string_view lightDiffFn; + if (diffFn == GX_DF_NONE) { + lightDiffFn = "1.0"sv; + } else if (diffFn == GX_DF_SIGN) { + if (UsePerPixelLighting) { + lightDiffFn = "dot(ldir, in.mv_nrm)"sv; + } else { + lightDiffFn = "dot(ldir, mv_nrm)"sv; + } + } else if (diffFn == GX_DF_CLAMP) { + if (UsePerPixelLighting) { + lightDiffFn = "max(0.0, dot(ldir, in.mv_nrm))"sv; + } else { + lightDiffFn = "max(0.0, dot(ldir, mv_nrm))"sv; + } + } + const auto outputTarget = alpha ? fmt::format("{}.a", outVar) : outVar; + const auto outputValue = + alpha + ? fmt::format("f32((material * (lacc + (lacc >> {}))) >> {}) / 255.0", shift7, shift8) + : fmt::format("vec4f(vec3f((material * (lacc + (lacc >> {}))) >> {}) / 255.0, 1.0)", shift7, + shift8); + return fmt::format(R"""( + {{ + var lighting = {11}(round({5}{8} * 255.0)); + for (var i = 0u; i < {1}u; i++) {{ + if ((ubuf.lightState{0}{9} & (1u << i)) == 0u) {{ continue; }} + var light = ubuf.lights[i];{10} + var attn: f32;{2} + var diff = {3}; + lighting = lighting + {11}(round(attn * diff * light.color{8} * 255.0)); + }} + let lacc = clamp(lighting, {13}, {14}); + let material = {11}(round({4}{8} * 255.0)); + {15} = {16}; + }})""", + i, GX::MaxLights, lightAttnFn, lightDiffFn, matSrc, ambSrc, posVar, outVar, swizzle, + alpha ? "a"sv : ""sv, lightDirSetup, intType, floatType, intZero, intMax, outputTarget, + outputValue); +} + +wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept { + ZoneScoped; + const auto hash = xxh3_hash(config); + const auto info = build_shader_info(config); + + std::string uniformPre; + std::string uniBufAttrs; + std::string texBindings; + std::string vtxOutAttrs; + std::string vtxInAttrs; + std::string vtxXfrAttrsPre; + std::string vtxXfrAttrs; + size_t vtxOutIdx = 0; + + // Load points for line/point expansion + std::string_view vidxAttr = "vidx"sv; + if (config.lineMode != 0) { + vtxInAttrs += ",\n @builtin(instance_index) iidx: u32"; + uniBufAttrs += + "\n line_width: f32," + "\n line_aspect_y: f32," + "\n line_tex_offset: f32," + "\n line_texcoord_mask: u32,"; + if (config.lineMode == 3) { + // GX_POINTS: each instance = one vertex, expand to quad + vtxXfrAttrsPre += fmt::format( + "\n let in_vidx = iidx;" + "\n let in_pos = {};" + "\n let in_pnmtxidx = {};" + "\n let mv_pos = vec4f(in_pos, 1.0) * ubuf.postex_mtx[in_pnmtxidx];", + attr_load(config, GX_VA_POS, "in_vidx"sv), attr_load(config, GX_VA_PNMTXIDX, "in_vidx"sv)); + } else { + // GX_LINES / GX_LINESTRIP: each instance = two vertices, expand to quad + vtxXfrAttrsPre += fmt::format( + "\n let use_b = vidx >= 2u;" + "\n let vidx_a = iidx * {}u;" + "\n let vidx_b = vidx_a + 1u;" + "\n let in_vidx = select(vidx_a, vidx_b, use_b);" + "\n let pos_a = {};" + "\n let pos_b = {};" + "\n let in_pos = select(pos_a, pos_b, use_b);" + "\n let pnmtxidx_a = {};" + "\n let pnmtxidx_b = {};" + "\n let in_pnmtxidx = select(pnmtxidx_a, pnmtxidx_b, use_b);" + "\n let mv_pos_a = vec4f(pos_a, 1.0) * ubuf.postex_mtx[pnmtxidx_a];" + "\n let mv_pos_b = vec4f(pos_b, 1.0) * ubuf.postex_mtx[pnmtxidx_b];" + "\n let mv_pos = select(mv_pos_a, mv_pos_b, use_b);", + config.lineMode == 1 ? 2 : 1, attr_load(config, GX_VA_POS, "vidx_a"sv), + attr_load(config, GX_VA_POS, "vidx_b"sv), attr_load(config, GX_VA_PNMTXIDX, "vidx_a"sv), + attr_load(config, GX_VA_PNMTXIDX, "vidx_b"sv)); + } + vidxAttr = "in_vidx"sv; + } else if (config.attrs[GX_VA_PNMTXIDX].attrType == GX_NONE) { + vtxXfrAttrsPre += "\n let in_pnmtxidx = ubuf.current_pnmtx;"; + } + + // Load vertex attributes + for (GXAttr attr = GX_VA_PNMTXIDX; attr <= GX_VA_TEX7; attr = static_cast(attr + 1)) { + const auto attrType = config.attrs[attr].attrType; + if (attrType == GX_NONE) { + continue; + } + // in_pnmtxidx and in_pos written above for line mode + if ((attr != GX_VA_PNMTXIDX && attr != GX_VA_POS) || config.lineMode == 0) { + vtxXfrAttrsPre += fmt::format("\n let {} = {};", vtx_attr(config, attr), attr_load(config, attr, vidxAttr)); + } + } + + if (config.lineMode == 0) { + vtxXfrAttrsPre += fmt::format( + "\n let mv_pos = vec4f({}, 1.0) * ubuf.postex_mtx[in_pnmtxidx];" + "\n out.pos = vec4f(mv_pos, 1.0) * ubuf.proj;", + vtx_attr(config, GX_VA_POS)); + } else if (config.lineMode == 3) { + // GX_POINTS: expand single vertex to axis-aligned screen-space square + vtxXfrAttrsPre += + "\n let clip = vec4f(mv_pos, 1.0) * ubuf.proj;" + "\n let viewport_scale = ubuf.render_viewport_size / max(ubuf.logical_viewport_size, vec2f(1.0));" + "\n let point_size = ubuf.line_width * min(viewport_scale.x, viewport_scale.y);" + "\n let x_sign = select(-1.0, 1.0, (vidx & 1u) != 0u);" + "\n let y_sign = select(-1.0, 1.0, vidx >= 2u);" + "\n let offset_px = vec2f(x_sign, y_sign) * (point_size / 2.0);" + "\n let offset_ndc = (offset_px * 2.0) / ubuf.render_viewport_size;" + "\n out.pos = vec4f(clip.xy + offset_ndc * clip.w, clip.zw);"; + } else { + // GX_LINES / GX_LINESTRIP: expand line segment perpendicular to direction + vtxXfrAttrsPre += + "\n let clip_a = vec4f(mv_pos_a, 1.0) * ubuf.proj;" + "\n let clip_b = vec4f(mv_pos_b, 1.0) * ubuf.proj;" + "\n let ndc_a = clip_a.xy / clip_a.w;" + "\n let ndc_b = clip_b.xy / clip_b.w;" + "\n let viewport_scale = ubuf.render_viewport_size / max(ubuf.logical_viewport_size, vec2f(1.0));" + "\n let delta_px = (ndc_b - ndc_a) / 2.0 * ubuf.render_viewport_size;" + "\n let dir_px = select(vec2f(1.0, 0.0), normalize(delta_px), dot(delta_px, delta_px) > 1e-10);" + "\n let perp_px = vec2f(-dir_px.y, dir_px.x);" + "\n let line_width = ubuf.line_width * min(viewport_scale.x, viewport_scale.y);" + "\n let offset_px = perp_px * (line_width / 2.0) * select(-1.0, 1.0, (vidx & 1u) != 0u);" + "\n let offset_ndc = (offset_px * 2.0) / ubuf.render_viewport_size;" + "\n let clip_base = select(clip_a, clip_b, use_b);" + "\n out.pos = vec4f(clip_base.xy + offset_ndc * clip_base.w, clip_base.zw);"; + } + if constexpr (UseReversedZ) { + vtxXfrAttrsPre += "\n out.pos.z = -out.pos.z;"; + } else { + vtxXfrAttrsPre += "\n out.pos.z += out.pos.w;"; + } + // GX rasterizes at a 7/12 pixel center when antialiasing is disabled, while WebGPU rasterizes at 1/2. + vtxXfrAttrsPre += + "\n let gx_pixel_center_correction = " + "vec2f(-1.0, 1.0) / (6.0 * max(abs(ubuf.render_viewport_size), vec2f(1.0)));" + "\n out.pos = vec4f(out.pos.xy + out.pos.w * gx_pixel_center_correction, out.pos.zw);"; + vtxXfrAttrsPre += fmt::format( + "\n let nrm_tmp = vec4f({}, 0.0) * ubuf.nrm_mtx[in_pnmtxidx];" + "\n let mv_nrm = select(nrm_tmp, normalize(nrm_tmp), dot(nrm_tmp, nrm_tmp) > 1e-10);", + vtx_attr(config, GX_VA_NRM)); + + uniBufAttrs += "\n proj: mat4x4f,"; + // Only the matrix slots this shader can read are uploaded, in compacted order; see UniformMatrixLayout. + uniBufAttrs += fmt::format("\n postex_mtx: array,", info.matrixLayout.postexCount); + uniBufAttrs += fmt::format("\n nrm_mtx: array,", info.matrixLayout.nrmCount); + std::string fragmentFnPre; + std::string fragmentFn; + const int zTexStage = tev_z_texture_stage(config); + const bool usesZTextureDepth = tev_z_texture_enabled(config); + std::array emittedFixedTexCoord{}; + const auto fixed_texcoord_name = [&](u32 texCoordId) { + if (!emittedFixedTexCoord[texCoordId]) { + fragmentFnPre += fmt::format( + "\n let tex{0}_fixed_uv = vec2i(tex{0}_uv * ubuf.texcoord{0}_scale.xy * 128.0);", texCoordId); + emittedFixedTexCoord[texCoordId] = true; + } + return fmt::format("tex{}_fixed_uv", texCoordId); + }; + + static std::array regName{"prev"sv, "tevreg0"sv, "tevreg1"sv, "tevreg2"sv}; + for (u32 idx = 0; idx < config.tevStageCount; ++idx) { + const auto& stage = config.tevStages[idx]; + std::string colorA = color_arg_reg(stage.colorPass.a, idx, config, stage); + std::string colorB = color_arg_reg(stage.colorPass.b, idx, config, stage); + fragmentFn += fmt::format("\n // TEV stage {}", idx); + if (alpha_compare_uses_color_inputs(stage.alphaOp.op)) { + const std::string colorAName = fmt::format("tev_stage{}_color_a", idx); + const std::string colorBName = fmt::format("tev_stage{}_color_b", idx); + // The color result is emitted before the alpha result below. + fragmentFn += fmt::format("\n let {0} = {1};\n let {2} = {3};", colorAName, colorA, colorBName, colorB); + colorA = colorAName; + colorB = colorBName; + } + { + std::string_view outReg = regName[stage.colorOp.outReg]; + std::string op = tev_color_op( + stage.colorOp.op, stage.colorOp.bias, stage.colorOp.scale, stage.colorOp.clamp, colorA, colorB, + color_arg_reg(stage.colorPass.c, idx, config, stage), color_arg_reg(stage.colorPass.d, idx, config, stage)); + fragmentFn += fmt::format("\n {0} = vec4f({1}, {0}.a);", outReg, op); + } + { + std::string_view outReg = regName[stage.alphaOp.outReg]; + std::string op = tev_alpha_op( + stage.alphaOp.op, stage.alphaOp.bias, stage.alphaOp.scale, stage.alphaOp.clamp, + alpha_arg_reg(stage.alphaPass.a, idx, config, stage), alpha_arg_reg(stage.alphaPass.b, idx, config, stage), + alpha_arg_reg(stage.alphaPass.c, idx, config, stage), alpha_arg_reg(stage.alphaPass.d, idx, config, stage), + colorA, colorB); + fragmentFn += fmt::format("\n {0}.a = {1};", outReg, op); + } + } + + { + const auto& lastStage = config.tevStages[config.tevStageCount - 1]; + if (lastStage.colorOp.outReg != 0) { + fragmentFn += fmt::format("\n prev = vec4f({0}.rgb, prev.a);", regName[lastStage.colorOp.outReg]); + } + if (lastStage.alphaOp.outReg != 0) { + fragmentFn += fmt::format("\n prev.a = {0}.a;", regName[lastStage.alphaOp.outReg]); + } + } + + const auto loadsTevRegs = info.loadsTevRegRgb | info.loadsTevRegAlpha; + const auto writesTevRegs = info.writesTevRegRgb | info.writesTevRegAlpha; + if (loadsTevRegs.test(0)) { + uniBufAttrs += "\n tevprev: vec4f,"; + fragmentFnPre += "\n var prev = ubuf.tevprev;"; + } else { + fragmentFnPre += "\n var prev: vec4f;"; + } + for (int i = 1 /* Skip TEVPREV */; i < loadsTevRegs.size(); ++i) { + if (loadsTevRegs.test(i)) { + uniBufAttrs += fmt::format("\n tevreg{}: vec4f,", i - 1); + fragmentFnPre += fmt::format("\n var tevreg{0} = ubuf.tevreg{0};", i - 1); + } else if (writesTevRegs.test(i)) { + fragmentFnPre += fmt::format("\n var tevreg{0}: vec4f;", i - 1); + } + } + + if (info.lightingEnabled) { + uniBufAttrs += fmt::format(FMT_STRING(R"""( + lights: array, + lightState0: u32, + lightState1: u32, + lightState0a: u32, + lightState1a: u32,)"""), + GX::MaxLights); + uniformPre += + "\n" + "struct Light {\n" + " pos: vec3f,\n" + " dir: vec3f,\n" + " color: vec4f,\n" + " cos_att: vec3f,\n" + " dist_att: vec3f,\n" + "};"; + if (UsePerPixelLighting) { + vtxOutAttrs += fmt::format("\n @location({}) mv_pos: vec3f,", vtxOutIdx++); + vtxOutAttrs += fmt::format("\n @location({}) mv_nrm: vec3f,", vtxOutIdx++); + vtxXfrAttrs += fmt::format(FMT_STRING(R"""( + out.mv_pos = mv_pos; + out.mv_nrm = mv_nrm;)""")); + } + } + + for (int i = 0; i < info.sampledColorChannels.size(); ++i) { + if (!info.sampledColorChannels.test(i)) { + continue; + } + + const auto& cc = config.colorChannels[i]; + const auto& cca = config.colorChannels[i + GX_ALPHA0]; + if (cc.lightingEnabled && cc.ambSrc == GX_SRC_REG) { + uniBufAttrs += fmt::format("\n cc{0}_amb: vec4f,", i); + } + if (cc.matSrc == GX_SRC_REG) { + uniBufAttrs += fmt::format("\n cc{0}_mat: vec4f,", i); + } + if (cca.lightingEnabled && cca.ambSrc == GX_SRC_REG) { + uniBufAttrs += fmt::format("\n cc{0}a_amb: vec4f,", i); + } + if (cca.matSrc == GX_SRC_REG) { + uniBufAttrs += fmt::format("\n cc{0}a_mat: vec4f,", i); + } + + // Output vertex color if necessary + if (UsePerPixelLighting) { + if ((cc.lightingEnabled && cc.ambSrc == GX_SRC_VTX) || cc.matSrc == GX_SRC_VTX || + (cca.lightingEnabled && cca.ambSrc == GX_SRC_VTX) || cca.matSrc == GX_SRC_VTX) { + vtxOutAttrs += fmt::format("\n @location({}) clr{}: vec4f,", vtxOutIdx++, i); + vtxXfrAttrs += fmt::format("\n out.clr{} = {};", i, vertex_color_attr(config, i)); + } + } + + if (UsePerPixelLighting) { + fragmentFnPre += fmt::format("\n var rast{}: vec4f;", i); + fragmentFnPre += lighting_func(config, cc, i, false); + fragmentFnPre += lighting_func(config, cca, i, true); + } else { + vtxOutAttrs += fmt::format("\n @location({}) cc{}: vec4f,", vtxOutIdx++, i); + vtxXfrAttrs += lighting_func(config, cc, i, false); + vtxXfrAttrs += lighting_func(config, cca, i, true); + fragmentFnPre += fmt::format("\n var rast{0} = in.cc{0};", i); + } + } + for (int i = 0; i < info.sampledKColors.size(); ++i) { + if (info.sampledKColors.test(i)) { + uniBufAttrs += fmt::format("\n kcolor{}: vec4f,", i); + } + } + for (int i = 0; i < info.sampledTexCoords.size(); ++i) { + if (!info.sampledTexCoords.test(i)) { + continue; + } + const auto& tcg = config.tcgs[i]; + if (tcg.type == GX_TG_MTX3x4) { + vtxOutAttrs += fmt::format("\n @location({}) tex{}_uvw: vec3f,", vtxOutIdx++, i); + } else { + vtxOutAttrs += fmt::format("\n @location({}) tex{}_uv: vec2f,", vtxOutIdx++, i); + } + if (tcg.src >= GX_TG_TEX0 && tcg.src <= GX_TG_TEX7) { + vtxXfrAttrs += fmt::format("\n var tc{} = vec4f({}, 1.0, 1.0);", i, + vtx_attr(config, GXAttr(GX_VA_TEX0 + (tcg.src - GX_TG_TEX0)))); + } else if (tcg.src == GX_MAX_TEXGENSRC) { + vtxXfrAttrs += fmt::format("\n var tc{} = vec4f({}, 1.0, 1.0);", i, + vtx_attr(config, GXAttr(GX_VA_TEX0 + i))); + } else if (tcg.src == GX_TG_POS) { + vtxXfrAttrs += fmt::format("\n var tc{} = vec4f({}, 1.0);", i, vtx_attr(config, GX_VA_POS)); + } else if (tcg.src == GX_TG_NRM) { + vtxXfrAttrs += fmt::format("\n var tc{} = vec4f({}, 1.0);", i, vtx_attr(config, GX_VA_NRM)); + } else if (tcg.src == GX_TG_BINRM) { + // GX source row 3 is Dolphin's tangent group in NBT normal arrays. + vtxXfrAttrs += fmt::format("\n var tc{} = vec4f({}, 1.0);", i, normal_group_load(config, 1, vidxAttr)); + } else if (tcg.src == GX_TG_TANGENT) { + // GX source row 4 is Dolphin's binormal group in NBT normal arrays. + vtxXfrAttrs += fmt::format("\n var tc{} = vec4f({}, 1.0);", i, normal_group_load(config, 2, vidxAttr)); + } else if (tcg.src == GX_TG_COLOR0) { + vtxXfrAttrs += fmt::format("\n var tc{} = vec4f({}.rgb, 1.0);", i, vtx_attr(config, GX_VA_CLR0)); + } else if (tcg.src == GX_TG_COLOR1) { + vtxXfrAttrs += fmt::format("\n var tc{} = vec4f({}.rgb, 1.0);", i, vtx_attr(config, GX_VA_CLR1)); + } else + UNLIKELY FATAL("unhandled tcg src {}", underlying(tcg.src)); + if (tcg.inputFormAB11) { + vtxXfrAttrs += fmt::format("\n tc{}.z = 1.0;", i); + } + if (tcg.type == GX_TG_MTX2x4 || tcg.type == GX_TG_MTX3x4) { + if (info.indexAttr.test(GX_VA_TEX0MTXIDX + i)) { + vtxXfrAttrs += fmt::format("\n var tc{0}_tmp = tc{0} * ubuf.postex_mtx[in_texmtxidx{0} / 3u];", i); + } else if (tcg.mtx == GX_IDENTITY) { + vtxXfrAttrs += fmt::format("\n var tc{0}_tmp = tc{0}.xyz;", i); + } else { + const u32 texMtxSlot = (tcg.mtx) / 3; + const u32 texMtxIdx = texMtxSlot < MaxPostexMtx ? info.matrixLayout.postexRemap[texMtxSlot] + : texMtxSlot; + CHECK(texMtxIdx != UniformMatrixLayout::kAbsent, + "texgen {} matrix slot {} missing from the uniform layout", i, texMtxSlot); + vtxXfrAttrs += fmt::format("\n var tc{0}_tmp = tc{0} * ubuf.postex_mtx[{1}];", i, texMtxIdx); + } + if (tcg.type == GX_TG_MTX2x4) { + vtxXfrAttrs += fmt::format("\n tc{0}_tmp.z = 1.0f;", i); + } + } else if (tcg.type == GX_TG_SRTG) { + vtxXfrAttrs += fmt::format("\n var tc{0}_tmp = vec3f(tc{0}.xy, 1.0f);", i); + } + if (config.dualTexEnabled && tcg.normalize) { + vtxXfrAttrs += fmt::format("\n tc{0}_tmp = normalize(tc{0}_tmp);", i); + } + if (!config.dualTexEnabled || tcg.postMtx == GX_PTIDENTITY) { + vtxXfrAttrs += fmt::format("\n var tc{0}_proj = tc{0}_tmp;", i); + } else { + u32 postMtxIdx = (tcg.postMtx - GX_PTTEXMTX0) / 3; + vtxXfrAttrs += + fmt::format("\n var tc{0}_proj = vec4f(tc{0}_tmp.xyz, 1.0) * ubuf.postmtx[{1}];", i, postMtxIdx); + } + if (tcg.type == GX_TG_MTX2x4 || tcg.type == GX_TG_MTX3x4) { + vtxXfrAttrs += fmt::format( + "\n if (tc{0}_proj.z == 0.0) {{" + "\n tc{0}_proj.x = clamp(tc{0}_proj.x / 2.0, -1.0, 1.0);" + "\n tc{0}_proj.y = clamp(tc{0}_proj.y / 2.0, -1.0, 1.0);" + "\n }}", + i); + } + // Apply line/point tex offset + if (config.lineMode == 3) { + // GX_POINTS: offset S for right columns, T for bottom rows + vtxXfrAttrs += fmt::format( + "\n if ((ubuf.line_texcoord_mask & (1u << {0})) != 0u) {{" + "\n if ((vidx & 1u) != 0u) {{ tc{0}_proj.x += ubuf.line_tex_offset; }}" + "\n if (vidx >= 2u) {{ tc{0}_proj.y += ubuf.line_tex_offset; }}" + "\n }}", + i); + } else if (config.lineMode != 0) { + // GX_LINES / GX_LINESTRIP: offset one axis for perpendicular side + vtxXfrAttrs += fmt::format( + "\n if ((ubuf.line_texcoord_mask & (1u << {0})) != 0u && (vidx & 1u) != 0u) {{" + "\n tc{0}_proj.y += ubuf.line_tex_offset;" + "\n }}", + i); + } + if (tcg.type == GX_TG_MTX3x4) { + vtxXfrAttrs += fmt::format("\n out.tex{0}_uvw = tc{0}_proj.xyz;", i); + fragmentFnPre += fmt::format( + "\n var tex{0}_uv = in.tex{0}_uvw.xy;" + "\n if (in.tex{0}_uvw.z != 0.0) {{" + "\n tex{0}_uv = in.tex{0}_uvw.xy / in.tex{0}_uvw.z;" + "\n }}", + i); + } else { + vtxXfrAttrs += fmt::format("\n out.tex{0}_uv = tc{0}_proj.xy;", i); + fragmentFnPre += fmt::format("\n var tex{0}_uv = in.tex{0}_uv.xy;", i); + } + } + // Multiple TEV stages may reference the same indirect stage, + // so we sample each indirect texture only once. + const auto ind_scale_shift = [](const GXIndTexScale s) -> u32 { + switch (s) { + case GX_ITS_1: + return 0; + case GX_ITS_2: + return 1; + case GX_ITS_4: + return 2; + case GX_ITS_8: + return 3; + case GX_ITS_16: + return 4; + case GX_ITS_32: + return 5; + case GX_ITS_64: + return 6; + case GX_ITS_128: + return 7; + case GX_ITS_256: + return 8; + default: + FATAL("unhandled indirect scale {}", underlying(s)); + } + }; + for (int i = 0; i < info.usedIndStages.size(); ++i) { + if (!info.usedIndStages.test(i)) { + continue; + } + const auto& indStage = config.indStages[i]; + const int effectiveIndTexCoord = tev_effective_texcoord(config, indStage.texCoordId); + const std::string indFixedUv = + effectiveIndTexCoord >= 0 ? fixed_texcoord_name(static_cast(effectiveIndTexCoord)) : "vec2i(0)"; + fragmentFnPre += fmt::format( + "\n // Indirect stage {0}" + "\n let ind{0}_sample_fixed = {5} >> vec2u({3}u, {4}u);" + "\n let t_IndTexCoord{0} = tev_byte_vec3f(textureSampleBias(tex{2}, tex{2}_samp, " + "vec2f(ind{0}_sample_fixed) / (ubuf.tex{2}_size_bias.xy * 128.0), ubuf.tex{2}_size_bias.z).abg);", + i, effectiveIndTexCoord, underlying(indStage.texMapId), ind_scale_shift(indStage.scaleS), + ind_scale_shift(indStage.scaleT), indFixedUv); + } + const bool usesFixedTexcoordState = shader_uses_fixed_texcoord_state(config); + if (usesFixedTexcoordState) { + fragmentFnPre += "\n var t_TexCoord = vec2i(0);"; + } + for (int i = 0; i < config.tevStageCount; ++i) { + const auto& stage = config.tevStages[i]; + const auto textureDependency = tev_stage_texture_dependency(config, static_cast(i)); + const bool needsIndirectCoord = stage.indTexMtxId != GX_ITM_OFF; + const bool hasIndirectStage = stage.indTexStage < config.numIndStages; + const bool needsTevTexCoord = textureDependency.needsFixedTexcoordState; + const bool needsTextureSample = textureDependency.combinerUsesTexture || static_cast(i) == zTexStage; + const bool willSampleTexture = tev_texture_sample_enabled(textureDependency, needsTextureSample); + if (!needsTevTexCoord && !needsTextureSample) { + continue; + } + const bool hasBaseCoord = textureDependency.texCoordId >= 0; + std::string uvIn; + if (needsTevTexCoord) { + fragmentFnPre += fmt::format("\n // TEV stage {} indirect", i); + + // Apply indirect texture matrix (produces an S17.7 fixed-point offset). + std::string indirectOffsetFixed; + if (needsIndirectCoord && hasIndirectStage) { + u32 fmtShift = 0; + switch (stage.indTexFormat) { + case GX_ITF_8: + break; + case GX_ITF_5: + fmtShift = 3; + break; + case GX_ITF_4: + fmtShift = 4; + break; + case GX_ITF_3: + fmtShift = 5; + break; + default: + FATAL("unhandled indirect format {}", underlying(stage.indTexFormat)); + } + if (fmtShift == 0) { + fragmentFnPre += fmt::format("\n var ind{0}_coord = t_IndTexCoord{1};", i, underlying(stage.indTexStage)); + } else { + fragmentFnPre += fmt::format("\n var ind{0}_coord = t_IndTexCoord{1} >> vec3u({2}u);", i, + underlying(stage.indTexStage), fmtShift); + } + + if (stage.indTexBiasSel != GX_ITB_NONE) { + auto bias = stage.indTexFormat == GX_ITF_8 ? "-128"sv : "1"sv; + auto biasS = "0"sv, biasT = "0"sv, biasU = "0"sv; + if (stage.indTexBiasSel == GX_ITB_S || stage.indTexBiasSel == GX_ITB_ST || stage.indTexBiasSel == GX_ITB_SU || + stage.indTexBiasSel == GX_ITB_STU) { + biasS = "1"sv; + } + if (stage.indTexBiasSel == GX_ITB_T || stage.indTexBiasSel == GX_ITB_ST || stage.indTexBiasSel == GX_ITB_TU || + stage.indTexBiasSel == GX_ITB_STU) { + biasT = "1"sv; + } + if (stage.indTexBiasSel == GX_ITB_U || stage.indTexBiasSel == GX_ITB_SU || stage.indTexBiasSel == GX_ITB_TU || + stage.indTexBiasSel == GX_ITB_STU) { + biasU = "1"sv; + } + fragmentFnPre += fmt::format("\n ind{0}_coord = ind{0}_coord + vec3i({1}, {2}, {3}) * {4};", i, biasS, + biasT, biasU, bias); + } + + const auto appendScaleShift = [&](u32 mtxIdx) { + fragmentFnPre += fmt::format( + "\n let ind{0}_shift = ubuf.ind_mtx[{1}].z;" + "\n if (ind{0}_shift >= 0) {{" + "\n ind{0}_offset_fixed = ind{0}_offset_fixed >> vec2u(u32(ind{0}_shift));" + "\n }} else {{" + "\n ind{0}_offset_fixed = ind{0}_offset_fixed << vec2u(u32(-ind{0}_shift));" + "\n }}", + i, mtxIdx * 2 + 1); + indirectOffsetFixed = fmt::format("ind{}_offset_fixed", i); + }; + + if (stage.indTexMtxId >= GX_ITM_0 && stage.indTexMtxId <= GX_ITM_2) { + // Static 2x3 matrix: integer dot products, then the hardware's fixed >>3 and exponent shift sequence. + u32 mtxIdx = stage.indTexMtxId - GX_ITM_0; + fragmentFnPre += fmt::format( + "\n let ind{0}_c0 = ubuf.ind_mtx[{1}];" + "\n let ind{0}_c1 = ubuf.ind_mtx[{2}];" + "\n var ind{0}_offset_fixed = vec2i(" + "\n (ind{0}_c0.x * ind{0}_coord.x + ind{0}_c0.z * ind{0}_coord.y + " + "ind{0}_c1.x * ind{0}_coord.z) >> 3u," + "\n (ind{0}_c0.y * ind{0}_coord.x + ind{0}_c0.w * ind{0}_coord.y + " + "ind{0}_c1.y * ind{0}_coord.z) >> 3u);", + i, mtxIdx * 2, mtxIdx * 2 + 1); + appendScaleShift(mtxIdx); + } else if (stage.indTexMtxId >= GX_ITM_S0 && stage.indTexMtxId <= GX_ITM_S2 && hasBaseCoord) { + // Dynamic S: (fixed UV * indirect S) >> 8, then exponent shift. + u32 mtxIdx = stage.indTexMtxId - GX_ITM_S0; + u32 regTexCoord = static_cast(textureDependency.texCoordId); + const auto fixedUv = fixed_texcoord_name(regTexCoord); + fragmentFnPre += fmt::format( + "\n var ind{0}_offset_fixed = ({1} * vec2i(ind{0}_coord.x)) >> vec2u(8u);", i, fixedUv); + appendScaleShift(mtxIdx); + } else if (stage.indTexMtxId >= GX_ITM_T0 && stage.indTexMtxId <= GX_ITM_T2 && hasBaseCoord) { + // Dynamic T: (fixed UV * indirect T) >> 8, then exponent shift. + u32 mtxIdx = stage.indTexMtxId - GX_ITM_T0; + u32 regTexCoord = static_cast(textureDependency.texCoordId); + const auto fixedUv = fixed_texcoord_name(regTexCoord); + fragmentFnPre += fmt::format( + "\n var ind{0}_offset_fixed = ({1} * vec2i(ind{0}_coord.y)) >> vec2u(8u);", i, fixedUv); + appendScaleShift(mtxIdx); + } + } + + // Wrap base coord and combine with the indirect translation. + auto wrap_comp = [](GXIndTexWrap wrap, std::string&& coord) -> std::string { + const int mask = tev_indirect_wrap_mask(wrap); + if (mask == -1) { + return std::move(coord); + } + if (mask == 0) { + return "0"; + } + CHECK(mask > 0, "invalid indirect wrap {}", underlying(wrap)); + return fmt::format("({} & {})", coord, mask); + }; + std::string baseCoordExpr = "vec2i(0)"; + if (hasBaseCoord) { + u32 texCoordId = static_cast(textureDependency.texCoordId); + baseCoordExpr = fixed_texcoord_name(texCoordId); + } + const std::string wrappedExpr = + fmt::format("vec2i({}, {})", wrap_comp(stage.indTexWrapS, fmt::format("{}.x", baseCoordExpr)), + wrap_comp(stage.indTexWrapT, fmt::format("{}.y", baseCoordExpr))); + std::string finalCoord = wrappedExpr; + if (!indirectOffsetFixed.empty()) { + finalCoord = fmt::format("{} + ({})", wrappedExpr, indirectOffsetFixed); + } + + if (usesFixedTexcoordState) { + if (stage.indTexAddPrev) { + fragmentFnPre += fmt::format("\n t_TexCoord += {};", finalCoord); + } else { + fragmentFnPre += fmt::format("\n t_TexCoord = {};", finalCoord); + } + fragmentFnPre += "\n t_TexCoord = tev_s24_fixed_vec2i(t_TexCoord);"; + + // ShaderInfo only emits texN_size_bias and texture bindings for a texture that can actually be sampled. + if (willSampleTexture) { + u32 texMapId = static_cast(textureDependency.texMapId); + fragmentFnPre += fmt::format( + "\n var ind{0}_uv = vec2f(t_TexCoord) / (ubuf.tex{1}_size_bias.xy * 128.0);", i, texMapId); + uvIn = fmt::format("ind{0}_uv", i); + } + } + } + if (!willSampleTexture) { + continue; + } + if (uvIn.empty()) { + // No indirect texturing + const auto fixedUv = fixed_texcoord_name(static_cast(textureDependency.texCoordId)); + uvIn = fmt::format("vec2f(tev_s24_fixed_vec2i({0})) / (ubuf.tex{1}_size_bias.xy * 128.0)", fixedUv, + textureDependency.texMapId); + } + fragmentFnPre += + fmt::format("\n var sampled{0} = textureSampleBias(tex{1}, tex{1}_samp, {2}, ubuf.tex{1}_size_bias.z);", i, + textureDependency.texMapId, uvIn); + } + + std::string fogDepthExpr = UseReversedZ ? "in.pos.z" : "(1.0 - in.pos.z)"; + std::string fogZCoordExpr = + fmt::format("u32(round(clamp({}, 0.0, 1.0) * 16777216.0))", fogDepthExpr); + if (usesZTextureDepth) { + const u32 zTexBias = config.zTexture & 0x00FFFFFFu; + const u32 zTexFmt = (config.zTexture >> 24) & 0x3u; + const u32 zTexOp = (config.zTexture >> 26) & 0x3u; + const std::string zTexSample = zTexStage >= 0 ? fmt::format("sampled{}", zTexStage) : "vec4f(0.0)"; + + fragmentFn += fmt::format( + "\n // Z texture" + "\n let ztexSample = clamp({0}, vec4f(0.0), vec4f(1.0)) * 255.0;" + "\n let ztexRaw = vec4u(round(ztexSample));", + zTexSample); + switch (zTexFmt) { + case 0: + fragmentFn += fmt::format("\n var ztexCoord = ({0}u + ztexRaw.a) & 0x00ffffffu;", zTexBias); + break; + case 1: + fragmentFn += + fmt::format("\n var ztexCoord = ({0}u + ((ztexRaw.a << 8u) | ztexRaw.r)) & 0x00ffffffu;", zTexBias); + break; + case 2: + default: + fragmentFn += fmt::format( + "\n var ztexCoord = ({0}u + ((ztexRaw.r << 16u) | (ztexRaw.g << 8u) | ztexRaw.b)) & 0x00ffffffu;", + zTexBias); + break; + } + if (zTexOp == GX_ZT_ADD) { + fragmentFn += fmt::format( + "\n let oldZ = u32(round(clamp({0}, 0.0, 1.0) * 16777216.0));" + "\n ztexCoord = (ztexCoord + oldZ) & 0x00ffffffu;", + UseReversedZ ? "in.pos.z" : "(1.0 - in.pos.z)"); + } + fragmentFn += "\n let ztexDepth = f32(ztexCoord) / 16777216.0;"; + fogZCoordExpr = "ztexCoord"; + } + + if (info.usesPTTexMtx.any()) + uniBufAttrs += fmt::format("\n postmtx: array,", MaxPTTexMtx); + if (info.usesFog) { + uniformPre += + "\n" + "struct Fog {\n" + " color: vec4f,\n" + " a: f32,\n" + " b: f32,\n" + " c: f32,\n" + " pad: f32,\n" + " range_base: vec4f,\n" + " range_k: array,\n" + "}"; + uniBufAttrs += "\n fog: Fog,"; + + if ((config.fogType & 0x8u) == 0) { + fragmentFn += fmt::format( + "\n // Fog" + "\n let fogZCoord = {};" + "\n let fogShiftedZ = fogZCoord >> u32(ubuf.fog.pad);" + "\n var fogZe = (ubuf.fog.a * 16777216.0) / (ubuf.fog.b - f32(fogShiftedZ));", + fogZCoordExpr); + } else { + fragmentFn += fmt::format( + "\n // Fog" + "\n let fogZCoord = {};" + "\n var fogZe = ubuf.fog.a * f32(fogZCoord) / 16777216.0;", + fogZCoordExpr); + } + if (config.fogRangeAdjust) { + fragmentFn += + "\n let fogRangeOffset = (2.0 * (in.pos.x / ubuf.fog.range_base.y)) - 1.0 - ubuf.fog.range_base.x;" + "\n let fogRangeFloatIndex = clamp(9.0 - abs(fogRangeOffset) * 9.0, 0.0, 9.0);" + "\n let fogRangeIndexLower = u32(fogRangeFloatIndex);" + "\n let fogRangeIndexUpper = fogRangeIndexLower + 1u;" + "\n let fogRangeKLower = ubuf.fog.range_k[fogRangeIndexLower >> 2u][fogRangeIndexLower & 3u];" + "\n let fogRangeKUpper = ubuf.fog.range_k[fogRangeIndexUpper >> 2u][fogRangeIndexUpper & 3u];" + "\n let fogRangeK = mix(fogRangeKLower, fogRangeKUpper, fract(fogRangeFloatIndex));" + "\n fogZe *= sqrt(fogRangeOffset * fogRangeOffset + fogRangeK * fogRangeK) / fogRangeK;"; + } + fragmentFn += "\n var fogF = clamp(fogZe - ubuf.fog.c, 0.0, 1.0);"; + switch (config.fogType) { + DEFAULT_FATAL("invalid fog type {}", config.fogType); + case static_cast(1): + case static_cast(3): + case GX_FOG_PERSP_LIN: + case static_cast(9): + case static_cast(11): + case GX_FOG_ORTHO_LIN: + fragmentFn += "\n var fogZ = fogF;"; + break; + case GX_FOG_PERSP_EXP: + case GX_FOG_ORTHO_EXP: + fragmentFn += "\n var fogZ = 1.0 - exp2(-8.0 * fogF);"; + break; + case GX_FOG_PERSP_EXP2: + case GX_FOG_ORTHO_EXP2: + fragmentFn += "\n var fogZ = 1.0 - exp2(-8.0 * fogF * fogF);"; + break; + case GX_FOG_PERSP_REVEXP: + case GX_FOG_ORTHO_REVEXP: + fragmentFn += "\n var fogZ = exp2(-8.0 * (1.0 - fogF));"; + break; + case GX_FOG_PERSP_REVEXP2: + case GX_FOG_ORTHO_REVEXP2: + fragmentFn += + "\n fogF = 1.0 - fogF;" + "\n var fogZ = exp2(-8.0 * fogF * fogF);"; + break; + } + fragmentFn += "\n prev = vec4f(mix(prev.rgb, ubuf.fog.color.rgb, clamp(fogZ, 0.0, 1.0)), prev.a);"; + } + if (info.usedIndTexMtxs.any()) { + uniBufAttrs += "\n ind_mtx: array,"; + } + for (int i = 0; i < info.sampledTexCoords.size(); ++i) { + if (info.sampledTexCoords.test(i)) { + uniBufAttrs += fmt::format("\n texcoord{}_scale: vec4f,", i); + } + } + for (int i = 0; i < info.sampledTextures.size(); ++i) { + if (!info.sampledTextures.test(i)) { + continue; + } + uniBufAttrs += fmt::format("\n tex{}_size_bias: vec4f,", i); + texBindings += fmt::format( + "\n@group(2) @binding({1})\n" + "var tex{0}: texture_2d;\n" + "@group(2) @binding({2})\n" + "var tex{0}_samp: sampler;", + i, i * 2, i * 2 + 1); + } + fragmentFn += "\n prev = tev_overflow_vec4f(prev);"; + if (config.alphaCompare) { + const auto comp0 = alpha_compare(config.alphaCompare.comp0, config.alphaCompare.ref0); + const auto comp1 = alpha_compare(config.alphaCompare.comp1, config.alphaCompare.ref1); + AlphaCompareExpr pass; + switch (config.alphaCompare.op) { + default: + pass = alpha_compare_and(comp0, comp1); + break; + case GX_AOP_AND: + pass = alpha_compare_and(comp0, comp1); + break; + case GX_AOP_OR: + pass = alpha_compare_or(comp0, comp1); + break; + case GX_AOP_XOR: + pass = alpha_compare_xor(comp0, comp1); + break; + case GX_AOP_XNOR: + pass = alpha_compare_xnor(comp0, comp1); + break; + } + const auto discard = alpha_compare_not(pass); + if (discard.constant == 1) { + fragmentFn += "\n // Alpha compare\n discard;"; + } else if (discard.constant != 0) { + fragmentFn += "\n // Alpha compare" + "\n let alphaCompare = u32(round(clamp(prev.a, 0.0, 1.0) * 255.0));"; + fragmentFn += fmt::format("\n if ({}) {{ discard; }}", discard.expr); + } + } + + std::string fragmentReturnType = "@location(0) vec4f"; + std::string fragmentReturn = " return prev;"; + if (usesZTextureDepth) { + uniformPre += + "\n" + "struct FragmentOutput {\n" + " @location(0) color: vec4f,\n" + " @builtin(frag_depth) depth: f32,\n" + "};"; + + fragmentFn += fmt::format("\n let fragDepth = {}ztexDepth;", UseReversedZ ? "" : "1.0 - "); + fragmentReturnType = "FragmentOutput"; + fragmentReturn = + " var out: FragmentOutput;\n" + " out.color = prev;\n" + " out.depth = fragDepth;\n" + " return out;"; + } + + const auto shaderSource = fmt::format(R"""( +fn bswap32(v: u32, le: bool) -> u32 {{ + if (le) {{ + return v; + }} + return ((v & 0x000000FFu) << 24u) | + ((v & 0x0000FF00u) << 8u) | + ((v & 0x00FF0000u) >> 8u) | + ((v & 0xFF000000u) >> 24u); +}} + +fn bswap16(v: u32, le: bool) -> u32 {{ + return select(((v & 0xFFu) << 8u) | (v >> 8u), v, le); +}} + +fn load_u8(p: ptr>, byte_off: u32) -> u32 {{ + let word = p[byte_off / 4u]; + let shift = (byte_off & 3u) * 8u; + return (word >> shift) & 0xFFu; +}} + +fn load_u32_raw(p: ptr>, byte_off: u32) -> u32 {{ + let word_idx = byte_off >> 2u; + let sub = byte_off & 3u; + let lo = p[word_idx]; + if (sub == 0u) {{ + return lo; + }} + let hi = p[word_idx + 1u]; + let shift = sub * 8u; + return (lo >> shift) | (hi << (32u - shift)); +}} + +fn load_u16(p: ptr>, byte_off: u32, le: bool) -> u32 {{ + let word_idx = byte_off >> 2u; + let sub = byte_off & 3u; + let word = p[word_idx]; + if (sub <= 2u) {{ + return bswap16(extractBits(word, sub * 8u, 16u), le); + }} + let next = p[word_idx + 1u]; + let raw = extractBits(word, 24u, 8u) | (extractBits(next, 0u, 8u) << 8u); + return bswap16(raw, le); +}} + +fn load_u24(p: ptr>, byte_off: u32, le: bool) -> u32 {{ + let raw = load_u32_raw(p, byte_off) & 0x00FFFFFFu; + if (le) {{ + return raw; + }} + return ((raw & 0x0000FFu) << 16u) | + (raw & 0x00FF00u) | + ((raw & 0xFF0000u) >> 16u); +}} + +fn load_u32(p: ptr>, byte_off: u32, le: bool) -> u32 {{ + return bswap32(load_u32_raw(p, byte_off), le); +}} + +fn load_f32(p: ptr>, byte_off: u32, le: bool) -> f32 {{ + return bitcast(load_u32(p, byte_off, le)); +}} + +fn raw_fetch_u8_1(p: ptr>, byte_off: u32) -> u32 {{ + return load_u8(p, byte_off); +}} + +fn raw_fetch_u8_2(p: ptr>, byte_off: u32) -> vec2u {{ + let word_idx = byte_off >> 2u; + let sub = byte_off & 3u; + let word = p[word_idx]; + if (sub <= 2u) {{ + let shift = sub * 8u; + return vec2u( + extractBits(word, shift + 0u, 8u), + extractBits(word, shift + 8u, 8u), + ); + }} + let next = p[word_idx + 1u]; + return vec2u( + extractBits(word, 24u, 8u), + extractBits(next, 0u, 8u), + ); +}} + +fn raw_fetch_u8_3(p: ptr>, byte_off: u32) -> vec3u {{ + let raw = load_u32_raw(p, byte_off); + return vec3u( + extractBits(raw, 0u, 8u), + extractBits(raw, 8u, 8u), + extractBits(raw, 16u, 8u), + ); +}} + +fn raw_fetch_u8_4(p: ptr>, byte_off: u32) -> vec4u {{ + let raw = load_u32_raw(p, byte_off); + return vec4u( + extractBits(raw, 0u, 8u), + extractBits(raw, 8u, 8u), + extractBits(raw, 16u, 8u), + extractBits(raw, 24u, 8u), + ); +}} + +fn raw_fetch_u16_1(p: ptr>, byte_off: u32, le: bool) -> u32 {{ + return load_u16(p, byte_off, le); +}} + +fn raw_fetch_u16_2(p: ptr>, byte_off: u32, le: bool) -> vec2u {{ + return vec2u( + load_u16(p, byte_off + 0u, le), + load_u16(p, byte_off + 2u, le), + ); +}} + +fn raw_fetch_u16_3(p: ptr>, byte_off: u32, le: bool) -> vec3u {{ + return vec3u( + load_u16(p, byte_off + 0u, le), + load_u16(p, byte_off + 2u, le), + load_u16(p, byte_off + 4u, le), + ); +}} + +fn raw_fetch_u16_4(p: ptr>, byte_off: u32, le: bool) -> vec4u {{ + return vec4u( + load_u16(p, byte_off + 0u, le), + load_u16(p, byte_off + 2u, le), + load_u16(p, byte_off + 4u, le), + load_u16(p, byte_off + 6u, le), + ); +}} + +fn raw_fetch_f32_1(p: ptr>, byte_off: u32, le: bool) -> f32 {{ + return load_f32(p, byte_off, le); +}} + +fn raw_fetch_f32_2(p: ptr>, byte_off: u32, le: bool) -> vec2f {{ + return vec2f( + load_f32(p, byte_off + 0u, le), + load_f32(p, byte_off + 4u, le), + ); +}} + +fn raw_fetch_f32_3(p: ptr>, byte_off: u32, le: bool) -> vec3f {{ + return vec3f( + load_f32(p, byte_off + 0u, le), + load_f32(p, byte_off + 4u, le), + load_f32(p, byte_off + 8u, le), + ); +}} + +fn raw_fetch_f32_4(p: ptr>, byte_off: u32, le: bool) -> vec4f {{ + return vec4f( + load_f32(p, byte_off + 0u, le), + load_f32(p, byte_off + 4u, le), + load_f32(p, byte_off + 8u, le), + load_f32(p, byte_off + 12u, le), + ); +}} + +fn fetch_u8_1(p: ptr>, byte_off: u32, frac: u32, le: bool) -> f32 {{ + let v = raw_fetch_u8_1(p, byte_off); + return f32(v) / f32(1u << frac); +}} + +fn fetch_s8_1(p: ptr>, byte_off: u32, frac: u32, le: bool) -> f32 {{ + let v = (bitcast(raw_fetch_u8_1(p, byte_off)) << 24) >> 24; + return f32(v) / f32(1u << frac); +}} + +fn fetch_u8_2(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec2f {{ + let v = raw_fetch_u8_2(p, byte_off); + return vec2f(v) / f32(1u << frac); +}} + +fn fetch_s8_2(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec2f {{ + let v = (bitcast(raw_fetch_u8_2(p, byte_off)) << vec2u(24u)) >> vec2u(24u); + return vec2f(v) / f32(1u << frac); +}} + +fn fetch_u8_3(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec3f {{ + let v = raw_fetch_u8_3(p, byte_off); + return vec3f(v) / f32(1u << frac); +}} + +fn fetch_s8_3(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec3f {{ + let v = (bitcast(raw_fetch_u8_3(p, byte_off)) << vec3u(24u)) >> vec3u(24u); + return vec3f(v) / f32(1u << frac); +}} + +fn fetch_u8_4(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec4f {{ + let v = raw_fetch_u8_4(p, byte_off); + return vec4f(v) / f32(1u << frac); +}} + +fn fetch_s8_4(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec4f {{ + let v = (bitcast(raw_fetch_u8_4(p, byte_off)) << vec4u(24u)) >> vec4u(24u); + return vec4f(v) / f32(1u << frac); +}} + +fn fetch_u16_1(p: ptr>, byte_off: u32, frac: u32, le: bool) -> f32 {{ + let v = raw_fetch_u16_1(p, byte_off, le); + return f32(v) / f32(1u << frac); +}} + +fn fetch_s16_1(p: ptr>, byte_off: u32, frac: u32, le: bool) -> f32 {{ + let v = bitcast(raw_fetch_u16_1(p, byte_off, le) << 16u) >> 16; + return f32(v) / f32(1u << frac); +}} + +fn fetch_u16_2(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec2f {{ + let v = raw_fetch_u16_2(p, byte_off, le); + return vec2f(v) / f32(1u << frac); +}} + +fn fetch_s16_2(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec2f {{ + let v = (bitcast(raw_fetch_u16_2(p, byte_off, le)) << vec2u(16u)) >> vec2u(16u); + return vec2f(v) / f32(1u << frac); +}} + +fn fetch_u16_3(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec3f {{ + let v = raw_fetch_u16_3(p, byte_off, le); + return vec3f(v) / f32(1u << frac); +}} + +fn fetch_s16_3(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec3f {{ + let v = (bitcast(raw_fetch_u16_3(p, byte_off, le)) << vec3u(16u)) >> vec3u(16u); + return vec3f(v) / f32(1u << frac); +}} + +fn fetch_u16_4(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec4f {{ + let v = raw_fetch_u16_4(p, byte_off, le); + return vec4f(v) / f32(1u << frac); +}} + +fn fetch_s16_4(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec4f {{ + let v = (bitcast(raw_fetch_u16_4(p, byte_off, le)) << vec4u(16u)) >> vec4u(16u); + return vec4f(v) / f32(1u << frac); +}} + +fn fetch_f32_1(p: ptr>, byte_off: u32, le: bool) -> f32 {{ + return raw_fetch_f32_1(p, byte_off, le); +}} + +fn fetch_f32_2(p: ptr>, byte_off: u32, le: bool) -> vec2f {{ + return raw_fetch_f32_2(p, byte_off, le); +}} + +fn fetch_f32_3(p: ptr>, byte_off: u32, le: bool) -> vec3f {{ + return raw_fetch_f32_3(p, byte_off, le); +}} + +fn fetch_f32_4(p: ptr>, byte_off: u32, le: bool) -> vec4f {{ + return raw_fetch_f32_4(p, byte_off, le); +}} + +fn fetch_rgb565(p: ptr>, byte_off: u32, le: bool) -> vec4f {{ + let v = load_u16(p, byte_off, le); + return vec4f( + f32((v >> 11u) & 0x1Fu) / f32(0x1Fu), + f32((v >> 5u) & 0x3Fu) / f32(0x3Fu), + f32((v >> 0u) & 0x1Fu) / f32(0x1Fu), + 1.0, + ); +}} + +fn fetch_rgb8(p: ptr>, byte_off: u32, le: bool) -> vec4f {{ + let v = raw_fetch_u8_3(p, byte_off); + return vec4f(f32(v.x), f32(v.y), f32(v.z), 255.0) / 255.0; +}} + +fn fetch_rgbx8(p: ptr>, byte_off: u32, le: bool) -> vec4f {{ + let v = raw_fetch_u8_4(p, byte_off); + return vec4f(f32(v.x), f32(v.y), f32(v.z), 255.0) / 255.0; +}} + +fn fetch_rgba4(p: ptr>, byte_off: u32, le: bool) -> vec4f {{ + let v = load_u16(p, byte_off, le); + return vec4f( + f32((v >> 12u) & 0x0Fu) / f32(0x0Fu), + f32((v >> 8u) & 0x0Fu) / f32(0x0Fu), + f32((v >> 4u) & 0x0Fu) / f32(0x0Fu), + f32((v >> 0u) & 0x0Fu) / f32(0x0Fu), + ); +}} + +fn fetch_rgba6(p: ptr>, byte_off: u32, le: bool) -> vec4f {{ + let v = load_u24(p, byte_off, le); + return vec4f( + f32((v >> 18u) & 0x3Fu) / f32(0x3Fu), + f32((v >> 12u) & 0x3Fu) / f32(0x3Fu), + f32((v >> 6u) & 0x3Fu) / f32(0x3Fu), + f32((v >> 0u) & 0x3Fu) / f32(0x3Fu), + ); +}} + +fn fetch_rgba8(p: ptr>, byte_off: u32, le: bool) -> vec4f {{ + let v = raw_fetch_u8_4(p, byte_off); + return vec4f(v) / 255.0; +}} + +fn tev_overflow_f32(in: f32) -> f32 {{ + let byte_space = in * 255.0; + return (byte_space - floor(byte_space / 256.0) * 256.0) / 255.0; +}} + +fn tev_overflow_vec3f(in: vec3f) -> vec3f {{ + let byte_space = in * 255.0; + return (byte_space - floor(byte_space / 256.0) * 256.0) / 255.0; +}} + +fn tev_overflow_vec4f(in: vec4f) -> vec4f {{ + let byte_space = in * 255.0; + return (byte_space - floor(byte_space / 256.0) * 256.0) / 255.0; +}} + +fn tev_s24_fixed_vec2i(in: vec2i) -> vec2i {{ + return (in << vec2u(8u)) >> vec2u(8u); +}} + +fn tev_byte_f32(in: f32) -> i32 {{ + return i32(round(in * 255.0)); +}} + +fn tev_byte_vec3f(in: vec3f) -> vec3i {{ + return vec3i(round(in * 255.0)); +}} + +fn tev_regular_i32(a: i32, b: i32, c: i32, d: i32, bias: i32, scale: u32, is_sub: bool) -> i32 {{ + var d_part = d + bias; + var lerp_part = (a << 8u) + (b - a) * (c + (c >> 7u)); + if (scale == 1u) {{ + d_part = d_part << 1u; + lerp_part = lerp_part << 1u; + }} + if (scale == 2u) {{ + d_part = d_part << 2u; + lerp_part = lerp_part << 2u; + }} + if (scale != 3u) {{ + lerp_part += select(128, 127, is_sub); + }} + let lerp = lerp_part >> 8u; + var result = select(d_part + lerp, d_part - lerp, is_sub); + if (scale == 3u) {{ + result = result >> 1u; + }} + return result; +}} + +fn tev_regular_f32(a: f32, b: f32, c: f32, d: f32, bias: i32, scale: u32, is_sub: bool) -> f32 {{ + return f32(tev_regular_i32(tev_byte_f32(a), tev_byte_f32(b), tev_byte_f32(c), tev_byte_f32(d), bias, scale, is_sub)) / + 255.0; +}} + +fn tev_regular_vec3f(a: vec3f, b: vec3f, c: vec3f, d: vec3f, bias: i32, scale: u32, is_sub: bool) -> vec3f {{ + let ai = tev_byte_vec3f(a); + let bi = tev_byte_vec3f(b); + let ci = tev_byte_vec3f(c); + let di = tev_byte_vec3f(d); + return vec3f( + f32(tev_regular_i32(ai.x, bi.x, ci.x, di.x, bias, scale, is_sub)) / 255.0, + f32(tev_regular_i32(ai.y, bi.y, ci.y, di.y, bias, scale, is_sub)) / 255.0, + f32(tev_regular_i32(ai.z, bi.z, ci.z, di.z, bias, scale, is_sub)) / 255.0, + ); +}} + +{8} + +struct Uniform {{ + vtx_start: u32, + current_pnmtx: u32, + render_viewport_size: vec2f, + logical_viewport_size: vec2f, + pad: vec2u, + array_start: array,{0} +}}; +@group(0) @binding(0) +var vbuf: array; +@group(0) @binding(1) +var abuf: array; +@group(1) @binding(0) +var ubuf: Uniform;{1} + +struct VertexOutput {{ + @builtin(position) pos: vec4f,{2} +}}; + +@vertex +fn vs_main( + @builtin(vertex_index) vidx: u32{3} +) -> VertexOutput {{ + var out: VertexOutput;{7}{4} + return out; +}} + +@fragment +fn fs_main(in: VertexOutput) -> {9} {{{6}{5} +{10} +}} +)""", + uniBufAttrs, texBindings, vtxOutAttrs, vtxInAttrs, vtxXfrAttrs, fragmentFn, + fragmentFnPre, vtxXfrAttrsPre, uniformPre, fragmentReturnType, + fragmentReturn); + wgpu::ShaderSourceWGSL wgslDescriptor{}; + wgslDescriptor.code = shaderSource.c_str(); + const auto label = fmt::format("GX Shader {:x}", hash); + const auto shaderDescriptor = wgpu::ShaderModuleDescriptor{ + .nextInChain = &wgslDescriptor, + .label = label.c_str(), + }; + return webgpu::g_device.CreateShaderModule(&shaderDescriptor); +} +} // namespace aurora::gx diff --git a/aurora-main/lib/gx/shader_info.cpp b/aurora-main/lib/gx/shader_info.cpp new file mode 100644 index 0000000..3c21e4f --- /dev/null +++ b/aurora-main/lib/gx/shader_info.cpp @@ -0,0 +1,738 @@ +#include "shader_info.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#if !defined(_LIBCPP_VERSION) +#include +#endif +#include +#include +#include +#include + +#include + +namespace aurora::gx { +// TODO: remove, just for testing +bool enableLodBias = true; + +namespace { +Module Log("aurora::gx"); + +bool is_alpha_bump_channel(GXChannelID id) { return id == GX_ALPHA_BUMP || id == GX_ALPHA_BUMPN; } + +bool sampled_color_channel(GXChannelID id, size_t& out) { + switch (id) { + case GX_COLOR0: + case GX_ALPHA0: + case GX_COLOR0A0: + out = 0; + return true; + case GX_COLOR1: + case GX_ALPHA1: + case GX_COLOR1A1: + out = 1; + return true; + default: + return false; + } +} + +Fog fog_uniform() { + const auto& state = g_gxState.fog; + Fog fog{ + .color = state.color, + .a = state.aRaw, + .b = static_cast(state.bMagnitude), + .c = state.c, + .pad = static_cast(state.bShift), + }; + + const u32 rangeBase = g_gxState.fogRange[0]; + const bool rangeEnabled = (rangeBase & (1u << 10)) != 0; + float rangeWidth = std::abs(g_gxState.xfViewport[0]) * 2.0f; + if (rangeWidth <= 0.0f) { + rangeWidth = 1.0f; + } + const int rangeCenter = static_cast(rangeBase & 0x3ffu) - 342; + const float screenSpaceCenter = rangeEnabled ? ((static_cast(rangeCenter) / rangeWidth) * 2.0f) - 1.0f + : 0.0f; + fog.rangeBase = {screenSpaceCenter, rangeEnabled ? rangeWidth : 1.0f, 0.0f, 0.0f}; + + std::array rangeK{}; + size_t kIndex = 0; + for (size_t i = 1; i < g_gxState.fogRange.size(); ++i) { + const u32 packed = g_gxState.fogRange[i]; + rangeK[kIndex++] = static_cast((packed >> 12) & 0xfffu) / 64.0f; + rangeK[kIndex++] = static_cast(packed & 0xfffu) / 64.0f; + } + rangeK[10] = rangeK[9]; + rangeK[11] = rangeK[9]; + for (size_t i = 0; i < fog.rangeK.size(); ++i) { + fog.rangeK[i] = {rangeK[i * 4 + 0], rangeK[i * 4 + 1], rangeK[i * 4 + 2], rangeK[i * 4 + 3]}; + } + return fog; +} + +float sanitize_light_dir_component(float value) noexcept { + u32 bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + if ((bits & 0x7f800000u) != 0x7f800000u) { + return value; + } + if ((bits & 0x007fffffu) != 0) { + return 0.0f; + } + return (bits & 0x80000000u) != 0 ? -1.0f : 1.0f; +} +} // namespace + +Light prepare_shader_light(Light light) noexcept { + if (std::fabs(light.distAtt[0]) < 0.00001f && std::fabs(light.distAtt[1]) < 0.00001f && + std::fabs(light.distAtt[2]) < 0.00001f) { + light.distAtt[0] = 0.00001f; + } + + const double normSq = static_cast(light.dir[0]) * static_cast(light.dir[0]) + + static_cast(light.dir[1]) * static_cast(light.dir[1]) + + static_cast(light.dir[2]) * static_cast(light.dir[2]); + if (normSq == 0.0) { + light.dir[0] = 0.0f; + light.dir[1] = 0.0f; + light.dir[2] = 0.0f; + return light; + } + const double invNorm = 1.0 / std::sqrt(normSq); + // Aurora is built with fast-math, under which std::isnan/std::isinf may be optimized away. + light.dir[0] = sanitize_light_dir_component(static_cast(light.dir[0] * invNorm)); + light.dir[1] = sanitize_light_dir_component(static_cast(light.dir[1] * invNorm)); + light.dir[2] = sanitize_light_dir_component(static_cast(light.dir[2] * invNorm)); + return light; +} + +namespace { +Vec4 texture_size_bias(const gfx::TextureBind& tex) { + auto width = static_cast(tex.texObj.width()); + auto height = static_cast(tex.texObj.height()); + const auto vpBias = + enableLodBias && tex.ref && tex.ref->hasArbitraryMips + ? log2(std::min(g_gxState.renderViewport.width / std::max(g_gxState.logicalViewport.width, 1.f), + g_gxState.renderViewport.height / std::max(g_gxState.logicalViewport.height, 1.f))) + : 0.f; + return {width, height, tex.texObj.lod_bias() + vpBias, 0.0f}; +} + +Vec4 texcoord_scale(const TexCoordScale& scale) { + return {static_cast(scale.scaleS) + 1.0f, static_cast(scale.scaleT) + 1.0f, + scale.biasS ? 1.0f : 0.0f, scale.biasT ? 1.0f : 0.0f}; +} + +void mark_texture_sample(const TevStageTextureDependency& dependency, ShaderInfo& info) { + if (!dependency.canSampleTexture) { + return; + } + info.sampledTexCoords.set(static_cast(dependency.texCoordId)); + info.sampledTextures.set(static_cast(dependency.texMapId)); +} + +void mark_tev_reg_read(GXTevRegID reg, bool alpha, ShaderInfo& info) { + auto& writes = alpha ? info.writesTevRegAlpha : info.writesTevRegRgb; + auto& loads = alpha ? info.loadsTevRegAlpha : info.loadsTevRegRgb; + if (!writes.test(reg)) { + loads.set(reg); + } +} + +void color_arg_reg_info(GXTevColorArg arg, const TevStage& stage, + const TevStageTextureDependency& textureDependency, ShaderInfo& info) { + switch (arg) { + case GX_CC_CPREV: + mark_tev_reg_read(GX_TEVPREV, false, info); + break; + case GX_CC_APREV: + mark_tev_reg_read(GX_TEVPREV, true, info); + break; + case GX_CC_C0: + mark_tev_reg_read(GX_TEVREG0, false, info); + break; + case GX_CC_A0: + mark_tev_reg_read(GX_TEVREG0, true, info); + break; + case GX_CC_C1: + mark_tev_reg_read(GX_TEVREG1, false, info); + break; + case GX_CC_A1: + mark_tev_reg_read(GX_TEVREG1, true, info); + break; + case GX_CC_C2: + mark_tev_reg_read(GX_TEVREG2, false, info); + break; + case GX_CC_A2: + mark_tev_reg_read(GX_TEVREG2, true, info); + break; + case GX_CC_TEXC: + case GX_CC_TEXA: + mark_texture_sample(textureDependency, info); + break; + case GX_CC_RASC: + case GX_CC_RASA: + if (stage.channelId != GX_COLOR_NULL && stage.channelId != GX_COLOR_ZERO && + !is_alpha_bump_channel(stage.channelId)) { + size_t channel = 0; + if (sampled_color_channel(stage.channelId, channel)) { + info.sampledColorChannels.set(channel); + } + } + break; + case GX_CC_KONST: + switch (stage.kcSel) { + case GX_TEV_KCSEL_K0: + case GX_TEV_KCSEL_K0_R: + case GX_TEV_KCSEL_K0_G: + case GX_TEV_KCSEL_K0_B: + case GX_TEV_KCSEL_K0_A: + info.sampledKColors.set(0); + break; + case GX_TEV_KCSEL_K1: + case GX_TEV_KCSEL_K1_R: + case GX_TEV_KCSEL_K1_G: + case GX_TEV_KCSEL_K1_B: + case GX_TEV_KCSEL_K1_A: + info.sampledKColors.set(1); + break; + case GX_TEV_KCSEL_K2: + case GX_TEV_KCSEL_K2_R: + case GX_TEV_KCSEL_K2_G: + case GX_TEV_KCSEL_K2_B: + case GX_TEV_KCSEL_K2_A: + info.sampledKColors.set(2); + break; + case GX_TEV_KCSEL_K3: + case GX_TEV_KCSEL_K3_R: + case GX_TEV_KCSEL_K3_G: + case GX_TEV_KCSEL_K3_B: + case GX_TEV_KCSEL_K3_A: + info.sampledKColors.set(3); + break; + default: + break; + } + break; + default: + break; + } +} + +void alpha_arg_reg_info(GXTevAlphaArg arg, const TevStage& stage, + const TevStageTextureDependency& textureDependency, ShaderInfo& info) { + switch (arg) { + case GX_CA_APREV: + mark_tev_reg_read(GX_TEVPREV, true, info); + break; + case GX_CA_A0: + mark_tev_reg_read(GX_TEVREG0, true, info); + break; + case GX_CA_A1: + mark_tev_reg_read(GX_TEVREG1, true, info); + break; + case GX_CA_A2: + mark_tev_reg_read(GX_TEVREG2, true, info); + break; + case GX_CA_TEXA: + mark_texture_sample(textureDependency, info); + break; + case GX_CA_RASA: + if (stage.channelId != GX_COLOR_NULL && stage.channelId != GX_COLOR_ZERO && + !is_alpha_bump_channel(stage.channelId)) { + size_t channel = 0; + if (sampled_color_channel(stage.channelId, channel)) { + info.sampledColorChannels.set(channel); + } + } + break; + case GX_CA_KONST: + switch (stage.kaSel) { + case GX_TEV_KASEL_K0_R: + case GX_TEV_KASEL_K0_G: + case GX_TEV_KASEL_K0_B: + case GX_TEV_KASEL_K0_A: + info.sampledKColors.set(0); + break; + case GX_TEV_KASEL_K1_R: + case GX_TEV_KASEL_K1_G: + case GX_TEV_KASEL_K1_B: + case GX_TEV_KASEL_K1_A: + info.sampledKColors.set(1); + break; + case GX_TEV_KASEL_K2_R: + case GX_TEV_KASEL_K2_G: + case GX_TEV_KASEL_K2_B: + case GX_TEV_KASEL_K2_A: + info.sampledKColors.set(2); + break; + case GX_TEV_KASEL_K3_R: + case GX_TEV_KASEL_K3_G: + case GX_TEV_KASEL_K3_B: + case GX_TEV_KASEL_K3_A: + info.sampledKColors.set(3); + break; + default: + break; + } + break; + default: + break; + } +} +} // namespace + +ShaderInfo build_shader_info(const ShaderConfig& config) noexcept { + ZoneScoped; + + ShaderInfo info{ + // vtx_start, current_pnmtx, render/logical viewport size, array_start, pad, proj + .uniformSize = 4 + 4 + 8 + 8 + 8 + 48 + 64, + }; + + if (config.lineMode != 0) { + info.uniformSize += 4 + 4 + 4 + 4; // line_width, line_aspect_y, line_tex_offset, line_texcoord_mask + info.lineMode = config.lineMode; + } + + for (int attr = 0; attr < config.attrs.size(); attr++) { + const auto attrType = config.attrs[attr].attrType; + if ((attr == GX_VA_PNMTXIDX || (attr >= GX_VA_TEX0MTXIDX && attr <= GX_VA_TEX7MTXIDX)) && attrType == GX_DIRECT) { + info.indexAttr.set(attr); + } else if (attrType == GX_INDEX8 || attrType == GX_INDEX16) { + info.indexAttr.set(attr); + } + } + + for (int i = 0; i < config.tevStageCount; ++i) { + const auto& stage = config.tevStages[i]; + const auto textureDependency = tev_stage_texture_dependency(config, static_cast(i)); + // Color pass + color_arg_reg_info(stage.colorPass.a, stage, textureDependency, info); + color_arg_reg_info(stage.colorPass.b, stage, textureDependency, info); + color_arg_reg_info(stage.colorPass.c, stage, textureDependency, info); + color_arg_reg_info(stage.colorPass.d, stage, textureDependency, info); + info.writesTevRegRgb.set(stage.colorOp.outReg); + + // Alpha pass + alpha_arg_reg_info(stage.alphaPass.a, stage, textureDependency, info); + alpha_arg_reg_info(stage.alphaPass.b, stage, textureDependency, info); + alpha_arg_reg_info(stage.alphaPass.c, stage, textureDependency, info); + alpha_arg_reg_info(stage.alphaPass.d, stage, textureDependency, info); + info.writesTevRegAlpha.set(stage.alphaOp.outReg); + } + if (const int zTexStage = tev_z_texture_stage(config); zTexStage >= 0) { + mark_texture_sample(tev_stage_texture_dependency(config, static_cast(zTexStage)), info); + } + for (int i = 0; i < config.tevStageCount; ++i) { + const auto& stage = config.tevStages[i]; + const auto textureDependency = tev_stage_texture_dependency(config, static_cast(i)); + + // Coordinate state is independent of whether this stage has an enabled texture map. + if (textureDependency.needsFixedTexcoordState && textureDependency.texCoordId >= 0) { + info.sampledTexCoords.set(static_cast(textureDependency.texCoordId)); + } + + // Skip if not enabled + if (stage.indTexStage >= config.numIndStages) { + continue; + } + const bool usesIndStage = stage.indTexMtxId != GX_ITM_OFF || is_alpha_bump_channel(stage.channelId); + if (!usesIndStage) { + continue; + } + info.usedIndStages.set(stage.indTexStage); + const auto& indStage = config.indStages[stage.indTexStage]; + if (indStage.texMapId < MaxTextures) { + info.sampledTextures.set(indStage.texMapId); + info.sampledIndTextures.set(indStage.texMapId); + } + const int effectiveIndTexCoord = tev_effective_texcoord(config, indStage.texCoordId); + if (effectiveIndTexCoord >= 0) { + info.sampledTexCoords.set(static_cast(effectiveIndTexCoord)); + } + // Track which indirect matrix is used + if (stage.indTexMtxId >= GX_ITM_0 && stage.indTexMtxId <= GX_ITM_2) { + info.usedIndTexMtxs.set(stage.indTexMtxId - GX_ITM_0); + } else if (stage.indTexMtxId >= GX_ITM_S0 && stage.indTexMtxId <= GX_ITM_S2) { + info.usedIndTexMtxs.set(stage.indTexMtxId - GX_ITM_S0); + } else if (stage.indTexMtxId >= GX_ITM_T0 && stage.indTexMtxId <= GX_ITM_T2) { + info.usedIndTexMtxs.set(stage.indTexMtxId - GX_ITM_T0); + } + } + + const auto loadsTevRegs = info.loadsTevRegRgb | info.loadsTevRegAlpha; + info.uniformSize += loadsTevRegs.count() * sizeof(Vec4); + for (int i = 0; i < info.sampledColorChannels.size(); ++i) { + if (info.sampledColorChannels.test(i)) { + const auto& cc = config.colorChannels[i]; + const auto& cca = config.colorChannels[i + GX_ALPHA0]; + if (cc.lightingEnabled || cca.lightingEnabled) { + info.lightingEnabled = true; + } + } + } + if (info.lightingEnabled) { + // Lights + light state for all channels + info.uniformSize += 16 + sizeof(Light) * GX::MaxLights; + } + for (int i = 0; i < info.sampledColorChannels.size(); ++i) { + if (info.sampledColorChannels.test(i)) { + const auto& cc = config.colorChannels[i]; + if (cc.lightingEnabled && cc.ambSrc == GX_SRC_REG) { + info.uniformSize += sizeof(Vec4); + } + if (cc.matSrc == GX_SRC_REG) { + info.uniformSize += sizeof(Vec4); + } + const auto& cca = config.colorChannels[i + GX_ALPHA0]; + if (cca.lightingEnabled && cca.ambSrc == GX_SRC_REG) { + info.uniformSize += sizeof(Vec4); + } + if (cca.matSrc == GX_SRC_REG) { + info.uniformSize += sizeof(Vec4); + } + } + } + info.uniformSize += info.sampledKColors.count() * sizeof(Vec4); + for (int i = 0; i < info.sampledTexCoords.size(); ++i) { + if (!info.sampledTexCoords.test(i)) { + continue; + } + const auto& tcg = config.tcgs[i]; + if (config.dualTexEnabled && tcg.postMtx != GX_PTIDENTITY) { + u32 postMtxIdx = (tcg.postMtx - GX_PTTEXMTX0) / 3; + info.usesPTTexMtx.set(postMtxIdx); + } + } + if (info.usesPTTexMtx.any()) + info.uniformSize += sizeof(Mat3x4) * MaxPTTexMtx; + + // Resolve the matrix slots this shader can read. + { + auto& layout = info.matrixLayout; + layout.postexRemap.fill(UniformMatrixLayout::kAbsent); + + const bool dynamicPnMtx = config.attrs[GX_VA_PNMTXIDX].attrType != GX_NONE; + bool dynamicTexMtx = false; + std::bitset literalSlots; + for (size_t i = 0; i < info.sampledTexCoords.size(); ++i) { + if (!info.sampledTexCoords.test(i)) { + continue; + } + const auto& tcg = config.tcgs[i]; + if (tcg.type != GX_TG_MTX2x4 && tcg.type != GX_TG_MTX3x4) { + continue; + } + if (info.indexAttr.test(GX_VA_TEX0MTXIDX + i)) { + // A per-vertex texture matrix index addresses raw matrix memory and can name any row, including the position rows. + dynamicTexMtx = true; + continue; + } + if (tcg.mtx == GX_IDENTITY) { + continue; + } + const u32 slot = static_cast(tcg.mtx) / 3; + if (slot >= MaxPostexMtx) { + // Out of range for the shader array; preserve the uncompacted layout rather than inventing a different (still invalid) index. + dynamicTexMtx = true; + continue; + } + literalSlots.set(slot); + } + + // At most MaxPostexMtx entries can ever be pushed: the absolute layout pushes the ten position slots plus at most ten texture slots, and the compacted layout pushes the current matrix plus at most one literal slot per texgen. + const auto pushPostex = [&](u8 slot) { + CHECK(layout.postexCount < layout.postexSlots.size(), "postex matrix layout overflow"); + layout.postexSlots[layout.postexCount++] = slot; + }; + const auto pushNrm = [&](u8 slot) { + CHECK(layout.nrmCount < layout.nrmSlots.size(), "normal matrix layout overflow"); + layout.nrmSlots[layout.nrmCount++] = slot; + }; + + layout.absolutePosRegion = dynamicPnMtx || dynamicTexMtx; + if (!layout.absolutePosRegion) { + // Compact slot 0 always holds the matrix selected by the current matrix index; build_uniform writes 0 into `current_pnmtx` to match. + pushPostex(UniformMatrixLayout::kCurrentPnMtx); + pushNrm(UniformMatrixLayout::kCurrentPnMtx); + } + for (u32 slot = 0; slot < MaxPnMtx; ++slot) { + if (!layout.absolutePosRegion && !literalSlots.test(slot)) { + continue; + } + layout.postexRemap[slot] = layout.postexCount; + pushPostex(static_cast(slot)); + if (layout.absolutePosRegion) { + // The normal array is indexed by the same expression as the position array, so its compact indices have to line up with the position slots. + pushNrm(static_cast(slot)); + } + } + for (u32 slot = MaxPnMtx; slot < MaxPostexMtx; ++slot) { + if (!dynamicTexMtx && !literalSlots.test(slot)) { + continue; + } + layout.postexRemap[slot] = layout.postexCount; + pushPostex(static_cast(slot)); + } + // `postex_mtx[in_pnmtxidx]` and `nrm_mtx[in_pnmtxidx]` are emitted unconditionally, so neither array can ever be empty. + info.uniformSize += sizeof(Mat3x4) * (layout.postexCount + layout.nrmCount); + } + if (config.fogType != GX_FOG_NONE) { + info.usesFog = true; + info.uniformSize += sizeof(Fog); + } + if (info.usedIndTexMtxs.any()) { + info.uniformSize += MaxIndTexMtxs * 2 * sizeof(Vec4); + } + info.uniformSize += info.sampledTexCoords.count() * sizeof(Vec4); + info.uniformSize += info.sampledTextures.count() * sizeof(Vec4); + info.uniformSize = gfx::align_uniform(info.uniformSize); + if (info.uniformSize > MaxUniformSize) { + Log.fatal("Uniform size exceeds maximum: {} > {}", info.uniformSize, MaxUniformSize); + } + return info; +} + +static f32 tex_offset(GXTexOffset offs) noexcept { + switch (offs) { + DEFAULT_FATAL("invalid tex offset {}", underlying(offs)); + case GX_TO_ZERO: + return 0.f; + case GX_TO_SIXTEENTH: + return 1.f / 16.f; + case GX_TO_EIGHTH: + return 1.f / 8.f; + case GX_TO_FOURTH: + return 1.f / 4.f; + case GX_TO_HALF: + return 1.f / 2.f; + case GX_TO_ONE: + return 1.f; + } +} + +static u32 point_texcoord_mask() noexcept { + u32 mask = 0; + for (int i = 0; i < MaxTexCoord; ++i) { + if (g_gxState.texCoordScales[i].pointOffset) { + mask |= 1 << i; + } + } + return mask; +} + +static u32 line_texcoord_mask() noexcept { + u32 mask = 0; + for (int i = 0; i < MaxTexCoord; ++i) { + if (g_gxState.texCoordScales[i].lineOffset) { + mask |= 1 << i; + } + } + return mask; +} + +namespace { +// Largest possible staged prefix: scalar head (80) + line/point block (16) + projection matrix + every matrix the uncompacted layout can hold. +constexpr size_t kStagedUniformBytes = + 96 + sizeof(Mat4x4) + sizeof(Mat3x4) * (MaxPostexMtx + MaxPnMtx); + +// The host viewport always receives the normalized GX depth window (render_pass_impl clamps to minDepth <= maxDepth). +static Mat4x4 effective_projection() noexcept { + const auto& vp = g_gxState.renderViewport; + const bool flip = (vp.znear <= vp.zfar) == UseReversedZ; + Mat4x4 proj = g_gxState.proj; + if (flip) { + for (size_t i = 0; i < 4; ++i) { + proj.m2.m[i] = -(proj.m2.m[i] + proj.m3.m[i]); + } + } + return proj; +} +} // namespace + +UniformRanges build_uniform(const ShaderInfo& info, u32 vtxStart, const BindGroupRanges& ranges, + const FrameInterpolationDrawIdentity& drawIdentity, bool perspective, + uint16_t usedPnMtxMask) noexcept { + ZoneScoped; + + auto [buf, range] = gfx::map_uniform(info.uniformSize); + + const auto& layout = info.matrixLayout; + // `postex_mtx[current_pnmtx]` indexes raw matrix memory, so a current-matrix index of 10 or more selects a texture matrix; the normal array only covers the position rows and clamps. + const u32 currentPostexSlot = std::min(g_gxState.currentPnMtx, MaxPostexMtx - 1); + const u32 currentNrmSlot = std::min(g_gxState.currentPnMtx, MaxPnMtx - 1); + + // The mapped uniform range lives in write-combine memory, which wants long sequential stores. + alignas(16) std::array staged; + size_t stagedSize = 0; + const auto stage = [&](const void* data, size_t size) noexcept { + std::memcpy(staged.data() + stagedSize, data, size); + stagedSize += size; + }; + const auto stage_u32 = [&](u32 value) noexcept { stage(&value, sizeof(value)); }; + const auto stage_f32 = [&](f32 value) noexcept { stage(&value, sizeof(value)); }; + + stage_u32(vtxStart); + // With a compacted position region the live matrix is uploaded to slot 0, so every `postex_mtx[in_pnmtxidx]` / `nrm_mtx[in_pnmtxidx]` read has to resolve to 0 as well. + stage_u32(layout.absolutePosRegion ? g_gxState.currentPnMtx : 0u); + stage_f32(g_gxState.renderViewport.width); + stage_f32(g_gxState.renderViewport.height); + stage_f32(g_gxState.logicalViewport.width); + stage_f32(g_gxState.logicalViewport.height); + std::memset(staged.data() + stagedSize, 0, 8); // pad + stagedSize += 8; + for (const auto& vaRange : ranges.vaRanges) { + stage_u32(vaRange.offset); + } + if (info.lineMode != 0) { + if (info.lineMode == 3) { // GX_POINTS + stage_f32(static_cast(g_gxState.pointSize) / 6.f); + stage_f32(1.0f); + stage_f32(tex_offset(g_gxState.pointTexOffset)); + stage_u32(point_texcoord_mask()); + } else { // GX_LINES / GX_LINESTRIP + stage_f32(static_cast(g_gxState.lineWidth) / 6.f); + stage_f32(g_gxState.lineHalfAspect ? 0.5f : 1.f); + stage_f32(tex_offset(g_gxState.lineTexOffset)); + stage_u32(line_texcoord_mask()); + } + } + const size_t projectionOffset = stagedSize; + const Mat4x4 effectiveProj = effective_projection(); + stage(&effectiveProj, sizeof(effectiveProj)); + + const size_t positionOffset = stagedSize; + for (u32 i = 0; i < layout.postexCount; ++i) { + const u32 slot = + layout.postexSlots[i] == UniformMatrixLayout::kCurrentPnMtx ? currentPostexSlot + : layout.postexSlots[i]; + stage(slot < MaxPnMtx ? &g_gxState.pnMtx[slot].pos : &g_gxState.texMtxs[slot - MaxPnMtx], + sizeof(Mat3x4)); + } + + const size_t normalOffset = stagedSize; + for (u32 i = 0; i < layout.nrmCount; ++i) { + const u32 slot = layout.nrmSlots[i] == UniformMatrixLayout::kCurrentPnMtx ? currentNrmSlot + : layout.nrmSlots[i]; + stage(&g_gxState.pnMtx[slot].nrm, sizeof(Mat3x4)); + } + buf.append(staged.data(), stagedSize); + + const auto loadsTevRegs = info.loadsTevRegRgb | info.loadsTevRegAlpha; + for (int i = 0; i < loadsTevRegs.size(); ++i) { + if (loadsTevRegs.test(i)) { + buf.append(g_gxState.colorRegs[i]); + } + } + if (info.lightingEnabled) { + // Sanitizing and normalizing light directions is substantially more expensive than copying the uniform data, while lights normally remain unchanged across many draws. + static_assert(sizeof(g_gxState.lights) == 80 * GX::MaxLights); + if (g_gxState.preparedLightsDirty) { + for (size_t i = 0; i < g_gxState.preparedLights.size(); ++i) { + g_gxState.preparedLights[i] = prepare_shader_light(g_gxState.lights[i]); + } + g_gxState.preparedLightsDirty = false; + } + buf.append(g_gxState.preparedLights); + // Light state for all channels + for (int i = 0; i < 4; ++i) { + buf.append(g_gxState.colorChannelState[i].lightMask.to_ulong()); + } + } + for (int i = 0; i < info.sampledColorChannels.size(); ++i) { + if (!info.sampledColorChannels.test(i)) { + continue; + } + const auto& ccc = g_gxState.colorChannelConfig[i]; + const auto& ccs = g_gxState.colorChannelState[i]; + if (ccc.lightingEnabled && ccc.ambSrc == GX_SRC_REG) { + buf.append(ccs.ambColor); + } + if (ccc.matSrc == GX_SRC_REG) { + buf.append(ccs.matColor); + } + const auto& ccca = g_gxState.colorChannelConfig[i + GX_ALPHA0]; + const auto& ccsa = g_gxState.colorChannelState[i + GX_ALPHA0]; + if (ccca.lightingEnabled && ccca.ambSrc == GX_SRC_REG) { + buf.append(ccsa.ambColor); + } + if (ccca.matSrc == GX_SRC_REG) { + buf.append(ccsa.matColor); + } + } + for (int i = 0; i < info.sampledKColors.size(); ++i) { + if (info.sampledKColors.test(i)) { + buf.append(g_gxState.kcolors[i]); + } + } + if (info.usesPTTexMtx.any()) { + for (int i = 0; i < info.usesPTTexMtx.size(); ++i) { + buf.append(g_gxState.ptTexMtxs[i]); + } + } + if (info.usesFog) { + buf.append(fog_uniform()); + } + if (info.usedIndTexMtxs.any()) { + for (int i = 0; i < MaxIndTexMtxs; ++i) { + const auto& mtx = g_gxState.indTexMtxs[i]; + buf.append(Vec4{indirect_matrix_mantissa(mtx.mtx.m0.x), indirect_matrix_mantissa(mtx.mtx.m0.y), + indirect_matrix_mantissa(mtx.mtx.m1.x), indirect_matrix_mantissa(mtx.mtx.m1.y)}); + buf.append(Vec4{indirect_matrix_mantissa(mtx.mtx.m2.x), indirect_matrix_mantissa(mtx.mtx.m2.y), + indirect_matrix_shift(mtx.scaleExp), 0}); + } + } + for (int i = 0; i < info.sampledTexCoords.size(); ++i) { + if (info.sampledTexCoords.test(i)) { + buf.append(texcoord_scale(g_gxState.texCoordScales[i])); + } + } + for (int i = 0; i < info.sampledTextures.size(); ++i) { + if (!info.sampledTextures.test(i)) { + continue; + } + const auto& tex = get_texture(static_cast(i)); + // CHECK(tex, "unbound texture {}", i); + buf.append(texture_size_bias(tex)); + } + + if (!perspective || frame_interpolation_fps() == 0) { + g_gxState.stateDirty = false; + return { + .current = range, + .interpolated = {}, + }; + } + + const auto interpolatedRanges = record_interpolation_draw( + drawIdentity, effectiveProj, usedPnMtxMask, + InterpolatedUniformLayout{ + .sourceUniformData = buf.data(), + .uniformSize = range.size, + .projectionOffset = projectionOffset, + .positionOffset = positionOffset, + .normalOffset = normalOffset, + // A compacted position region holds the current matrix at slot 0. + .currentMatrix = layout.absolutePosRegion + ? std::min(g_gxState.currentPnMtx, MaxPnMtx - 1) + : 0, + .indexedMatrices = info.indexAttr.test(GX_VA_PNMTXIDX), + }); + g_gxState.stateDirty = false; + return { + .current = range, + .interpolated = interpolatedRanges, + }; +} +} // namespace aurora::gx diff --git a/aurora-main/lib/gx/shader_info.hpp b/aurora-main/lib/gx/shader_info.hpp new file mode 100644 index 0000000..6b8e329 --- /dev/null +++ b/aurora-main/lib/gx/shader_info.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "gx.hpp" +// MaxInterpolatedFrames / FrameInterpolationDrawIdentity, and the interpolation API that build_uniform feeds. +#include "frame_interpolation.hpp" + +namespace aurora::gx { +struct UniformRanges { + gfx::Range current; + std::array interpolated; +}; + +ShaderInfo build_shader_info(const ShaderConfig& config) noexcept; +Light prepare_shader_light(Light light) noexcept; +UniformRanges build_uniform(const ShaderInfo& info, uint32_t vtxStart, const BindGroupRanges& ranges, + const FrameInterpolationDrawIdentity& drawIdentity, bool perspective, + uint16_t usedPnMtxMask = 1) noexcept; +u8 color_channel(GXChannelID id) noexcept; +}; // namespace aurora::gx diff --git a/aurora-main/lib/gx/texture_bind_group_cache_key.hpp b/aurora-main/lib/gx/texture_bind_group_cache_key.hpp new file mode 100644 index 0000000..5cb78a0 --- /dev/null +++ b/aurora-main/lib/gx/texture_bind_group_cache_key.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include +#include +#include + +namespace aurora::gx { + +struct TextureBindGroupCacheKey { + std::uint64_t sampledTextures = 0; + std::uint64_t sampledIndTextures = 0; + std::array textureViews{}; + std::array mode0{}; + std::array mode1{}; + std::array refState{}; + + bool operator==(const TextureBindGroupCacheKey&) const = default; +}; + +inline void set_texture_bind_group_cache_slot(TextureBindGroupCacheKey& key, std::size_t slot, + const void* textureView, std::uint32_t mode0, + std::uint32_t mode1, std::uint32_t refState) noexcept { + key.textureViews[slot] = textureView; + key.mode0[slot] = mode0; + key.mode1[slot] = mode1; + key.refState[slot] = refState; +} + +} // namespace aurora::gx diff --git a/aurora-main/lib/imgui.cpp b/aurora-main/lib/imgui.cpp new file mode 100644 index 0000000..6129e37 --- /dev/null +++ b/aurora-main/lib/imgui.cpp @@ -0,0 +1,253 @@ +#include "imgui.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "internal.hpp" +#include "webgpu/gpu.hpp" +#include "window.hpp" + +#define IMGUI_IMPL_WEBGPU_BACKEND_DAWN +#include "backends/imgui_impl_sdl3.h" +#include "backends/imgui_impl_sdlrenderer3.h" +#include "backends/imgui_impl_wgpu.h" +#include "tracy/Tracy.hpp" + +namespace aurora::imgui { +static float g_scale; +static std::string g_imguiLog{}; +static bool g_useSdlRenderer = false; +// Set once ImGui::Render() has produced this frame's draw data. Interpolation encodes up to four +// ImGui passes per frame, and every one of them used to rebuild the draw lists from scratch. +static bool g_frameDataBuilt = false; + +static std::vector g_sdlTextures; +static std::vector g_wgpuTextures; + +void remove_legacy_ini_file(const char* basePath) noexcept { + if (basePath == nullptr || *basePath == '\0') { + return; + } + + std::error_code ec; + std::filesystem::remove(std::filesystem::path{basePath} / "imgui.ini", ec); +} + +void create_context() noexcept { + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + remove_legacy_ini_file(g_config.userPath); + remove_legacy_ini_file(g_config.cachePath); + g_imguiLog = std::string{g_config.cachePath} + "/imgui.log"; + io.IniFilename = nullptr; + io.LogFilename = g_imguiLog.c_str(); + ImGui::LoadIniSettingsFromMemory("", 0); + io.WantSaveIniSettings = false; +} + +void initialize() noexcept { + ZoneScoped; + SDL_Renderer* renderer = window::get_sdl_renderer(); + ImGui_ImplSDL3_InitForSDLRenderer(window::get_sdl_window(), renderer); + g_useSdlRenderer = renderer != nullptr; + if (g_useSdlRenderer) { + ImGui_ImplSDLRenderer3_Init(renderer); + } else { + ImGui_ImplWGPU_InitInfo info; + info.Device = webgpu::g_device.Get(); + info.RenderTargetFormat = static_cast(webgpu::g_graphicsConfig.surfaceConfiguration.format); + // Interpolation records up to four ImGui passes in one command buffer, so keep three logical + // frames of renderer resources to stop them overwriting each other's vertex/index buffers. + info.NumFramesInFlight = 12; + ImGui_ImplWGPU_Init(&info); + } +} + +void shutdown() noexcept { + ZoneScoped; + if (g_useSdlRenderer) { + ImGui_ImplSDLRenderer3_Shutdown(); + } else { + ImGui_ImplWGPU_Shutdown(); + } + ImGui_ImplSDL3_Shutdown(); + ImGui::DestroyContext(); + for (const auto& texture : g_sdlTextures) { + SDL_DestroyTexture(texture); + } + g_sdlTextures.clear(); + g_wgpuTextures.clear(); +} + +void process_event(const SDL_Event& event) noexcept { + auto renderEvent = event; + if (g_useSdlRenderer) { + if (SDL_Renderer* renderer = window::get_sdl_renderer()) { + SDL_ConvertEventToRenderCoordinates(renderer, &renderEvent); + } + } + ImGui_ImplSDL3_ProcessEvent(&renderEvent); +} + +bool wants_capture_event(const SDL_Event& event) noexcept { + if (ImGui::GetCurrentContext() == nullptr) { + return false; + } + + const ImGuiIO& io = ImGui::GetIO(); + switch (event.type) { + case SDL_EVENT_MOUSE_MOTION: + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: + case SDL_EVENT_MOUSE_WHEEL: + case SDL_EVENT_FINGER_DOWN: + case SDL_EVENT_FINGER_MOTION: + case SDL_EVENT_FINGER_UP: + case SDL_EVENT_FINGER_CANCELED: + return io.WantCaptureMouse; + case SDL_EVENT_KEY_DOWN: + case SDL_EVENT_KEY_UP: + case SDL_EVENT_TEXT_INPUT: + return io.WantCaptureKeyboard || io.WantTextInput; + default: + return false; + } +} + +void new_frame(const AuroraWindowSize& size) noexcept { + ZoneScoped; + ImVec2 framebufferScale{ + size.width > 0 ? static_cast(size.native_fb_width) / static_cast(size.width) : 1.0f, + size.height > 0 ? static_cast(size.native_fb_height) / static_cast(size.height) : 1.0f, + }; + ImVec2 displaySize{static_cast(size.width), static_cast(size.height)}; + + if (g_useSdlRenderer) { + if (SDL_Renderer* renderer = window::get_sdl_renderer()) { + float renderScaleX = 1.0f; + float renderScaleY = 1.0f; + SDL_GetRenderScale(renderer, &renderScaleX, &renderScaleY); + if (renderScaleX > 0.0f && renderScaleY > 0.0f && + (std::fabs(renderScaleX - 1.0f) > 0.0001f || std::fabs(renderScaleY - 1.0f) > 0.0001f)) { + int outputWidth = static_cast(size.native_fb_width); + int outputHeight = static_cast(size.native_fb_height); + SDL_GetRenderOutputSize(renderer, &outputWidth, &outputHeight); + displaySize = { + static_cast(outputWidth) / renderScaleX, + static_cast(outputHeight) / renderScaleY, + }; + framebufferScale = {renderScaleX, renderScaleY}; + } + } + ImGui_ImplSDLRenderer3_NewFrame(); + g_scale = size.scale; + } else { + if (g_scale != size.scale) { + if (g_scale > 0.f) { + ImGui_ImplWGPU_CreateDeviceObjects(); + } + g_scale = size.scale; + } + if (!ImGui::GetIO().Fonts->IsBuilt()) { + ImGui_ImplWGPU_CreateDeviceObjects(); + } + ImGui_ImplWGPU_NewFrame(); + } + ImGui_ImplSDL3_NewFrame(); + + ImGuiIO& io = ImGui::GetIO(); + io.DisplayFramebufferScale = framebufferScale; + ImGui::GetIO().DisplaySize = displaySize; + ImGui::NewFrame(); + g_frameDataBuilt = false; +} + +void render_frame_data() noexcept { + ZoneScoped; + if (g_frameDataBuilt) { + return; + } + ImGui::Render(); + auto* data = ImGui::GetDrawData(); + data->FramebufferScale = ImGui::GetIO().DisplayFramebufferScale; + g_frameDataBuilt = true; +} + +void render(const wgpu::RenderPassEncoder& pass) noexcept { + ZoneScoped; + render_frame_data(); + + auto* data = ImGui::GetDrawData(); + if (g_useSdlRenderer) { + SDL_Renderer* renderer = window::get_sdl_renderer(); + SDL_RenderClear(renderer); + ImGui_ImplSDLRenderer3_RenderDrawData(data, renderer); + SDL_RenderPresent(renderer); + } else { + pass.PushDebugGroup("Aurora: Dear Imgui"); + ImGui_ImplWGPU_RenderDrawData(data, pass.Get()); + pass.PopDebugGroup(); + } +} + +ImTextureID add_texture(uint32_t width, uint32_t height, const uint8_t* data) noexcept { + if (SDL_Renderer* renderer = window::get_sdl_renderer()) { + SDL_Texture* texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STATIC, width, height); + SDL_UpdateTexture(texture, nullptr, data, width * 4); + SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_LINEAR); + g_sdlTextures.push_back(texture); + return reinterpret_cast(texture); + } + const wgpu::Extent3D size{ + .width = width, + .height = height, + .depthOrArrayLayers = 1, + }; + const wgpu::TextureDescriptor textureDescriptor{ + .label = "imgui texture", + .usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopyDst, + .dimension = wgpu::TextureDimension::e2D, + .size = size, + .format = wgpu::TextureFormat::RGBA8Unorm, + .mipLevelCount = 1, + .sampleCount = 1, + }; + const wgpu::TextureViewDescriptor textureViewDescriptor{ + .label = "imgui texture view", + .format = wgpu::TextureFormat::RGBA8Unorm, + .dimension = wgpu::TextureViewDimension::e2D, + .mipLevelCount = WGPU_MIP_LEVEL_COUNT_UNDEFINED, + .arrayLayerCount = WGPU_ARRAY_LAYER_COUNT_UNDEFINED, + }; + auto texture = webgpu::g_device.CreateTexture(&textureDescriptor); + auto textureView = texture.CreateView(&textureViewDescriptor); + { + const wgpu::TexelCopyTextureInfo dstView{ + .texture = texture, + }; + const wgpu::TexelCopyBufferLayout dataLayout{ + .bytesPerRow = 4 * width, + .rowsPerImage = height, + }; + webgpu::g_queue.WriteTexture(&dstView, data, width * height * 4, &dataLayout, &size); + } + g_wgpuTextures.push_back(texture); + return reinterpret_cast(textureView.MoveToCHandle()); +} +} // namespace aurora::imgui + +// C bindings +extern "C" { +ImTextureID aurora_imgui_add_texture(uint32_t width, uint32_t height, const void* rgba8) { + return aurora::imgui::add_texture(width, height, static_cast(rgba8)); +} +} diff --git a/aurora-main/lib/imgui.hpp b/aurora-main/lib/imgui.hpp new file mode 100644 index 0000000..908a122 --- /dev/null +++ b/aurora-main/lib/imgui.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +union SDL_Event; + +namespace wgpu { +class RenderPassEncoder; +} // namespace wgpu + +namespace aurora::imgui { +void create_context() noexcept; +void initialize() noexcept; +void shutdown() noexcept; + +void process_event(const SDL_Event& event) noexcept; +bool wants_capture_event(const SDL_Event& event) noexcept; +void new_frame(const AuroraWindowSize& size) noexcept; +// Build this frame's ImGui draw data. Idempotent for the rest of the frame: the lists are built +// once and every presentation slot replays them. Reset by new_frame. +void render_frame_data() noexcept; +void render(const wgpu::RenderPassEncoder& pass) noexcept; +} // namespace aurora::imgui diff --git a/aurora-main/lib/imgui_config.cpp b/aurora-main/lib/imgui_config.cpp new file mode 100644 index 0000000..e06eecb --- /dev/null +++ b/aurora-main/lib/imgui_config.cpp @@ -0,0 +1,61 @@ +#include + +#include + +#include + +ImFileHandle ImFileOpen(const char* filename, const char* mode) { + if (filename == nullptr || mode == nullptr) { + return nullptr; + } + return SDL_IOFromFile(filename, mode); +} + +bool ImFileClose(ImFileHandle file) { return file == nullptr || SDL_CloseIO(file); } + +ImU64 ImFileGetSize(ImFileHandle file) { + if (file == nullptr) { + return static_cast(-1); + } + const Sint64 size = SDL_GetIOSize(file); + if (size < 0) { + return static_cast(-1); + } + return static_cast(size); +} + +ImU64 ImFileRead(void* data, ImU64 size, ImU64 count, ImFileHandle file) { + ImU64 byteCount = size * count; + if (file == nullptr || data == nullptr || byteCount == 0) { + return 0; + } + + ImU64 totalRead = 0; + auto* dst = static_cast(data); + while (totalRead < byteCount) { + const size_t read = SDL_ReadIO(file, dst + totalRead, byteCount - totalRead); + if (read == 0) { + return totalRead / size; + } + totalRead += read; + } + return count; +} + +ImU64 ImFileWrite(const void* data, ImU64 size, ImU64 count, ImFileHandle file) { + ImU64 byteCount = size * count; + if (file == nullptr || data == nullptr || byteCount == 0) { + return 0; + } + + ImU64 totalWritten = 0; + auto* src = static_cast(data); + while (totalWritten < byteCount) { + const size_t written = SDL_WriteIO(file, src + totalWritten, byteCount - totalWritten); + if (written == 0) { + return totalWritten / size; + } + totalWritten += written; + } + return count; +} diff --git a/aurora-main/lib/input.cpp b/aurora-main/lib/input.cpp new file mode 100644 index 0000000..e5a2f56 --- /dev/null +++ b/aurora-main/lib/input.cpp @@ -0,0 +1,480 @@ +#include "input.hpp" +#include "internal.hpp" + +#include "magic_enum.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace std::string_view_literals; + +namespace aurora::input { +Module Log("aurora::input"); +absl::flat_hash_map g_GameControllers; + +namespace { +constexpr uint32_t kPortPreferencesMagic = SBIG('CPRT'); +constexpr uint32_t kPortPreferencesVersion = 2; +constexpr uint32_t kMaxPersistedStringLength = 256; + +enum class PortPreferenceState : uint8_t { + Unset = 0, + None = 1, + Controller = 2, +}; + +struct ControllerIdentity { + std::string guid; + std::string serial; +}; + +struct PortPreference { + PortPreferenceState state = PortPreferenceState::Unset; + ControllerIdentity identity; +}; + +std::array g_portPreferences; +bool g_portPreferencesLoaded = false; + +std::string port_preferences_path() { + if (g_config.userPath == nullptr) { + return {}; + } + + std::string path{g_config.userPath}; + if (!path.empty() && path.back() != '/' && path.back() != '\\') { + path += '/'; + } + path += "controller_ports.dat"; + return path; +} + +std::string normalize_serial(const char* serial) { + std::string normalized; + if (serial == nullptr) { + return normalized; + } + + for (const char* c = serial; *c != '\0'; ++c) { + if (*c == ':' || *c == '-') { + continue; + } + normalized += *c >= 'A' && *c <= 'Z' ? static_cast(*c - 'A' + 'a') : *c; + } + return normalized; +} + +bool serial_matches(const std::string& saved, const std::string& current) { + return !saved.empty() && !current.empty() && normalize_serial(saved.c_str()) == normalize_serial(current.c_str()); +} + +ControllerIdentity controller_identity(const GameController& controller) { + ControllerIdentity identity; + char guid[33] = {}; + SDL_GUIDToString(SDL_GetGamepadGUIDForID(SDL_GetGamepadID(controller.m_controller)), guid, sizeof(guid)); + identity.guid = guid; + identity.serial = normalize_serial(SDL_GetGamepadSerial(controller.m_controller)); + return identity; +} + +bool read_exact(SDL_IOStream* file, void* dst, size_t size) { + auto* bytes = static_cast(dst); + size_t total = 0; + while (total < size) { + const size_t read = SDL_ReadIO(file, bytes + total, size - total); + if (read == 0) { + return false; + } + total += read; + } + return true; +} + +bool write_exact(SDL_IOStream* file, const void* src, size_t size) { + auto* bytes = static_cast(src); + size_t total = 0; + while (total < size) { + const size_t written = SDL_WriteIO(file, bytes + total, size - total); + if (written == 0) { + return false; + } + total += written; + } + return true; +} + +template +bool read_value(SDL_IOStream* file, T& value) { + return read_exact(file, &value, sizeof(value)); +} + +template +bool write_value(SDL_IOStream* file, const T& value) { + return write_exact(file, &value, sizeof(value)); +} + +bool read_string(SDL_IOStream* file, std::string& value) { + uint32_t size = 0; + if (!read_value(file, size) || size > kMaxPersistedStringLength) { + return false; + } + + value.resize(size); + return size == 0 || read_exact(file, value.data(), size); +} + +bool write_string(SDL_IOStream* file, const std::string& value) { + const uint32_t size = static_cast(std::min(value.size(), kMaxPersistedStringLength)); + return write_value(file, size) && (size == 0 || write_exact(file, value.data(), size)); +} + +bool read_identity(SDL_IOStream* file, ControllerIdentity& identity) { + return read_string(file, identity.guid) && read_string(file, identity.serial); +} + +bool write_identity(SDL_IOStream* file, const ControllerIdentity& identity) { + return write_string(file, identity.guid) && write_string(file, identity.serial); +} + +bool read_port_preferences_file(std::array& preferences) { + const auto path = port_preferences_path(); + if (path.empty()) { + return true; + } + + SDL_IOStream* file = SDL_IOFromFile(path.c_str(), "rb"); + if (file == nullptr) { + return true; + } + + uint32_t magic = 0; + uint32_t version = 0; + bool ok = read_value(file, magic) && read_value(file, version) && magic == kPortPreferencesMagic && + version == kPortPreferencesVersion; + + for (auto& preference : preferences) { + uint8_t state = 0; + ControllerIdentity identity; + ok = ok && read_value(file, state) && state <= static_cast(PortPreferenceState::Controller) && + read_identity(file, identity); + if (ok) { + preference.state = static_cast(state); + preference.identity = std::move(identity); + } + } + + SDL_CloseIO(file); + if (!ok) { + Log.warn("Ignoring invalid controller port preference file '{}'", path); + } + return ok; +} + +void ensure_port_preferences_loaded() { + if (g_portPreferencesLoaded) { + return; + } + + std::array preferences; + if (read_port_preferences_file(preferences)) { + g_portPreferences = std::move(preferences); + } + g_portPreferencesLoaded = true; +} + +void save_port_preferences() { + const auto path = port_preferences_path(); + if (path.empty()) { + return; + } + + if (!SDL_CreateDirectory(g_config.userPath)) { + Log.warn("Failed to create controller port preference directory '{}': {}", g_config.userPath, SDL_GetError()); + return; + } + + SDL_IOStream* file = SDL_IOFromFile(path.c_str(), "wb"); + if (file == nullptr) { + Log.warn("Failed to open controller port preference file '{}': {}", path, SDL_GetError()); + return; + } + + bool ok = write_value(file, kPortPreferencesMagic) && write_value(file, kPortPreferencesVersion); + for (const auto& preference : g_portPreferences) { + const auto state = static_cast(preference.state); + ok = ok && write_value(file, state) && write_identity(file, preference.identity); + } + + if (!SDL_FlushIO(file)) { + ok = false; + } + if (!SDL_CloseIO(file)) { + ok = false; + } + if (!ok) { + Log.warn("Failed to write controller port preference file '{}': {}", path, SDL_GetError()); + } +} + +enum class IdentityMatch { + None, + Fallback, + Exact, +}; + +IdentityMatch identity_match(const ControllerIdentity& saved, const ControllerIdentity& current) { + if (saved.guid.empty()) { + return IdentityMatch::None; + } + if (saved.guid == current.guid) { + return saved.serial.empty() || serial_matches(saved.serial, current.serial) ? IdentityMatch::Exact + : IdentityMatch::None; + } + if (!serial_matches(saved.serial, current.serial)) { + return IdentityMatch::None; + } + + // Attempt to match against VID/PID as a fallback if the GUID changes + uint16_t savedVendor = 0; + uint16_t savedProduct = 0; + uint16_t currentVendor = 0; + uint16_t currentProduct = 0; + SDL_GetJoystickGUIDInfo(SDL_StringToGUID(saved.guid.c_str()), &savedVendor, &savedProduct, nullptr, nullptr); + SDL_GetJoystickGUIDInfo(SDL_StringToGUID(current.guid.c_str()), ¤tVendor, ¤tProduct, nullptr, nullptr); + return savedVendor != 0 && savedVendor == currentVendor && savedProduct != 0 && savedProduct == currentProduct + ? IdentityMatch::Fallback + : IdentityMatch::None; +} + +bool is_instance_claimed(const std::array& claimedControllers, size_t claimedCount, + Uint32 instance) { + return std::find(claimedControllers.begin(), claimedControllers.begin() + claimedCount, instance) != + claimedControllers.begin() + claimedCount; +} + +void apply_port_preferences() noexcept { + ensure_port_preferences_loaded(); + if (!std::any_of(g_portPreferences.begin(), g_portPreferences.end(), + [](const auto& preference) { return preference.state != PortPreferenceState::Unset; })) { + return; + } + + for (auto& [instance, controller] : g_GameControllers) { + const int32_t player = SDL_GetGamepadPlayerIndex(controller.m_controller); + if (player >= 0 && player < PAD_MAX_CONTROLLERS && g_portPreferences[player].state != PortPreferenceState::Unset) { + // Keep SDL's default player assignment from taking explicitly configured ports + SDL_SetGamepadPlayerIndex(controller.m_controller, -1); + } + } + + std::array claimedControllers{}; + size_t claimedCount = 0; + for (uint32_t port = 0; port < g_portPreferences.size(); ++port) { + const auto& preference = g_portPreferences[port]; + if (preference.state != PortPreferenceState::Controller) { + continue; + } + + Uint32 fallbackInstance = 0; + GameController* fallbackController = nullptr; + for (auto& [instance, controller] : g_GameControllers) { + if (is_instance_claimed(claimedControllers, claimedCount, instance)) { + continue; + } + + switch (identity_match(preference.identity, controller_identity(controller))) { + case IdentityMatch::Exact: + SDL_SetGamepadPlayerIndex(controller.m_controller, static_cast(port)); + claimedControllers[claimedCount++] = instance; + fallbackController = nullptr; + break; + case IdentityMatch::Fallback: + // Prefer any later exact match before claiming a fallback candidate + if (fallbackController == nullptr) { + fallbackInstance = instance; + fallbackController = &controller; + } + continue; + case IdentityMatch::None: + continue; + } + break; + } + + if (fallbackController != nullptr) { + SDL_SetGamepadPlayerIndex(fallbackController->m_controller, static_cast(port)); + claimedControllers[claimedCount++] = fallbackInstance; + } + } +} +} // namespace + +GameController* get_controller_for_player(uint32_t player) noexcept { + for (auto& [which, controller] : g_GameControllers) { + if (player_index(which) == player) { + return &controller; + } + } + + return nullptr; +} + +Sint32 get_instance_for_player(uint32_t player) noexcept { + for (const auto& [which, controller] : g_GameControllers) { + if (player_index(which) == player) { + return which; + } + } + + return {}; +} + +SDL_JoystickID add_controller(SDL_JoystickID which) noexcept { + if (g_GameControllers.contains(which)) { + return which; + } + auto* ctrl = SDL_OpenGamepad(which); + if (ctrl != nullptr) { + GameController controller; + controller.m_controller = ctrl; + controller.m_index = which; + controller.m_vid = SDL_GetGamepadVendor(ctrl); + controller.m_pid = SDL_GetGamepadProduct(ctrl); + if (controller.m_vid == 0x05ac /* USB_VENDOR_APPLE */ && controller.m_pid == 3) { + // Ignore Apple TV remote + SDL_CloseGamepad(ctrl); + return -1; + } + controller.m_isGameCube = controller.m_vid == 0x057E && controller.m_pid == 0x0337; + if (controller.m_isGameCube || + (SDL_GetGamepadType(ctrl) == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO && controller.m_pid == 0x2073)) { + controller.m_deadZones.emulateTriggers = false; + } + const auto props = SDL_GetGamepadProperties(ctrl); + controller.m_hasRumble = SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN, true); + controller.m_hasRgbLed = SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false); + SDL_JoystickID instance = SDL_GetJoystickID(SDL_GetGamepadJoystick(ctrl)); + g_GameControllers[instance] = controller; + apply_port_preferences(); + return instance; + } + + return -1; +} + +void remove_controller(Uint32 instance) noexcept { + if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) { + SDL_CloseGamepad(it->second.m_controller); + g_GameControllers.erase(it); + apply_port_preferences(); + } +} + +bool is_gamecube(Uint32 instance) noexcept { + if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) { + return it->second.m_isGameCube; + } + return false; +} + +int32_t player_index(Uint32 instance) noexcept { + if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) { + const int player = SDL_GetGamepadPlayerIndex(it->second.m_controller); + return player >= 0 ? player : it->second.m_playerIndex; + } + return -1; +} + +void set_player_index(Uint32 instance, Sint32 index) noexcept { + if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) { + SDL_SetGamepadPlayerIndex(it->second.m_controller, index); + it->second.m_playerIndex = index; + } +} + +std::string controller_name(Uint32 instance) noexcept { + if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) { + const auto* name = SDL_GetGamepadName(it->second.m_controller); + if (name != nullptr) { + return {name}; + } + } + return {}; +} + +bool controller_has_rumble(Uint32 instance) noexcept { + if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) { + return it->second.m_hasRumble; + } + return false; +} + +void controller_rumble(uint32_t instance, uint16_t low_freq_intensity, uint16_t high_freq_intensity, + uint16_t duration_ms) noexcept { + if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) { + SDL_RumbleGamepad(it->second.m_controller, low_freq_intensity, high_freq_intensity, duration_ms); + } +} + +uint32_t controller_count() noexcept { return g_GameControllers.size(); } + +void persist_controller_for_player(uint32_t player, const GameController* controller) noexcept { + if (player >= PAD_MAX_CONTROLLERS) { + return; + } + + ensure_port_preferences_loaded(); + if (controller != nullptr) { + g_portPreferences[player].state = PortPreferenceState::Controller; + g_portPreferences[player].identity = controller_identity(*controller); + } else { + g_portPreferences[player].state = PortPreferenceState::None; + g_portPreferences[player].identity = {}; + } + save_port_preferences(); +} + +void initialize() noexcept { + /* Make sure we initialize everything input related now, this will automatically add all of the connected controllers + * as expected */ + ASSERT(SDL_Init(SDL_INIT_HAPTIC | SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD), "Failed to initialize SDL subsystems: {}", + SDL_GetError()); +} + +struct MouseScrollStatus { + float scrollX; + float scrollY; +}; + +static MouseScrollStatus g_MouseStatus; + +void set_mouse_scroll(const float scrollX, const float scrollY) noexcept { + g_MouseStatus.scrollX = scrollX; + g_MouseStatus.scrollY = scrollY; +} + +void get_mouse_scroll(float* scrollX, float* scrollY) noexcept { + *scrollX = g_MouseStatus.scrollX; + *scrollY = g_MouseStatus.scrollY; +} + +void shutdown() noexcept { + // Upon shutdown we want to ensure all controllers are in a default state, so force all rumble supporting controllers + // to shut off their rumble motors. + for (const auto& controller : g_GameControllers) { + if (!controller.second.m_hasRumble) { + continue; + } + controller_rumble(controller.first, 0, 0, 0); + } +} +} // namespace aurora::input diff --git a/aurora-main/lib/input.hpp b/aurora-main/lib/input.hpp new file mode 100644 index 0000000..b839865 --- /dev/null +++ b/aurora-main/lib/input.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include "dolphin/pad.h" // For PADDeaZones and PADButtonMapping +#include "SDL3/SDL_gamepad.h" +#include "SDL3/SDL_keyboard.h" +#include "SDL3/SDL_keycode.h" +#include "SDL3/SDL_mouse.h" +#include "logging.hpp" + +#include + +namespace aurora::input { +extern Module Log; + +struct GameController { + SDL_Gamepad* m_controller = nullptr; + bool m_isGameCube = false; + Sint32 m_index = -1; + Sint32 m_playerIndex = -1; + bool m_hasRumble = false; + PADDeadZones m_deadZones{ + .emulateTriggers = true, + .useDeadzones = true, + .stickDeadZone = 8000, + .substickDeadZone = 8000, + .leftTriggerActivationZone = 31150, + .rightTriggerActivationZone = 31150, + }; + uint16_t m_vid = 0; + uint16_t m_pid = 0; + std::array m_buttonMapping{}; + // Secondary binding per GC button; not persisted in .controller files, the + // runtime re-applies it from its own config whenever a controller attaches. + std::array m_altButtonMapping{}; + std::array m_axisMapping{}; + uint16_t m_rumbleIntensityLow = 32767; + uint16_t m_rumbleIntensityHigh = 32767; + bool m_mappingLoaded = false; + constexpr bool operator==(const GameController& other) const { + return m_controller == other.m_controller && m_index == other.m_index; + } + uint8_t m_ledRed = 0xFF; + uint8_t m_ledGreen = 0xFF; + uint8_t m_ledBlue = 0xFF; + bool m_isColorDirty = true; + bool m_hasRgbLed = false; +}; + +GameController* get_controller_for_player(uint32_t player) noexcept; +Sint32 get_instance_for_player(uint32_t player) noexcept; +SDL_JoystickID add_controller(SDL_JoystickID which) noexcept; +void remove_controller(Uint32 instance) noexcept; +Sint32 player_index(Uint32 instance) noexcept; +void set_player_index(Uint32 instance, Sint32 index) noexcept; +std::string controller_name(Uint32 instance) noexcept; +bool is_gamecube(Uint32 instance) noexcept; +bool controller_has_rumble(Uint32 instance) noexcept; +void controller_rumble(uint32_t instance, uint16_t low_freq_intensity, uint16_t high_freq_intensity, + uint16_t duration_ms) noexcept; +uint32_t controller_count() noexcept; +void initialize() noexcept; +void persist_controller_for_player(uint32_t player, const GameController* controller) noexcept; +extern absl::flat_hash_map g_GameControllers; + +void set_mouse_scroll(float scrollX, float scrollY) noexcept; +void get_mouse_scroll(float* scrollX, float* scrollY) noexcept; + +void shutdown() noexcept; +} // namespace aurora::input diff --git a/aurora-main/lib/internal.hpp b/aurora-main/lib/internal.hpp new file mode 100644 index 0000000..20169a4 --- /dev/null +++ b/aurora-main/lib/internal.hpp @@ -0,0 +1,197 @@ +#pragma once + +#include "logging.hpp" // IWYU pragma: keep + +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace std::string_view_literals; + +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ +#ifndef SBIG +#define SBIG(q) \ + (((q) & 0x000000FF) << 24 | ((q) & 0x0000FF00) << 8 | ((q) & 0x00FF0000) >> 8 | ((q) & 0xFF000000) >> 24) +#endif +#else +#ifndef SBIG +#define SBIG(q) (q) +#endif +#endif + +template + requires(sizeof(T) == sizeof(uint16_t) && std::is_arithmetic_v) +constexpr T bswap(T val) noexcept { + union { + uint16_t u; + T t; + } v{.t = val}; +#if __GNUC__ + v.u = __builtin_bswap16(v.u); +#elif _WIN32 + v.u = _byteswap_ushort(v.u); +#else + v.u = (v.u << 8) | ((v.u >> 8) & 0xFF); +#endif + return v.t; +} + +template + requires(sizeof(T) == sizeof(uint32_t) && std::is_arithmetic_v) +constexpr T bswap(T val) noexcept { + union { + uint32_t u; + T t; + } v{.t = val}; +#if __GNUC__ + v.u = __builtin_bswap32(v.u); +#elif _WIN32 + v.u = _byteswap_ulong(v.u); +#else + v.u = ((v.u & 0x0000FFFF) << 16) | ((v.u & 0xFFFF0000) >> 16) | ((v.u & 0x00FF00FF) << 8) | ((v.u & 0xFF00FF00) >> 8); +#endif + return v.t; +} + +template + requires(sizeof(T) == sizeof(uint64_t) && std::is_arithmetic_v) +constexpr T bswap(T val) noexcept { + union { + uint64_t u; + T t; + } v{.t = val}; +#if __GNUC__ + v.u = __builtin_bswap64(v.u); +#elif _WIN32 + v.u = _byteswap_uint64(v.u); +#else + static_assert(false, "bswap 64bit not implemented on this target"); +#endif + return v.t; +} + +template + requires(std::is_enum_v) +auto underlying(T value) -> std::underlying_type_t { + return static_cast>(value); +} + +#define AURORA_ALIGN(x, a) (((x) + ((a) - 1)) & ~((a) - 1)) + +#define POINTER_ADD_TYPE(type_, ptr_, offset_) ((type_)((uintptr_t)(ptr_) + (uintptr_t)(offset_))) +#define POINTER_ADD(ptr_, offset_) POINTER_ADD_TYPE(decltype(ptr_), ptr_, offset_) + +#if !defined(__has_cpp_attribute) +#define __has_cpp_attribute(name) 0 +#endif +#if __has_cpp_attribute(unlikely) +#define UNLIKELY [[unlikely]] +#else +#define UNLIKELY +#endif +#if __has_cpp_attribute(likely) +#define LIKELY [[likely]] +#else +#define LIKELY +#endif +#define FATAL(msg, ...) Log.fatal(msg, ##__VA_ARGS__); +#define ASSERT(cond, msg, ...) \ + if (!(cond)) \ + UNLIKELY FATAL(msg, ##__VA_ARGS__) +#ifdef NDEBUG +#define CHECK(cond, msg, ...) +#else +#define CHECK(cond, msg, ...) ASSERT(cond, msg, ##__VA_ARGS__) +#endif +#define DEFAULT_FATAL(msg, ...) UNLIKELY default : FATAL(msg, ##__VA_ARGS__) +#define TRY(cond, msg, ...) \ + if (!(cond)) \ + UNLIKELY { \ + Log.error(msg, ##__VA_ARGS__); \ + return false; \ + } +#define TRY_WARN(cond, msg, ...) \ + if (!(cond)) \ + UNLIKELY { Log.warn(msg, ##__VA_ARGS__); } + +#define UNIMPLEMENTED() FATAL("UNIMPLEMENTED: {}", __FUNCTION__) + +namespace aurora { +extern AuroraConfig g_config; +extern uint32_t g_sdlCustomEventsStart; +extern char g_gameName[4]; + +// wait_for_frame_worker() joins the DONE phase (ImGui, surface, sealing another frame need this). +// wait_for_frame_worker_sealed() joins only SEALED, which is what the FIFO drain uses. +void wait_for_frame_worker() noexcept; +std::chrono::nanoseconds wait_for_frame_worker_sealed() noexcept; +bool wait_for_frame_worker_for(std::chrono::microseconds timeout) noexcept; +std::recursive_mutex& renderer_gpu_mutex() noexcept; + +template +class ArrayRef { +public: + using value_type = std::remove_cvref_t; + using pointer = value_type*; + using const_pointer = const value_type*; + using reference = value_type&; + using const_reference = const value_type&; + using iterator = const_pointer; + using const_iterator = const_pointer; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + ArrayRef() = default; + explicit ArrayRef(const T& one) : ptr(&one), length(1) {} + ArrayRef(const T* data, size_t length) : ptr(data), length(length) {} + ArrayRef(const T* begin, const T* end) : ptr(begin), length(end - begin) {} + template + constexpr ArrayRef(const T (&arr)[N]) : ptr(arr), length(N) {} + template + constexpr ArrayRef(const std::array& arr) : ptr(arr.data()), length(arr.size()) {} + ArrayRef(const std::vector& vec) : ptr(vec.data()), length(vec.size()) {} + + const T* data() const { return ptr; } + size_t size() const { return length; } + bool empty() const { return length == 0; } + + const T& front() const { + assert(!empty()); + return ptr[0]; + } + const T& back() const { + assert(!empty()); + return ptr[length - 1]; + } + const T& operator[](size_t i) const { + assert(i < length && "Invalid index!"); + return ptr[i]; + } + + iterator begin() const { return ptr; } + iterator end() const { return ptr + length; } + + reverse_iterator rbegin() const { return reverse_iterator(end()); } + reverse_iterator rend() const { return reverse_iterator(begin()); } + + /// Disallow accidental assignment from a temporary. + template + std::enable_if_t::value, ArrayRef>& operator=(U&& Temporary) = delete; + + /// Disallow accidental assignment from a temporary. + template + std::enable_if_t::value, ArrayRef>& operator=(std::initializer_list) = delete; + +private: + const T* ptr = nullptr; + size_t length = 0; +}; +} // namespace aurora diff --git a/aurora-main/lib/logging.cpp b/aurora-main/lib/logging.cpp new file mode 100644 index 0000000..609d5d2 --- /dev/null +++ b/aurora-main/lib/logging.cpp @@ -0,0 +1,78 @@ +#include "logging.hpp" + +#include +#include +#include + +#include +#include +#include + +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#endif + +namespace aurora { +extern AuroraConfig g_config; + +void Module::show_fatal_dialog(const char* module, std::string_view message) noexcept { + try { + std::string body = "Aurora stopped because of a fatal renderer error"; + if (module != nullptr && module[0] != '\0') { + body += " in "; + body += module; + } + body += ":\n\n"; + body.append(message.data(), message.size()); + body += "\n\nSee the console and log files for more details."; +#if defined(_WIN32) + ::MessageBoxA(nullptr, body.c_str(), "Aurora fatal error", + MB_OK | MB_ICONERROR | MB_SETFOREGROUND | MB_TASKMODAL); +#else + std::fprintf(stderr, "[aurora] fatal dialog: %s\n", body.c_str()); +#endif + } catch (...) { + // Fatal reporting must never hide the original abort. + } +} + +void log_internal(const AuroraLogLevel level, const char* module, const char* message, + const unsigned int len) noexcept { + if (module == nullptr) { + module = ""; + } + if (g_config.logCallback == nullptr) { + fmt::println(stderr, "[{}] [{}] {}", level, module, std::string_view(message, len)); + } else { + g_config.logCallback(level, module, message, len); + } +} +} // namespace aurora + +auto fmt::formatter::format(const AuroraLogLevel level, format_context& ctx) const + -> format_context::iterator { + std::string_view name = "unknown"; + switch (level) { + case LOG_DEBUG: + name = "debug"; + break; + case LOG_INFO: + name = "info"; + break; + case LOG_WARNING: + name = "warning"; + break; + case LOG_ERROR: + name = "error"; + break; + case LOG_FATAL: + name = "fatal"; + break; + default: + break; + } + return formatter::format(name, ctx); +} diff --git a/aurora-main/lib/logging.hpp b/aurora-main/lib/logging.hpp new file mode 100644 index 0000000..13f9314 --- /dev/null +++ b/aurora-main/lib/logging.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include + +#include +#include + +#include +#include + +namespace aurora { +void log_internal(AuroraLogLevel level, const char* module, const char* message, unsigned int len) noexcept; + +extern AuroraConfig g_config; + +struct Module { + const char* name; + explicit Module(const char* name) noexcept : name(name) {} + + template + void report(const AuroraLogLevel level, fmt::format_string fmt, T&&... args) noexcept { + if (g_config.logLevel > level) return; + + auto message = fmt::format(fmt, std::forward(args)...); + log_internal(level, name, message.c_str(), static_cast(message.size())); + } + + template + void debug(fmt::format_string fmt, T&&... args) noexcept { + report(LOG_DEBUG, fmt, std::forward(args)...); + } + + template + void info(fmt::format_string fmt, T&&... args) noexcept { + report(LOG_INFO, fmt, std::forward(args)...); + } + + template + void warn(fmt::format_string fmt, T&&... args) noexcept { + report(LOG_WARNING, fmt, std::forward(args)...); + } + + template + void error(fmt::format_string fmt, T&&... args) noexcept { + report(LOG_ERROR, fmt, std::forward(args)...); + } + + template + [[noreturn]] void fatal(fmt::format_string fmt, T&&... args) noexcept { + auto message = fmt::format(fmt, std::forward(args)...); + log_internal(LOG_FATAL, name, message.c_str(), static_cast(message.size())); + // Embedders can provide a fatal log callback, which the runtime uses for its own crash dialog. + // Standalone Aurora builds fall back to logging.cpp's native one. + if (g_config.logCallback == nullptr) { + show_fatal_dialog(name, message); + } + std::abort(); + } + +private: + static void show_fatal_dialog(const char* module, std::string_view message) noexcept; +}; +} // namespace aurora + +template <> +struct fmt::formatter : formatter { + auto format(AuroraLogLevel level, format_context& ctx) const -> format_context::iterator; +}; diff --git a/aurora-main/lib/main.cpp b/aurora-main/lib/main.cpp new file mode 100644 index 0000000..8a7fbee --- /dev/null +++ b/aurora-main/lib/main.cpp @@ -0,0 +1,6 @@ +#include +#undef main + +#include + +int main(int argc, char** argv) { return aurora_main(argc, argv); } diff --git a/aurora-main/lib/sqlite_utils.hpp b/aurora-main/lib/sqlite_utils.hpp new file mode 100644 index 0000000..38db46b --- /dev/null +++ b/aurora-main/lib/sqlite_utils.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include "internal.hpp" + +#include + +namespace aurora::sqlite { + +inline int exec(sqlite3* db, const char* sql) { return sqlite3_exec(db, sql, nullptr, nullptr, nullptr); } + +template +int exec(sqlite3* db, const char* sql, T callback, char** errmsg = nullptr) { + return sqlite3_exec( + db, sql, + [](void* cb, int argc, char** argv, char** columns) -> int { + auto& fp = *static_cast(cb); + if constexpr (std::is_same_v) { + fp(argc, argv, columns); + return 0; + } else { + return fp(argc, argv, columns); + } + }, + &callback, errmsg); +} + +class Transaction { +public: + Transaction(sqlite3* db, Module& log, bool immediate = false) : m_db(db), m_log(log) { + const auto type = immediate ? "BEGIN IMMEDIATE" : "BEGIN"; + const auto ret = sqlite3_exec(m_db, type, nullptr, nullptr, nullptr); + if (ret != SQLITE_OK) { + m_log.error("Failed to start transaction: {}", sqlite3_errmsg(m_db)); + return; + } + m_active = true; + } + + Transaction(const Transaction&) = delete; + Transaction& operator=(const Transaction&) = delete; + Transaction(Transaction&&) = delete; + Transaction& operator=(Transaction&&) = delete; + + ~Transaction() { + if (!m_active) { + return; + } + + const auto ret = sqlite3_exec(m_db, "ROLLBACK", nullptr, nullptr, nullptr); + if (ret != SQLITE_OK) { + m_log.error("Failed to roll back transaction (uh oh?): {}", sqlite3_errmsg(m_db)); + } + } + + void commit() { + if (!m_active) { + return; + } + + const auto ret = sqlite3_exec(m_db, "COMMIT", nullptr, nullptr, nullptr); + if (ret != SQLITE_OK) { + m_log.error("Failed to commit transaction: {}", sqlite3_errmsg(m_db)); + return; + } + + m_active = false; + } + + explicit operator bool() const { return m_active; } + +private: + sqlite3* m_db = nullptr; + Module& m_log; + bool m_active = false; +}; + +} // namespace aurora::sqlite diff --git a/aurora-main/lib/system_info.cpp b/aurora-main/lib/system_info.cpp new file mode 100644 index 0000000..15a58d4 --- /dev/null +++ b/aurora-main/lib/system_info.cpp @@ -0,0 +1,368 @@ +#include "system_info.hpp" +#include "internal.hpp" + +#if _WIN32 +#define WIN32_LEAN_AND_MEAN 1 +#include +#include + +template +class ComPtr { +public: + ComPtr() = default; + ComPtr(const ComPtr&) = delete; + ComPtr& operator=(const ComPtr&) = delete; + ~ComPtr() { reset(); } + + T* Get() const { return value; } + T* operator->() const { return value; } + T** Put() { + reset(); + return &value; + } + +private: + void reset() { + if (value != nullptr) { + value->Release(); + value = nullptr; + } + } + + T* value = nullptr; +}; +typedef LONG NTSTATUS, *PNTSTATUS; +extern "C" NTSYSAPI NTSTATUS NTAPI RtlGetVersion(PRTL_OSVERSIONINFOEXW lpVersionInformation); +#elif __APPLE__ +#include "sys/sysctl.h" +#elif linux +#include +#include +#include +#include +#endif + + +using namespace std::string_literals; + +namespace aurora { + +static constexpr auto Unknown = "Unknown"; + +static Module Log("aurora::system_info"); + +static std::string GetOSVersion(); +static std::string GetCpuModel(); +static uint64_t GetMemoryAmount(); +static void LogMisc(); + +void log_system_information() { + Log.info("CPU model: {}", GetCpuModel()); + const auto memSize = GetMemoryAmount() / 1024 / 1024; + Log.info("Memory: {} MiB", memSize); + +#if defined(__x86_64__) || defined(_M_X64) || defined(_M_X64) + Log.info("Architecture: x86_64"); +#elif defined(__aarch64__) || defined(_M_ARM64) + Log.info("Architecture: aarch64"); +#else + Log.info("Architecture: Unknown"); +#endif + + Log.info("OS: {}", GetOSVersion()); + LogMisc(); +} + +#if _WIN32 +static std::string wideStringToUtf8(std::wstring_view str) { + const auto size = WideCharToMultiByte( + CP_UTF8, + 0, + str.data(), + static_cast(str.size()), + nullptr, + 0, + nullptr, + nullptr + ); + + std::string result{}; + result.resize(size); + + WideCharToMultiByte( + CP_UTF8, + 0, + str.data(), + static_cast(str.size()), + result.data(), + static_cast(result.size()), + nullptr, + nullptr + ); + + return result; +} + +std::string GetCpuModel() { + HKEY key = nullptr; + const auto opened = RegOpenKeyExW( + HKEY_LOCAL_MACHINE, + L"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", + 0, + KEY_QUERY_VALUE, + &key); + if (opened != ERROR_SUCCESS) { + Log.error("Unable to open the processor registry key"); + return Unknown; + } + + wchar_t value[256]{}; + DWORD type = 0; + DWORD size = sizeof(value); + const auto queried = RegQueryValueExW( + key, + L"ProcessorNameString", + nullptr, + &type, + reinterpret_cast(value), + &size); + RegCloseKey(key); + if (queried != ERROR_SUCCESS || (type != REG_SZ && type != REG_EXPAND_SZ)) { + Log.error("Unable to read the processor name"); + return Unknown; + } + + const auto length = wcsnlen(value, sizeof(value) / sizeof(value[0])); + return wideStringToUtf8(std::wstring_view(value, length)); +} + +uint64_t GetMemoryAmount() { + MEMORYSTATUSEX status; + status.dwLength = sizeof(status); + GlobalMemoryStatusEx(&status); + + return status.ullTotalPhys; +} + +std::string GetOSVersion() { + RTL_OSVERSIONINFOEXW info = {}; + info.dwOSVersionInfoSize = sizeof(info); + auto result = RtlGetVersion(&info); + if (result != 0) { + Log.error("RtlGetVersion failed"); + return Unknown; + } + + return fmt::format( + "Microsoft Windows {}.{} build {}", + info.dwMajorVersion, + info.dwMinorVersion, + info.dwBuildNumber); +} + +static void LogGpus() { + ComPtr factory; + auto result = CreateDXGIFactory1( + __uuidof(IDXGIFactory1), + reinterpret_cast(factory.Put())); + if (FAILED(result)) { + Log.error("Unable to create IDXGIFactory1"); + return; + } + + for (UINT i = 0;;i++) { + ComPtr adapter; + result = factory->EnumAdapters1(i, adapter.Put()); + if (result == DXGI_ERROR_NOT_FOUND) + break; + + DXGI_ADAPTER_DESC1 desc{}; + adapter->GetDesc1(&desc); + + std::wstring descName(desc.Description, wcsnlen(desc.Description, 128)); + Log.info("Detected GPU: {}", wideStringToUtf8(descName)); + } +} + +void LogMisc() { + LogGpus(); +} + +#elif __APPLE__ + +static std::string sysCtlToString(const char* name) { + size_t length; + auto err = sysctlbyname(name, nullptr, &length, nullptr, 0); + if (err) { + Log.error("sysctlbyname failed: %d", err); + return Unknown; + } + + std::string value; + value.resize(length); + err = sysctlbyname(name, value.data(), &length, nullptr, 0); + if (err) { + Log.error("second sysctlbyname failed: %d", err); + return Unknown; + } + + if (value[length - 1] == '\0') + value.pop_back(); + + return value; +} + +std::string GetCpuModel() { + return sysCtlToString("machdep.cpu.brand_string"); +} + +uint64_t GetMemoryAmount() { + uint64_t result; + size_t size = sizeof(result); + const auto err = sysctlbyname("hw.memsize", &result, &size, nullptr, 0); + if (err) { + Log.error("sysctlbyname failed: %d", err); + return 0; + } + + return result; +} + +std::string GetOSVersion() { +#if TARGET_OS_MAC + constexpr auto name = "macOS"; +#elif TARGET_OS_IOS + constexpr auto name = "iOS"; +#elif TARGET_OS_TV + constexpr auto name = "tvOS"; +#elif + constexpr auto name = Unknown; +#endif + + return fmt::format("{} {}", name, system_info::getSystemVersionString()); +} + +void LogMisc() { + // Nada. +} +#elif linux + +// https://stackoverflow.com/questions/216823/how-can-i-trim-a-stdstring +static void ltrim(std::string &s) { + s.erase(s.begin(), std::ranges::find_if(s.begin(), s.end(), [](unsigned char ch) { + return !std::isspace(ch); + })); +} +static void rtrim(std::string &s) { + s.erase(std::ranges::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { + return !std::isspace(ch); + }).base(), s.end()); +} +inline std::string trim(std::string str) { + ltrim(str); + rtrim(str); + return std::move(str); +} + +std::string GetCpuModel() { + std::ifstream cpuInfo("/proc/cpuinfo"); + if (!cpuInfo) + { + Log.error("Failed to open /proc/cpuinfo"); + return Unknown; + } + + while (!cpuInfo.bad() && !cpuInfo.eof()) { + std::string line; + std::getline(cpuInfo, line); + + const auto colon = line.find(':'); + if (colon == std::string::npos) { + continue; + } + + auto left = trim(line.substr(0, colon)); + auto right = trim(line.substr(colon + 1)); + + if (left == "model name") { + return right; + } + } + + return Unknown; +} + +uint64_t GetMemoryAmount() { + struct sysinfo info{}; + sysinfo(&info); + + return info.totalram; +} + +std::string GetOSVersion() { + auto path = "/etc/os-release"; + if (!std::filesystem::exists(path)) { + path = "/usr/lib/os-release"; + } + + std::ifstream releaseInfo(path); + if (!releaseInfo) + { + Log.error("Failed to open /etc/os-release or /usr/lib/os-release"); + return Unknown; + } + + std::string name, version; + + while (!releaseInfo.bad() && !releaseInfo.eof()) { + std::string line; + std::getline(releaseInfo, line); + + const auto split = line.find('='); + if (split == std::string::npos) { + continue; + } + + auto left = trim(line.substr(0, split)); + auto right = trim(line.substr(split + 1)); + + if (right[0] == '"' && right[right.size()-1] == '"') { + right = right.substr(1, right.size()-2); + } + + if (left == "NAME") { + name = right; + } else if (left == "VERSION") { + version = right; + } + } + + if (name.empty()) { + return Unknown; + } + + return fmt::format("{} {}", name, version); +} + +void LogMisc() { + +} +#else +std::string GetCpuModel() { + return Unknown; +} + +uint64_t GetMemoryAmount() { + return 0; +} + +std::string GetOSVersion() { + return Unknown; +} + +void LogMisc() { + +} +#endif + +} // namespace aurora diff --git a/aurora-main/lib/system_info.hpp b/aurora-main/lib/system_info.hpp new file mode 100644 index 0000000..6dda15e --- /dev/null +++ b/aurora-main/lib/system_info.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace aurora { + void log_system_information(); + + namespace system_info { +#if __APPLE__ + std::string getSystemVersionString(); +#endif + } +} \ No newline at end of file diff --git a/aurora-main/lib/system_info_mac.mm b/aurora-main/lib/system_info_mac.mm new file mode 100644 index 0000000..113ac47 --- /dev/null +++ b/aurora-main/lib/system_info_mac.mm @@ -0,0 +1,10 @@ +#include "system_info.hpp" +#include +#include + +namespace aurora::system_info { + std::string getSystemVersionString() { + NSString * str = [[NSProcessInfo processInfo] operatingSystemVersionString]; + return [str UTF8String]; + } +} \ No newline at end of file diff --git a/aurora-main/lib/webgpu/gpu.cpp b/aurora-main/lib/webgpu/gpu.cpp new file mode 100644 index 0000000..e2fe351 --- /dev/null +++ b/aurora-main/lib/webgpu/gpu.cpp @@ -0,0 +1,966 @@ +#include "gpu.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "../gfx/common.hpp" +#include "../internal.hpp" +#include "../window.hpp" +#include "../dolphin/vi/vi_internal.hpp" + +#if defined(WEBGPU_DAWN) && !defined(__MINGW32__) +#include "../dawn/BackendBinding.hpp" +#include +#elif defined(WEBGPU_DAWN) +#include "../dawn/BackendBinding.hpp" +#endif + +#if defined(WEBGPU_DAWN) && defined(_WIN32) +#include +#endif + +namespace aurora::gx { +void clear_display_copy_cache() noexcept; +} // namespace aurora::gx +namespace aurora::gfx { +void clear_offscreen_cache(); +} // namespace aurora::gfx + +namespace aurora::webgpu { +static Module Log("aurora::gpu"); + +wgpu::Device g_device; +wgpu::Queue g_queue; +wgpu::Surface g_surface; +wgpu::BackendType g_backendType; +GraphicsConfig g_graphicsConfig; +TextureWithSampler g_frameBuffer; +TextureWithSampler g_frameBufferResolved; +TextureWithSampler g_depthBuffer; + +// EFB -> XFB copy pipeline +static wgpu::BindGroupLayout g_CopyBindGroupLayout; +wgpu::RenderPipeline g_CopyPipeline; +wgpu::BindGroup g_CopyBindGroup; +static bool g_presentSourceOverrideActive = false; +static wgpu::BindGroup g_presentSourceOverrideBindGroup; +static wgpu::Texture g_presentSourceOverrideTexture; +static wgpu::Extent3D g_presentSourceOverrideSize{}; +static wgpu::TextureFormat g_presentSourceOverrideFormat = wgpu::TextureFormat::Undefined; + +static wgpu::Adapter g_adapter; +wgpu::Instance g_instance; +static wgpu::AdapterInfo g_adapterInfo; +static wgpu::SurfaceCapabilities g_surfaceCapabilities; +bool g_bcTexturesSupported; +// Written by Dawn's device-loss callback and consumed at ordered frame boundaries. Keep the +// callback free of logging, allocation, teardown and renderer state mutation. +static std::atomic_bool g_deviceLost{false}; +static std::atomic g_deviceLostReason{wgpu::DeviceLostReason::Unknown}; +// The reason enum is almost always `Unknown`, while Dawn's message carries the real cause, so +// keep a truncated copy. Written with a plain memcpy, published by the g_deviceLost store. +static std::array g_deviceLostMessage{}; +// Errors raised before initialize() completes must not be fatal: the backend fallback loop retries +// the next backend, and a broken ICD can raise uncaptured errors mid-probe. +static std::atomic_bool g_initialized{false}; + +namespace { + +struct RenderTargetSize { + uint32_t width; + uint32_t height; +}; + +RenderTargetSize clamp_render_target_size(uint32_t width, uint32_t height) noexcept { + const uint32_t maxDimension = g_graphicsConfig.maxTextureDimension2D; + if (width == 0 || height == 0 || maxDimension == 0 || maxDimension == WGPU_LIMIT_U32_UNDEFINED || + (width <= maxDimension && height <= maxDimension)) { + return {width, height}; + } + + // Keep the requested aspect while fitting both axes inside the adapter's maximum 2D texture size. + // 64-bit arithmetic so a large window cannot wrap during the scale. + if (width > maxDimension) { + height = std::max(1u, static_cast((static_cast(height) * maxDimension) / width)); + width = maxDimension; + } + if (height > maxDimension) { + width = std::max(1u, static_cast((static_cast(width) * maxDimension) / height)); + height = maxDimension; + } + return {width, height}; +} + +RenderTargetSize clamp_frame_buffer_size(uint32_t width, uint32_t height) noexcept { + const auto adapterClamped = clamp_render_target_size(width, height); + const auto budgeted = + render_size_limits::fit_framebuffer_to_budget(adapterClamped.width, adapterClamped.height, + g_graphicsConfig.maxTextureDimension2D); + return {budgeted.width, budgeted.height}; +} + +// V-Sync is never enabled: the guest drives its own pacing, and blocking in Present() couples the +// whole machine to the monitor (a 120 FPS target on a 75 Hz display runs in slow motion). +wgpu::PresentMode best_present_mode() { + const auto supports = [](const wgpu::PresentMode candidate) { + for (size_t i = 0; i < g_surfaceCapabilities.presentModeCount; ++i) { + if (g_surfaceCapabilities.presentModes[i] == candidate) { + return true; + } + } + return false; + }; + // Vulkan prefers Mailbox, every other backend Immediate. Under window capture the Vulkan driver + // cannot flip and Immediate leaks about a megabyte per present until the device is lost. + const bool preferMailbox = g_backendType == wgpu::BackendType::Vulkan; + if (preferMailbox && supports(wgpu::PresentMode::Mailbox)) { + return wgpu::PresentMode::Mailbox; + } + if (supports(wgpu::PresentMode::Immediate)) { + return wgpu::PresentMode::Immediate; + } + if (g_backendType != wgpu::BackendType::Metal && supports(wgpu::PresentMode::Mailbox)) { + return wgpu::PresentMode::Mailbox; + } + // Mailbox is preferred over Fifo because Fifo caps presentation at the refresh rate and every slot + // deadline after the first is missed. Reaching this means neither is offered, so say so loudly. + Log.warn("Surface supports neither Immediate nor Mailbox; falling back to Fifo. Presentation " + "is capped at the display refresh rate, so the game may run slower than its own " + "pace and frame interpolation cannot exceed the refresh rate."); + return wgpu::PresentMode::Fifo; +} + +wgpu::TextureFormat to_linear(wgpu::TextureFormat format) { + if (format == wgpu::TextureFormat::RGBA8UnormSrgb) { + return wgpu::TextureFormat::RGBA8Unorm; + } + if (format == wgpu::TextureFormat::BGRA8UnormSrgb) { + return wgpu::TextureFormat::BGRA8Unorm; + } + return format; +} + +wgpu::TextureFormat best_surface_format() { + if (g_surfaceCapabilities.formatCount == 0) { + return wgpu::TextureFormat::Undefined; + } + for (size_t i = 0; i < g_surfaceCapabilities.formatCount; ++i) { + const auto format = to_linear(g_surfaceCapabilities.formats[i]); + if (format == wgpu::TextureFormat::RGBA8Unorm || format == wgpu::TextureFormat::BGRA8Unorm) { + return format; + } + } + return g_surfaceCapabilities.formats[0]; +} + +} // namespace + +TextureWithSampler create_render_texture(uint32_t width, uint32_t height, bool multisampled) { + const auto renderTargetSize = clamp_render_target_size(width, height); + const wgpu::Extent3D size{ + .width = renderTargetSize.width, + .height = renderTargetSize.height, + .depthOrArrayLayers = 1, + }; + const auto format = g_graphicsConfig.surfaceConfiguration.format; + uint32_t sampleCount = 1; + if (multisampled) { + sampleCount = g_graphicsConfig.msaaSamples; + } + if (width == 0 || height == 0) { + Log.fatal("Invalid render texture size! {}x{}, multisampled {}, format {}", width, height, static_cast(format), multisampled); + } + const wgpu::TextureDescriptor textureDescriptor{ + .label = "Render texture", + .usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc | + wgpu::TextureUsage::CopyDst, + .dimension = wgpu::TextureDimension::e2D, + .size = size, + .format = format, + .mipLevelCount = 1, + .sampleCount = sampleCount, + }; + auto texture = g_device.CreateTexture(&textureDescriptor); + + constexpr wgpu::TextureViewDescriptor viewDescriptor{ + .label = "Render texture view", + .dimension = wgpu::TextureViewDimension::e2D, + }; + auto view = texture.CreateView(&viewDescriptor); + + constexpr wgpu::SamplerDescriptor samplerDescriptor{ + .label = "Render sampler", + .addressModeU = wgpu::AddressMode::ClampToEdge, + .addressModeV = wgpu::AddressMode::ClampToEdge, + .addressModeW = wgpu::AddressMode::ClampToEdge, + .magFilter = wgpu::FilterMode::Linear, + .minFilter = wgpu::FilterMode::Linear, + .mipmapFilter = wgpu::MipmapFilterMode::Linear, + .lodMinClamp = 0.f, + .lodMaxClamp = 1000.f, + .maxAnisotropy = 1, + }; + auto sampler = g_device.CreateSampler(&samplerDescriptor); + + return { + .texture = std::move(texture), + .view = std::move(view), + .size = size, + .format = format, + .sampler = std::move(sampler), + }; +} + +const TextureWithSampler& present_source() noexcept { + return g_graphicsConfig.msaaSamples > 1 ? g_frameBufferResolved : g_frameBuffer; +} + +PresentSource current_present_source() noexcept { + if (g_presentSourceOverrideActive && g_presentSourceOverrideBindGroup != nullptr) { + return { + .bindGroup = g_presentSourceOverrideBindGroup, + .texture = g_presentSourceOverrideTexture, + .size = g_presentSourceOverrideSize, + .format = g_presentSourceOverrideFormat, + }; + } + + return { + .bindGroup = g_CopyBindGroup, + .texture = present_source().texture, + .size = present_source().size, + .format = present_source().format, + }; +} + +void set_present_source_override(wgpu::BindGroup bindGroup, wgpu::Texture texture, wgpu::Extent3D size, + wgpu::TextureFormat format) noexcept { + g_presentSourceOverrideBindGroup = std::move(bindGroup); + g_presentSourceOverrideTexture = std::move(texture); + g_presentSourceOverrideSize = size; + g_presentSourceOverrideFormat = format; + g_presentSourceOverrideActive = true; +} + +void clear_present_source_override() noexcept { + g_presentSourceOverrideActive = false; + g_presentSourceOverrideBindGroup = {}; + g_presentSourceOverrideTexture = {}; + g_presentSourceOverrideSize = {}; + g_presentSourceOverrideFormat = wgpu::TextureFormat::Undefined; +} + +Viewport calculate_present_viewport_for_aspect(uint32_t surface_width, uint32_t surface_height, + float content_aspect) noexcept { + if (surface_width == 0 || surface_height == 0 || !(content_aspect > 0.f)) { + return {}; + } + + uint32_t viewport_width = surface_width; + uint32_t viewport_height = std::min( + surface_height, std::max(1u, static_cast(std::lround(static_cast(viewport_width) * + static_cast(1.f / content_aspect))))); + if (viewport_height == surface_height) { + viewport_width = std::min( + surface_width, std::max(1u, static_cast(std::lround(static_cast(viewport_height) * + static_cast(content_aspect))))); + } + + return { + .left = static_cast((surface_width - viewport_width) / 2), + .top = static_cast((surface_height - viewport_height) / 2), + .width = static_cast(viewport_width), + .height = static_cast(viewport_height), + .znear = 0.f, + .zfar = 1.f, + }; +} + +Viewport calculate_present_viewport(uint32_t surface_width, uint32_t surface_height, uint32_t content_width, + uint32_t content_height) noexcept { + if (content_width == 0 || content_height == 0) { + return {}; + } + return calculate_present_viewport_for_aspect( + surface_width, surface_height, static_cast(content_width) / static_cast(content_height)); +} + +static TextureWithSampler create_depth_texture(uint32_t width, uint32_t height) { + const auto renderTargetSize = clamp_render_target_size(width, height); + const wgpu::Extent3D size{ + .width = renderTargetSize.width, + .height = renderTargetSize.height, + .depthOrArrayLayers = 1, + }; + const auto format = g_graphicsConfig.depthFormat; + const wgpu::TextureDescriptor textureDescriptor{ + .label = "Depth texture", + .usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding, + .dimension = wgpu::TextureDimension::e2D, + .size = size, + .format = format, + .mipLevelCount = 1, + .sampleCount = g_graphicsConfig.msaaSamples, + }; + auto texture = g_device.CreateTexture(&textureDescriptor); + + const wgpu::TextureViewDescriptor viewDescriptor{ + .label = "Depth texture view", + .dimension = wgpu::TextureViewDimension::e2D, + }; + auto view = texture.CreateView(&viewDescriptor); + + const wgpu::SamplerDescriptor samplerDescriptor{ + .label = "Depth sampler", + .addressModeU = wgpu::AddressMode::ClampToEdge, + .addressModeV = wgpu::AddressMode::ClampToEdge, + .addressModeW = wgpu::AddressMode::ClampToEdge, + .magFilter = wgpu::FilterMode::Linear, + .minFilter = wgpu::FilterMode::Linear, + .mipmapFilter = wgpu::MipmapFilterMode::Linear, + .lodMinClamp = 0.f, + .lodMaxClamp = 1000.f, + .maxAnisotropy = 1, + }; + auto sampler = g_device.CreateSampler(&samplerDescriptor); + + return { + .texture = std::move(texture), + .view = std::move(view), + .size = size, + .format = format, + .sampler = std::move(sampler), + }; +} + +void create_copy_pipeline() { + wgpu::ShaderSourceWGSL sourceDescriptor{}; + sourceDescriptor.code = R"""( +@group(0) @binding(0) +var efb_sampler: sampler; +@group(0) @binding(1) +var efb_texture: texture_2d; + +struct VertexOutput { + @builtin(position) pos: vec4, + @location(0) uv: vec2, +}; + +var pos: array, 3> = array, 3>( + vec2(-1.0, 1.0), + vec2(-1.0, -3.0), + vec2(3.0, 1.0), +); +var uvs: array, 3> = array, 3>( + vec2(0.0, 0.0), + vec2(0.0, 2.0), + vec2(2.0, 0.0), +); + +@vertex +fn vs_main(@builtin(vertex_index) vtxIdx: u32) -> VertexOutput { + var out: VertexOutput; + out.pos = vec4(pos[vtxIdx], 0.0, 1.0); + out.uv = uvs[vtxIdx]; + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + let color = textureSample(efb_texture, efb_sampler, in.uv); + return vec4(color.rgb, 1.0); +} +)"""; + const wgpu::ShaderModuleDescriptor moduleDescriptor{ + .nextInChain = &sourceDescriptor, + .label = "XFB Copy Module", + }; + auto module = g_device.CreateShaderModule(&moduleDescriptor); + const std::array colorTargets{wgpu::ColorTargetState{ + .format = g_graphicsConfig.surfaceConfiguration.format, + .writeMask = wgpu::ColorWriteMask::All, + }}; + const wgpu::FragmentState fragmentState{ + .module = module, + .entryPoint = "fs_main", + .targetCount = colorTargets.size(), + .targets = colorTargets.data(), + }; + const std::array bindGroupLayoutEntries{ + wgpu::BindGroupLayoutEntry{ + .binding = 0, + .visibility = wgpu::ShaderStage::Fragment, + .sampler = + wgpu::SamplerBindingLayout{ + .type = wgpu::SamplerBindingType::Filtering, + }, + }, + wgpu::BindGroupLayoutEntry{ + .binding = 1, + .visibility = wgpu::ShaderStage::Fragment, + .texture = + wgpu::TextureBindingLayout{ + .sampleType = wgpu::TextureSampleType::Float, + .viewDimension = wgpu::TextureViewDimension::e2D, + }, + }, + }; + const wgpu::BindGroupLayoutDescriptor bindGroupLayoutDescriptor{ + .entryCount = bindGroupLayoutEntries.size(), + .entries = bindGroupLayoutEntries.data(), + }; + g_CopyBindGroupLayout = g_device.CreateBindGroupLayout(&bindGroupLayoutDescriptor); + const wgpu::PipelineLayoutDescriptor layoutDescriptor{ + .bindGroupLayoutCount = 1, + .bindGroupLayouts = &g_CopyBindGroupLayout, + }; + auto pipelineLayout = g_device.CreatePipelineLayout(&layoutDescriptor); + const wgpu::RenderPipelineDescriptor pipelineDescriptor{ + .layout = pipelineLayout, + .vertex = + wgpu::VertexState{ + .module = module, + .entryPoint = "vs_main", + }, + .primitive = + wgpu::PrimitiveState{ + .topology = wgpu::PrimitiveTopology::TriangleList, + }, + .multisample = + wgpu::MultisampleState{ + .count = 1, + .mask = UINT32_MAX, + }, + .fragment = &fragmentState, + }; + g_CopyPipeline = g_device.CreateRenderPipeline(&pipelineDescriptor); +} + +wgpu::BindGroup create_copy_bind_group(wgpu::TextureView sourceView, wgpu::Sampler sampler) { + const std::array bindGroupEntries{ + wgpu::BindGroupEntry{ + .binding = 0, + .sampler = sampler, + }, + wgpu::BindGroupEntry{ + .binding = 1, + .textureView = sourceView, + }, + }; + const wgpu::BindGroupDescriptor bindGroupDescriptor{ + .layout = g_CopyBindGroupLayout, + .entryCount = bindGroupEntries.size(), + .entries = bindGroupEntries.data(), + }; + return g_device.CreateBindGroup(&bindGroupDescriptor); +} + +wgpu::BindGroup create_copy_bind_group(const TextureWithSampler& source) { + return create_copy_bind_group(source.view, source.sampler); +} + +static wgpu::BackendType to_wgpu_backend(AuroraBackend backend) { + switch (backend) { + case BACKEND_WEBGPU: + return wgpu::BackendType::WebGPU; + case BACKEND_D3D11: + return wgpu::BackendType::D3D11; + case BACKEND_D3D12: + return wgpu::BackendType::D3D12; + case BACKEND_METAL: + return wgpu::BackendType::Metal; + case BACKEND_VULKAN: + return wgpu::BackendType::Vulkan; + case BACKEND_OPENGL: + return wgpu::BackendType::OpenGL; + case BACKEND_OPENGLES: + return wgpu::BackendType::OpenGLES; + default: + return wgpu::BackendType::Null; + } +} + +static bool create_surface() { + SDL_Window* window = window::get_sdl_window(); + if (window == nullptr) { + Log.error("Failed to create surface: no window"); + return false; + } + const auto chainedDescriptor = utils::SetupWindowAndGetSurfaceDescriptor(window); + if (!chainedDescriptor) { + Log.error("Failed to create surface descriptor for current window"); + return false; + } + const wgpu::SurfaceDescriptor surfaceDescriptor{ + .nextInChain = chainedDescriptor.get(), + .label = "Surface", + }; + release_surface(); + g_surface = g_instance.CreateSurface(&surfaceDescriptor); + if (!g_surface) { + Log.error("Failed to create surface"); + return false; + } + return true; +} + +bool initialize(AuroraBackend auroraBackend) { + if (!g_instance) { + Log.info("Creating WebGPU instance"); + const std::array requiredInstanceFeatures{ + wgpu::InstanceFeatureName::TimedWaitAny, + }; + wgpu::InstanceDescriptor instanceDescriptor{ + .requiredFeatureCount = requiredInstanceFeatures.size(), + .requiredFeatures = requiredInstanceFeatures.data(), + }; +#if defined(WEBGPU_DAWN) && !defined(__MINGW32__) + // DawnNative.h's C++ constructor has an MSVC ABI that cannot cross into llvm-mingw, and the + // descriptor only restates Dawn's defaults, so use the public WebGPU descriptor here. + dawn::native::DawnInstanceDescriptor dawnInstanceDescriptor; + dawnInstanceDescriptor.backendValidationLevel = dawn::native::BackendValidationLevel::Disabled; + instanceDescriptor.nextInChain = &dawnInstanceDescriptor; +#endif + g_instance = wgpu::CreateInstance(&instanceDescriptor); + if (!g_instance) { + Log.error("Failed to create WebGPU instance"); + return false; + } + } + const wgpu::BackendType backend = to_wgpu_backend(auroraBackend); + Log.info("Attempting to initialize {}", magic_enum::enum_name(backend)); + // One call is one backend attempt. aurora::initialize() retries without calling shutdown(), so a + // leftover adapter would pass the `if (!g_adapter)` guard and mismatch adapter with device. + g_queue = {}; + g_device = {}; + g_deviceLostReason.store(wgpu::DeviceLostReason::Unknown, std::memory_order_relaxed); + g_deviceLost.store(false, std::memory_order_release); + g_adapter = {}; + g_backendType = wgpu::BackendType::Undefined; + { + window::SurfaceLock surfaceLock; + if (!create_surface()) { + return false; + } + } + { + const wgpu::RequestAdapterOptions options{ + .powerPreference = wgpu::PowerPreference::HighPerformance, + .backendType = backend, + .compatibleSurface = g_surface, + }; + const auto future = g_instance.RequestAdapter( + &options, wgpu::CallbackMode::WaitAnyOnly, + [](wgpu::RequestAdapterStatus status, wgpu::Adapter adapter, wgpu::StringView message) { + if (status == wgpu::RequestAdapterStatus::Success) { + g_adapter = std::move(adapter); + } else { + Log.warn("Adapter request failed: {}", message); + } + }); + const auto status = g_instance.WaitAny(future, 5000000000); + if (status != wgpu::WaitStatus::Success) { + Log.error("Failed to create {} adapter: {}", magic_enum::enum_name(backend), + magic_enum::enum_name(status)); + return false; + } + if (!g_adapter) { + Log.error("No {} adapter is available on this system", magic_enum::enum_name(backend)); + return false; + } + } + g_adapter.GetInfo(&g_adapterInfo); + g_backendType = g_adapterInfo.backendType; + const auto backendName = magic_enum::enum_name(g_backendType); + auto adapterName = g_adapterInfo.device; + if (adapterName.IsUndefined()) { + adapterName = wgpu::StringView("Unknown"); + } + auto description = g_adapterInfo.description; + if (description.IsUndefined()) { + description = wgpu::StringView("Unknown"); + } + Log.info("Graphics adapter information\n API: {}\n Device: {} ({})\n Driver: {}", backendName, adapterName, + magic_enum::enum_name(g_adapterInfo.adapterType), description); + + uint32_t maxTextureDimension2D = 0; + { + wgpu::Limits supportedLimits{}; + g_adapter.GetLimits(&supportedLimits); + maxTextureDimension2D = supportedLimits.maxTextureDimension2D; + const wgpu::Limits requiredLimits{ + // Use "best" supported limits + .maxTextureDimension1D = supportedLimits.maxTextureDimension1D == 0 ? WGPU_LIMIT_U32_UNDEFINED + : supportedLimits.maxTextureDimension1D, + .maxTextureDimension2D = supportedLimits.maxTextureDimension2D == 0 ? WGPU_LIMIT_U32_UNDEFINED + : supportedLimits.maxTextureDimension2D, + .maxTextureDimension3D = supportedLimits.maxTextureDimension3D == 0 ? WGPU_LIMIT_U32_UNDEFINED + : supportedLimits.maxTextureDimension3D, + .maxTextureArrayLayers = supportedLimits.maxTextureArrayLayers == 0 ? WGPU_LIMIT_U32_UNDEFINED + : supportedLimits.maxTextureArrayLayers, + .maxDynamicStorageBuffersPerPipelineLayout = supportedLimits.maxDynamicStorageBuffersPerPipelineLayout == 0 + ? WGPU_LIMIT_U32_UNDEFINED + : supportedLimits.maxDynamicStorageBuffersPerPipelineLayout, + .maxStorageBuffersPerShaderStage = supportedLimits.maxStorageBuffersPerShaderStage == 0 + ? WGPU_LIMIT_U32_UNDEFINED + : supportedLimits.maxStorageBuffersPerShaderStage, + .minUniformBufferOffsetAlignment = + supportedLimits.minUniformBufferOffsetAlignment < 64 ? 64 : supportedLimits.minUniformBufferOffsetAlignment, + .minStorageBufferOffsetAlignment = + supportedLimits.minStorageBufferOffsetAlignment < 16 ? 16 : supportedLimits.minStorageBufferOffsetAlignment, + }; + Log.info( + "Using limits:" + "\n maxTextureDimension1D: {}" + "\n maxTextureDimension2D: {}" + "\n maxTextureDimension3D: {}" + "\n maxTextureArrayLayers: {}" + "\n maxDynamicStorageBuffersPerPipelineLayout: {}" + "\n maxStorageBuffersPerShaderStage: {}" + "\n minUniformBufferOffsetAlignment: {}" + "\n minStorageBufferOffsetAlignment: {}", + requiredLimits.maxTextureDimension1D, requiredLimits.maxTextureDimension2D, + requiredLimits.maxTextureDimension3D, requiredLimits.maxTextureArrayLayers, + requiredLimits.maxDynamicStorageBuffersPerPipelineLayout, requiredLimits.maxStorageBuffersPerShaderStage, + requiredLimits.minUniformBufferOffsetAlignment, requiredLimits.minStorageBufferOffsetAlignment); + std::vector requiredFeatures; + bool implicitDeviceSynchronizationSupported = false; + wgpu::SupportedFeatures supportedFeatures; + g_adapter.GetFeatures(&supportedFeatures); + for (size_t i = 0; i < supportedFeatures.featureCount; ++i) { + const auto feature = supportedFeatures.features[i]; + if (feature == wgpu::FeatureName::TextureCompressionBC) { + g_bcTexturesSupported = true; + requiredFeatures.push_back(feature); + } + // The presenter calls device and queue methods while the frame worker encodes, which Dawn only + // supports with this feature; without it the two race inside the device's dynamic uploader. + if (feature == wgpu::FeatureName::ImplicitDeviceSynchronization) { + implicitDeviceSynchronizationSupported = true; + requiredFeatures.push_back(feature); + } + } + if (!implicitDeviceSynchronizationSupported) { + Log.warn( + "Adapter does not support ImplicitDeviceSynchronization; multi-threaded presentation is " + "not safe on this device."); + } +#ifdef WEBGPU_DAWN + wgpu::DawnCacheDeviceDescriptor cacheDescriptor({ + .isolationKey = nullptr, + .loadDataFunction = load_from_cache, + .storeDataFunction = store_to_cache, + .functionUserdata = nullptr, + }); + + std::vector enableToggles{ + /* clang-format off */ +#if _WIN32 + "use_dxc", +#ifndef NDEBUG + "emit_hlsl_debug_symbols", +#endif +#endif +#ifndef ANDROID + "use_user_defined_labels_in_backend", +#endif + "disable_symbol_renaming", + "enable_immediate_error_handling", + /* clang-format on */ + }; +#ifdef NDEBUG + enableToggles.push_back("skip_validation"); + enableToggles.push_back("disable_robustness"); +#endif + if (g_backendType == wgpu::BackendType::Vulkan) { + enableToggles.push_back("vulkan_monolithic_pipeline_cache"); + } + const wgpu::DawnTogglesDescriptor togglesDescriptor({ + .nextInChain = &cacheDescriptor, + .enabledToggleCount = enableToggles.size(), + .enabledToggles = enableToggles.data(), + }); +#endif + wgpu::DeviceDescriptor deviceDescriptor; +#ifdef WEBGPU_DAWN + deviceDescriptor.nextInChain = &togglesDescriptor; +#endif + deviceDescriptor.requiredFeatureCount = requiredFeatures.size(); + deviceDescriptor.requiredFeatures = requiredFeatures.data(); + deviceDescriptor.requiredLimits = &requiredLimits; + deviceDescriptor.SetUncapturedErrorCallback( + [](const wgpu::Device& device, wgpu::ErrorType type, wgpu::StringView message) { + if (g_initialized.load(std::memory_order_acquire)) { + FATAL("WebGPU error {}: {}", underlying(type), message); + } else { + Log.warn("WebGPU error {}: {}", underlying(type), message); + } + }); + deviceDescriptor.SetDeviceLostCallback(wgpu::CallbackMode::AllowSpontaneous, + [](const wgpu::Device& device, wgpu::DeviceLostReason reason, + wgpu::StringView message) { + (void)device; + // Shutdown and backend retry release the final + // device reference here too, not a real failure. + if (reason == wgpu::DeviceLostReason::Destroyed) { + return; + } + // Via string_view, so Dawn resolves a + // WGPU_STRLEN length instead of SIZE_MAX. + const std::string_view text{message}; + const size_t copied = + std::min(text.size(), g_deviceLostMessage.size() - 1); + if (copied > 0) { + std::memcpy(g_deviceLostMessage.data(), text.data(), copied); + } + g_deviceLostMessage[copied] = '\0'; + g_deviceLostReason.store(reason, std::memory_order_relaxed); + g_deviceLost.store(true, std::memory_order_release); + }); + const auto future = + g_adapter.RequestDevice(&deviceDescriptor, wgpu::CallbackMode::WaitAnyOnly, + [](wgpu::RequestDeviceStatus status, wgpu::Device device, wgpu::StringView message) { + if (status == wgpu::RequestDeviceStatus::Success) { + g_device = std::move(device); + } else { + Log.warn("Device request failed: {}", message); + } + }); + const auto status = g_instance.WaitAny(future, 5000000000); + if (status != wgpu::WaitStatus::Success) { + Log.error("Failed to create device: {}", magic_enum::enum_name(status)); + return false; + } + if (!g_device) { + return false; + } + g_device.SetLoggingCallback([](wgpu::LoggingType type, wgpu::StringView message) { + AuroraLogLevel level = LOG_FATAL; + switch (type) { + case wgpu::LoggingType::Verbose: + level = LOG_DEBUG; + break; + case wgpu::LoggingType::Info: + level = LOG_INFO; + break; + case wgpu::LoggingType::Warning: + level = LOG_WARNING; + break; + case wgpu::LoggingType::Error: + level = LOG_ERROR; + break; + default: + break; + } + Log.report(level, "WebGPU message: {}", message); + }); + } + g_queue = g_device.GetQueue(); + + const wgpu::Status status = g_surface.GetCapabilities(g_adapter, &g_surfaceCapabilities); + if (status != wgpu::Status::Success) { + Log.error("Failed to get surface capabilities: {}", magic_enum::enum_name(status)); + return false; + } + if (g_surfaceCapabilities.formatCount == 0) { + Log.error("Surface has no formats"); + return false; + } + if (g_surfaceCapabilities.presentModeCount == 0) { + Log.error("Surface has no present modes"); + return false; + } + auto surfaceFormat = best_surface_format(); + auto presentMode = best_present_mode(); + Log.info("Using surface format {}, present mode {}", magic_enum::enum_name(surfaceFormat), + magic_enum::enum_name(presentMode)); + const auto size = window::get_window_size(); + g_graphicsConfig = GraphicsConfig{ + .surfaceConfiguration = + wgpu::SurfaceConfiguration{ + .format = surfaceFormat, + .usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc, + .width = size.native_fb_width, + .height = size.native_fb_height, + .presentMode = presentMode, + }, + .depthFormat = wgpu::TextureFormat::Depth32Float, + .msaaSamples = g_config.msaa, + .textureAnisotropy = g_config.maxTextureAnisotropy, + .maxTextureDimension2D = maxTextureDimension2D, + }; + create_copy_pipeline(); + { + window::SurfaceLock surfaceLock; + resize_swapchain(size.fb_width, size.fb_height, size.native_fb_width, size.native_fb_height, true); + } + g_initialized.store(true, std::memory_order_release); + return true; +} + +void fail_if_device_lost() noexcept { + if (!g_deviceLost.load(std::memory_order_acquire)) { + return; + } + + // Several frame-owning threads can observe loss, so serialize escalation: one thread logs and the + // rest wait for termination instead of submitting more work to a lost device. + static std::mutex fatalMutex; + const std::lock_guard lock(fatalMutex); + const auto reason = g_deviceLostReason.load(std::memory_order_relaxed); + const char* const detail = g_deviceLostMessage.data(); + if (detail[0] != '\0') { + Log.fatal("WebGPU device was lost ({}: {}). Rendering cannot continue safely; restart the application.", + magic_enum::enum_name(reason), detail); + } else { + Log.fatal("WebGPU device was lost ({}). Rendering cannot continue safely; restart the application.", + magic_enum::enum_name(reason)); + } +} + +void serialize_pipeline_caches() noexcept { +#if defined(WEBGPU_DAWN) && defined(_WIN32) + if (!g_device || g_backendType != wgpu::BackendType::Vulkan) { + return; + } + using PerformIdleTasksFn = void(*)(const wgpu::Device*); + static const auto performIdleTasks = []() -> PerformIdleTasksFn { + const HMODULE dawnModule = GetModuleHandleW(L"webgpu_dawn.dll"); + if (dawnModule == nullptr) { + return nullptr; + } + return reinterpret_cast(reinterpret_cast( + GetProcAddress(dawnModule, "?PerformIdleTasks@native@dawn@@YAXAEBVDevice@wgpu@@@Z"))); + }(); + if (performIdleTasks != nullptr) { + performIdleTasks(&g_device); + } +#endif +} + +void shutdown() { + serialize_pipeline_caches(); + g_initialized.store(false, std::memory_order_release); + g_CopyBindGroupLayout = {}; + g_CopyPipeline = {}; + g_CopyBindGroup = {}; + g_frameBuffer = {}; + g_frameBufferResolved = {}; + g_depthBuffer = {}; + g_queue = {}; + g_surface = {}; + g_device = {}; + g_adapter = {}; + g_instance = {}; + + cache_shutdown(); +} + +void release_surface() noexcept { + const bool hadSurface = static_cast(g_surface); + if (g_surface) { + g_surface.Unconfigure(); + } + g_surface = {}; + if (hadSurface && g_instance && g_device && g_queue) { + const auto future = g_queue.OnSubmittedWorkDone( + wgpu::CallbackMode::WaitAnyOnly, [](wgpu::QueueWorkDoneStatus, wgpu::StringView) {}); + g_instance.WaitAny(future, 1000000000); + } +} + +bool refresh_surface(bool recreate) { + if (!g_instance || !g_device) { + return false; + } + if (!window::is_presentable()) { + release_surface(); + return false; + } + if ((!g_surface || recreate) && !create_surface()) { + return false; + } + uint32_t width = g_graphicsConfig.surfaceConfiguration.width; + uint32_t height = g_graphicsConfig.surfaceConfiguration.height; + uint32_t native_width = width; + uint32_t native_height = height; + if (window::get_sdl_window() != nullptr) { + const auto size = window::get_window_size(); + width = size.fb_width; + height = size.fb_height; + native_width = size.native_fb_width; + native_height = size.native_fb_height; + } + if (width != 0 && height != 0) { + resize_swapchain(width, height, native_width, native_height, true); + } + return true; +} + +void resize_swapchain(uint32_t width, uint32_t height, uint32_t native_width, uint32_t native_height, bool force) { + if (!g_surface || !g_device || width == 0 || height == 0 || native_height == 0 || native_width == 0) { + return; + } + + uint32_t render_width = width; + uint32_t render_height = height; + const auto [efbWidth, efbHeight] = vi::configured_fb_size(); + if (efbWidth != 0 && efbHeight != 0) { + render_width = std::max(render_width, efbWidth); + render_height = std::max(render_height, efbHeight); + } + + const auto requestedRenderSize = RenderTargetSize{render_width, render_height}; + const auto clampedRenderSize = clamp_frame_buffer_size(render_width, render_height); + render_width = clampedRenderSize.width; + render_height = clampedRenderSize.height; + if (requestedRenderSize.width != render_width || requestedRenderSize.height != render_height) { + Log.warn( + "Render target {}x{} exceeds the safe framebuffer budget (adapter max {}, practical max {} / {} " + "pixels); clamping to {}x{}", + requestedRenderSize.width, requestedRenderSize.height, g_graphicsConfig.maxTextureDimension2D, + render_size_limits::kMaxFramebufferDimension, render_size_limits::kMaxFramebufferPixels, render_width, + render_height); + } + + const bool sizeChanged = g_graphicsConfig.surfaceConfiguration.width != native_width || + g_graphicsConfig.surfaceConfiguration.height != native_height || + g_frameBuffer.size.width != render_width || g_frameBuffer.size.height != render_height; + if (!force && !sizeChanged) { + return; + } + if (sizeChanged) { + gx::clear_display_copy_cache(); + gfx::clear_caches(); + clear_present_source_override(); + } + g_graphicsConfig.surfaceConfiguration.width = native_width; + g_graphicsConfig.surfaceConfiguration.height = native_height; + auto surfaceConfiguration = g_graphicsConfig.surfaceConfiguration; + surfaceConfiguration.device = g_device; + g_surface.Configure(&surfaceConfiguration); + if (!sizeChanged) { + // Forced reconfigure at an unchanged size (present-mode change or recreated surface). The + // offscreen targets are not swapchain images, so reallocating them would only stall the frame. + return; + } + g_frameBuffer = create_render_texture(render_width, render_height, true); + g_frameBufferResolved = create_render_texture(render_width, render_height, false); + g_depthBuffer = create_depth_texture(render_width, render_height); + g_CopyBindGroup = create_copy_bind_group(present_source()); +} +} // namespace aurora::webgpu diff --git a/aurora-main/lib/webgpu/gpu.hpp b/aurora-main/lib/webgpu/gpu.hpp new file mode 100644 index 0000000..7b96e95 --- /dev/null +++ b/aurora-main/lib/webgpu/gpu.hpp @@ -0,0 +1,100 @@ +#pragma once + +#include +#include + +#include "wgpu.hpp" + +#include +#include + +struct SDL_Window; + +namespace aurora::webgpu { +struct GraphicsConfig { + wgpu::SurfaceConfiguration surfaceConfiguration; + wgpu::TextureFormat depthFormat; + uint32_t msaaSamples; + uint16_t textureAnisotropy; + uint32_t maxTextureDimension2D; +}; +struct TextureWithSampler { + wgpu::Texture texture; + wgpu::TextureView view; + wgpu::Extent3D size; + wgpu::TextureFormat format; + wgpu::Sampler sampler; +}; +struct PresentSource { + wgpu::BindGroup bindGroup; + wgpu::Texture texture; + wgpu::Extent3D size; + wgpu::TextureFormat format; +}; +struct Viewport { + float left; + float top; + float width; + float height; + float znear; + float zfar; + + bool operator==(const Viewport& rhs) const { + return left == rhs.left && top == rhs.top && width == rhs.width && height == rhs.height && znear == rhs.znear && + zfar == rhs.zfar; + } + bool operator!=(const Viewport& rhs) const { return !(*this == rhs); } +}; + +extern wgpu::Device g_device; +extern wgpu::Queue g_queue; +extern wgpu::Surface g_surface; +extern wgpu::BackendType g_backendType; +extern GraphicsConfig g_graphicsConfig; +extern TextureWithSampler g_frameBuffer; +extern TextureWithSampler g_frameBufferResolved; +extern TextureWithSampler g_depthBuffer; +extern wgpu::RenderPipeline g_CopyPipeline; +extern wgpu::BindGroup g_CopyBindGroup; +extern wgpu::Instance g_instance; +extern bool g_bcTexturesSupported; + +bool initialize(AuroraBackend backend); +void shutdown(); +// Device-loss callbacks may run on a Dawn-managed thread and only publish loss state; frame +// threads call this at ordered boundaries to turn it into the one user-visible fatal error. +void fail_if_device_lost() noexcept; +void release_surface() noexcept; +bool refresh_surface(bool recreate = true); +void resize_swapchain(uint32_t width, uint32_t height, uint32_t native_width, uint32_t native_height, + bool force = false); +TextureWithSampler create_render_texture(uint32_t width, uint32_t height, bool multisampled); +const TextureWithSampler& present_source() noexcept; +PresentSource current_present_source() noexcept; +void set_present_source_override(wgpu::BindGroup bindGroup, wgpu::Texture texture, wgpu::Extent3D size, + wgpu::TextureFormat format) noexcept; +void clear_present_source_override() noexcept; +wgpu::BindGroup create_copy_bind_group(const TextureWithSampler& source); +wgpu::BindGroup create_copy_bind_group(wgpu::TextureView sourceView, wgpu::Sampler sampler); +Viewport calculate_present_viewport(uint32_t surface_width, uint32_t surface_height, uint32_t content_width, + uint32_t content_height) noexcept; +Viewport calculate_present_viewport_for_aspect(uint32_t surface_width, uint32_t surface_height, + float content_aspect) noexcept; +void draw_clear(const wgpu::RenderPassEncoder& pass, bool clearColor, bool clearAlpha, bool clearDepth, + const Vec4& clearColorValue, float clearDepthValue); + +size_t load_from_cache(void const* key, size_t keySize, void* value, size_t valueSize, void* userdata); +void store_to_cache(void const* key, size_t keySize, void const* value, size_t valueSize, void* userdata); +void cache_shutdown(); + +struct BlobCacheStats { + uint64_t lookups; + uint64_t hits; + uint64_t stores; + uint64_t hitBytes; +}; +BlobCacheStats blob_cache_stats() noexcept; +// Persists the monolithic Vulkan pipeline cache into the blob cache (no-op elsewhere). +void serialize_pipeline_caches() noexcept; + +} // namespace aurora::webgpu diff --git a/aurora-main/lib/webgpu/gpu_cache.cpp b/aurora-main/lib/webgpu/gpu_cache.cpp new file mode 100644 index 0000000..b2a477b --- /dev/null +++ b/aurora-main/lib/webgpu/gpu_cache.cpp @@ -0,0 +1,422 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "../fs_helper.hpp" +#include "../internal.hpp" +#include "../sqlite_utils.hpp" +#include "gpu.hpp" + +#include +#include +#if defined(AURORA_CACHE_USE_ZSTD) +#include +#endif +#define XXH_STATIC_LINKING_ONLY +#include + +namespace aurora::webgpu { +static Module Log("aurora::gpu::cache"); + +static sqlite3* db; +static sqlite3_stmt* load_stmt; +static sqlite3_stmt* store_stmt; +static sqlite3_stmt* touch_stmt; +static bool cache_broken; +static std::mutex cache_mutex; +#if defined(AURORA_CACHE_USE_ZSTD) +static std::vector compress_buffer; +#endif + +// Schema 3 added last_used (whole days since the Unix epoch) so stale blobs can be +// pruned: config-version bumps and driver updates change every Dawn cache key, and +// without an age column the orphaned rows accumulate forever (observed 1.6 GB). +// Schema 4 is a content reset for the vulkan_monolithic_pipeline_cache switch, which +// obsoletes every per-pipeline blob at once. +constexpr int CACHE_SCHEMA = 4; +constexpr int64_t PruneAfterDays = 30; +// Rows whose last_used lags today are collected in memory and written in one batch at +// shutdown: the load path sits inside a read transaction that is always rolled back, +// and day granularity makes anything more eager pointless. + +static std::atomic g_lookups{0}; +static std::atomic g_hits{0}; +static std::atomic g_stores{0}; +static std::atomic g_hitBytes{0}; +static std::vector g_pendingTouches; + +static int64_t days_now() { return static_cast(std::time(nullptr) / 86400); } + +static void init_abort() { + cache_broken = true; + sqlite3_close(db); + db = nullptr; +} + +static int check(int ret) { + if (ret != SQLITE_OK) { + Log.error("SQLite operation failed: {}", sqlite3_errmsg(db)); + } + + return ret; +} + +enum class SchemaState { Match, Mismatch, Error }; + +static SchemaState check_schema() { + auto ret = sqlite::exec(db, "CREATE TABLE IF NOT EXISTS aurora_schema(value INTEGER);"); + if (ret != SQLITE_OK) { + Log.error("Failed to create schema table: {}", sqlite3_errmsg(db)); + return SchemaState::Error; + } + + bool match = false; + const auto cmd = fmt::format("SELECT * FROM aurora_schema WHERE value = {}", CACHE_SCHEMA); + ret = sqlite::exec(db, cmd.c_str(), [&match](int, char**, char**) { match = true; }, nullptr); + if (ret != SQLITE_OK) { + Log.error("Failed to check schema table: {}", sqlite3_errmsg(db)); + return SchemaState::Error; + } + return match ? SchemaState::Match : SchemaState::Mismatch; +} + +static bool create_schema() { + sqlite::Transaction tx(db, Log, true); + if (!tx) { + Log.error("Failed to open schema transaction: {}", sqlite3_errmsg(db)); + return false; + } + const auto cmd = fmt::format( + R"(CREATE TABLE IF NOT EXISTS aurora_schema(value INTEGER); +DROP TABLE IF EXISTS cache; +CREATE TABLE cache ( + key BLOB PRIMARY KEY NOT NULL, + value BLOB NOT NULL, + size INTEGER NOT NULL, + compressed INTEGER NOT NULL, + last_used INTEGER NOT NULL DEFAULT 0 +); +DELETE FROM aurora_schema; +INSERT INTO aurora_schema VALUES ({});)", + CACHE_SCHEMA); + const auto ret = sqlite::exec(db, cmd.c_str()); + if (ret != SQLITE_OK) { + Log.error("Failed to create schema: {}", sqlite3_errmsg(db)); + return false; + } + tx.commit(); + return true; +} + +static void prune_stale_rows() { + const auto cmd = fmt::format("DELETE FROM cache WHERE last_used < {}", days_now() - PruneAfterDays); + auto ret = sqlite::exec(db, cmd.c_str()); + if (ret != SQLITE_OK) { + Log.error("Failed to prune stale cache rows: {}", sqlite3_errmsg(db)); + return; + } + const auto pruned = sqlite3_changes(db); + if (pruned > 0) { + Log.info("Pruned {} stale Dawn cache blobs (unused for {}+ days)", pruned, PruneAfterDays); + } + // Freed pages are only reused, never returned to the filesystem, so compact when a + // meaningful amount was dropped. Blobs run hundreds of KB each, making even a few + // hundred rows a noticeable slice of the file. + if (pruned > 256) { + ret = sqlite::exec(db, "VACUUM;"); + if (ret != SQLITE_OK) { + Log.warn("Failed to vacuum Dawn cache after pruning: {}", sqlite3_errmsg(db)); + } + } +} + +static bool cache_init_core() { + Log.debug("SQLite version {}", sqlite3_libversion()); + + const auto path = std::filesystem::path{reinterpret_cast(g_config.cachePath)} / "dawn_cache.db"; + std::string file = fs_path_to_string(path); + Log.debug("Using dawn cache at {}", file); + auto ret = sqlite3_open(file.c_str(), &db); + if (ret != SQLITE_OK) { + Log.error("Failed to open database: {}", sqlite3_errmsg(db)); + return false; + } + + // WAL mode + NORMAL = no need for disk syncs, consistent but not durable is fine. + ret = sqlite::exec(db, "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;"); + if (ret != SQLITE_OK) { + Log.error("Failed to set pragmas: {}", sqlite3_errmsg(db)); + return false; + } + + switch (check_schema()) { + case SchemaState::Match: + prune_stale_rows(); + break; + case SchemaState::Mismatch: { + // Dropping the table would leave the freed pages inside the file, so a schema + // change deletes the database outright; pre-schema-3 files had grown unbounded. + Log.info("Dawn cache schema changed; recreating '{}'", file); + sqlite3_close(db); + db = nullptr; + std::error_code ec; + std::filesystem::remove(path, ec); + std::filesystem::remove(std::filesystem::path{file + "-wal"}, ec); + std::filesystem::remove(std::filesystem::path{file + "-shm"}, ec); + ret = sqlite3_open(file.c_str(), &db); + if (ret != SQLITE_OK) { + Log.error("Failed to recreate database: {}", sqlite3_errmsg(db)); + return false; + } + ret = sqlite::exec(db, "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;"); + if (ret != SQLITE_OK) { + Log.error("Failed to set pragmas: {}", sqlite3_errmsg(db)); + return false; + } + if (!create_schema()) { + return false; + } + break; + } + case SchemaState::Error: + return false; + } + + ret = sqlite3_prepare_v3(db, "SELECT value, size, compressed, last_used FROM cache WHERE key = ?", -1, + SQLITE_PREPARE_PERSISTENT, &load_stmt, nullptr); + if (ret != SQLITE_OK) { + Log.error("Failed to prepare statement: {}", sqlite3_errmsg(db)); + return false; + } + + ret = sqlite3_prepare_v3(db, "REPLACE INTO cache (key, value, size, compressed, last_used) VALUES (?, ?, ?, ?, ?)", + -1, SQLITE_PREPARE_PERSISTENT, &store_stmt, nullptr); + if (ret != SQLITE_OK) { + Log.error("Failed to prepare statement: {}", sqlite3_errmsg(db)); + return false; + } + + ret = sqlite3_prepare_v3(db, "UPDATE cache SET last_used = ? WHERE key = ?", -1, SQLITE_PREPARE_PERSISTENT, + &touch_stmt, nullptr); + if (ret != SQLITE_OK) { + Log.error("Failed to prepare statement: {}", sqlite3_errmsg(db)); + return false; + } + + return true; +} + +// Caller holds cache_mutex. Writes the queued last_used refreshes in one transaction. +static void flush_touches() { + if (g_pendingTouches.empty() || db == nullptr || touch_stmt == nullptr) { + return; + } + + sqlite::Transaction tx(db, Log, true); + if (!tx) { + Log.error("Failed to open touch transaction"); + g_pendingTouches.clear(); + return; + } + const auto today = days_now(); + for (const auto& keyHash : g_pendingTouches) { + check(sqlite3_bind_int64(touch_stmt, 1, today)); + check(sqlite3_bind_blob(touch_stmt, 2, &keyHash, sizeof(keyHash), SQLITE_TRANSIENT)); + if (sqlite3_step(touch_stmt) != SQLITE_DONE) { + Log.error("Failed to refresh cache row age: {}", sqlite3_errmsg(db)); + } + check(sqlite3_reset(touch_stmt)); + } + g_pendingTouches.clear(); + tx.commit(); +} + +static bool cache_init() { + if (cache_broken) { + return false; + } + + if (db) { + return true; + } + + if (!cache_init_core()) { + Log.error("SQLite DB init failed"); + init_abort(); + return false; + } + + Log.debug("SQLite cache init succeeded"); + + return true; +} + +size_t load_from_cache(void const* key, size_t keySize, void* value, size_t valueSize, void*) { + std::lock_guard lock(cache_mutex); + + if (!cache_init()) { + return 0; + } + + sqlite::Transaction tx(db, Log); + if (!tx) { + Log.error("Failed to open load transaction"); + return 0; + } + + // Dawn probes with value == nullptr for the size first, then fetches; count each + // probe as one logical lookup so the hit rate reads per-entry. + if (value == nullptr) { + g_lookups.fetch_add(1, std::memory_order_relaxed); + } + + const auto keyHash = XXH128(key, keySize, 0); + check(sqlite3_bind_blob(load_stmt, 1, &keyHash, sizeof(keyHash), SQLITE_TRANSIENT)); + + const auto ret = sqlite3_step(load_stmt); + size_t foundSize; + if (ret == SQLITE_ROW) { + // Hit + const auto foundPtr = sqlite3_column_blob(load_stmt, 0); + foundSize = sqlite3_column_int64(load_stmt, 1); + const bool compressed = sqlite3_column_int(load_stmt, 2) != 0; + if (value == nullptr) { + g_hits.fetch_add(1, std::memory_order_relaxed); + } else { + g_hitBytes.fetch_add(static_cast(foundSize), std::memory_order_relaxed); + } + if (sqlite3_column_int64(load_stmt, 3) != days_now()) { + g_pendingTouches.push_back(keyHash); + } + + if (value && valueSize == foundSize) { + if (compressed) { +#if defined(AURORA_CACHE_USE_ZSTD) + const auto compSize = sqlite3_column_bytes(load_stmt, 0); + const auto zstdRet = ZSTD_decompress(value, valueSize, foundPtr, compSize); + if (ZSTD_isError(zstdRet)) { + Log.error("zstd decompression error: {}", ZSTD_getErrorName(zstdRet)); + foundSize = 0; + } else if (zstdRet != foundSize) { + Log.error("zstd decompression size mismatch: expected {}, got {}", foundSize, zstdRet); + foundSize = 0; + } +#else + Log.error("Cache entry is zstd-compressed but zstd support is disabled"); + foundSize = 0; +#endif + } else { + if (foundSize != 0 && !foundPtr) { + Log.error("Cache entry is missing raw value data"); + foundSize = 0; + } else if (foundSize != 0) { + std::memcpy(value, foundPtr, foundSize); + } + } + } + } else if (ret == SQLITE_DONE) { + // Miss + foundSize = 0; + } else { + Log.error("Looking up cache key failed: {}", sqlite3_errmsg(db)); + return 0; + } + + check(sqlite3_reset(load_stmt)); + + return foundSize; +} + +void store_to_cache(void const* key, size_t keySize, void const* value, size_t valueSize, void*) { + std::lock_guard lock(cache_mutex); + + if (!cache_init()) { + return; + } + + sqlite::Transaction tx(db, Log, true); + if (!tx) { + Log.error("Failed to open store transaction"); + return; + } + + const void* storedValue = value; + sqlite3_uint64 storedValueSize = valueSize; + int compressed = 0; +#if defined(AURORA_CACHE_USE_ZSTD) + const auto bound = ZSTD_compressBound(valueSize); + if (ZSTD_isError(bound)) { + Log.error("Failed to calculate ZSTD_compressBound: {}", ZSTD_getErrorName(bound)); + return; + } + + if (compress_buffer.size() < bound) { + compress_buffer.resize(bound); + } + + const auto compressRet = ZSTD_compress(compress_buffer.data(), compress_buffer.size(), value, valueSize, 0); + if (ZSTD_isError(compressRet)) { + Log.error("ZSTD compression error: {}", ZSTD_getErrorName(compressRet)); + return; + } + + if (compressRet < valueSize) { + storedValue = compress_buffer.data(); + storedValueSize = compressRet; + compressed = 1; + } +#endif + + const auto keyHash = XXH128(key, keySize, 0); + check(sqlite3_bind_blob64(store_stmt, 1, &keyHash, sizeof(keyHash), SQLITE_TRANSIENT)); + check( + sqlite3_bind_blob64(store_stmt, 2, storedValue, storedValueSize, compressed ? SQLITE_STATIC : SQLITE_TRANSIENT)); + check(sqlite3_bind_int64(store_stmt, 3, static_cast(valueSize))); + check(sqlite3_bind_int(store_stmt, 4, compressed)); + check(sqlite3_bind_int64(store_stmt, 5, days_now())); + g_stores.fetch_add(1, std::memory_order_relaxed); + + const auto ret = sqlite3_step(store_stmt); + if (ret != SQLITE_DONE) { + // Error or something + Log.error("Failed to insert row: {}", sqlite3_errmsg(db)); + return; + } + + check(sqlite3_reset(store_stmt)); + check(sqlite3_bind_null(store_stmt, 2)); + check(sqlite3_bind_null(store_stmt, 4)); + + tx.commit(); +} + +void cache_shutdown() { + std::lock_guard lock(cache_mutex); +#if defined(AURORA_CACHE_USE_ZSTD) + compress_buffer.clear(); +#endif + flush_touches(); + check(sqlite3_finalize(load_stmt)); + check(sqlite3_finalize(store_stmt)); + check(sqlite3_finalize(touch_stmt)); + load_stmt = nullptr; + store_stmt = nullptr; + touch_stmt = nullptr; + check(sqlite3_close(db)); + db = nullptr; +} + +BlobCacheStats blob_cache_stats() noexcept { + return BlobCacheStats{ + .lookups = g_lookups.load(std::memory_order_relaxed), + .hits = g_hits.load(std::memory_order_relaxed), + .stores = g_stores.load(std::memory_order_relaxed), + .hitBytes = g_hitBytes.load(std::memory_order_relaxed), + }; +} + +} // namespace aurora::webgpu diff --git a/aurora-main/lib/webgpu/wgpu.hpp b/aurora-main/lib/webgpu/wgpu.hpp new file mode 100644 index 0000000..ba7ebea --- /dev/null +++ b/aurora-main/lib/webgpu/wgpu.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +#include + +static inline bool operator==(const wgpu::Extent3D& lhs, const wgpu::Extent3D& rhs) { + return lhs.width == rhs.width && lhs.height == rhs.height && lhs.depthOrArrayLayers == rhs.depthOrArrayLayers; +} +static inline bool operator!=(const wgpu::Extent3D& lhs, const wgpu::Extent3D& rhs) { return !(lhs == rhs); } + +namespace wgpu { +inline std::string_view format_as(wgpu::StringView str) { return str; } +} // namespace wgpu diff --git a/aurora-main/lib/window.cpp b/aurora-main/lib/window.cpp new file mode 100644 index 0000000..37d64be --- /dev/null +++ b/aurora-main/lib/window.cpp @@ -0,0 +1,809 @@ +#include "window.hpp" + +#ifdef AURORA_ENABLE_GX +#include "imgui.hpp" +#include "webgpu/gpu.hpp" +#endif +#include "input.hpp" +#include "internal.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(_WIN32) +#include +#endif + +#if defined(SDL_PLATFORM_ANDROID) +#include +extern "C" void Android_LockActivityMutex(void); +extern "C" void Android_UnlockActivityMutex(void); +#endif + +#include +#include +#include + +#include "dolphin/vi/vi_internal.hpp" + +namespace aurora::window { +namespace { +Module Log("aurora::window"); + +SDL_Window* g_window; +SDL_Renderer* g_renderer; +float g_frameBufferScale = 0.f; +bool g_frameBufferAspectFit = true; +bool g_presentSurfaceFill = false; +int g_presentAspectWidth = 0; +int g_presentAspectHeight = 0; +AuroraWindowSize g_windowSize; +std::vector g_events; +std::atomic_bool g_backgrounded = false; +std::atomic_bool g_nativeResizePending = false; +std::atomic g_displayMode{AURORA_DISPLAY_MODE_WINDOWED}; +#if defined(SDL_PLATFORM_ANDROID) +std::atomic_bool g_surfaceReady = false; +#else +std::atomic_bool g_surfaceReady = true; +#endif +bool g_lastPaused = false; +bool g_gotFocus = false; +#if defined(_WIN32) +WNDPROC g_prevWindowProc = nullptr; +// Native window handle captured once when the hook is installed. The presenter checks the native +// size several times per slot, which used to be an SDL lookup for a handle that never changes. +HWND g_nativeWindowHandle = nullptr; +// Packed native client size ((width << 32) | height) kept by the window procedure; zero means +// unknown. Atomic because that procedure runs on the SDL thread while the presenter reads it. +std::atomic g_nativeClientSize{0}; + +constexpr uint64_t pack_native_size(uint32_t width, uint32_t height) noexcept { + return (static_cast(width) << 32) | static_cast(height); +} + +LRESULT CALLBACK aurora_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { + bool sizeChanged = msg == WM_SIZE; + if (msg == WM_WINDOWPOSCHANGED) { + const auto* pos = reinterpret_cast(lparam); + sizeChanged = pos != nullptr && (pos->flags & SWP_NOSIZE) == 0; + } + if (sizeChanged) { + g_nativeResizePending.store(true, std::memory_order_release); + if (msg == WM_SIZE) { + // WM_SIZE carries the new client size directly. + g_nativeClientSize.store(pack_native_size(static_cast(LOWORD(lparam)), + static_cast(HIWORD(lparam))), + std::memory_order_release); + } else { + // WM_WINDOWPOSCHANGED carries no client size. Invalidate before forwarding: DefWindowProc turns + // it into a nested WM_SIZE through this procedure, which refills the cache authoritatively. + g_nativeClientSize.store(0, std::memory_order_release); + } + } + if (g_prevWindowProc == nullptr) { + return DefWindowProc(hwnd, msg, wparam, lparam); + } + return CallWindowProc(g_prevWindowProc, hwnd, msg, wparam, lparam); +} + +void install_window_proc_hook() noexcept { + const auto props = SDL_GetWindowProperties(g_window); + auto* hwnd = static_cast(SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr)); + if (hwnd == nullptr) { + return; + } + g_nativeWindowHandle = hwnd; + g_nativeClientSize.store(0, std::memory_order_release); + g_prevWindowProc = reinterpret_cast(SetWindowLongPtr(hwnd, GWLP_WNDPROC, + reinterpret_cast(aurora_window_proc))); +} + +void uninstall_window_proc_hook() noexcept { + if (g_window == nullptr || g_prevWindowProc == nullptr) { + g_nativeWindowHandle = nullptr; + g_nativeClientSize.store(0, std::memory_order_release); + return; + } + const auto props = SDL_GetWindowProperties(g_window); + auto* hwnd = static_cast(SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr)); + if (hwnd != nullptr) { + SetWindowLongPtr(hwnd, GWLP_WNDPROC, reinterpret_cast(g_prevWindowProc)); + } + g_prevWindowProc = nullptr; + g_nativeWindowHandle = nullptr; + g_nativeClientSize.store(0, std::memory_order_release); +} +#endif + +// Native client size of the window without going through SDL. Returns false when the platform has +// no cheap native query, in which case callers fall back to get_window_size(). +bool query_native_client_size(uint32_t& width, uint32_t& height) noexcept { +#if defined(_WIN32) + const uint64_t cached = g_nativeClientSize.load(std::memory_order_acquire); + if (cached != 0) { + width = static_cast(cached >> 32); + height = static_cast(cached & 0xffffffffull); + return true; + } + if (g_nativeWindowHandle != nullptr) { + RECT client{}; + if (GetClientRect(g_nativeWindowHandle, &client) != 0) { + width = static_cast(std::max(client.right - client.left, 0)); + height = static_cast(std::max(client.bottom - client.top, 0)); + if (width != 0 && height != 0) { + g_nativeClientSize.store(pack_native_size(width, height), std::memory_order_release); + } + return true; + } + } +#else + (void)width; + (void)height; +#endif + return false; +} + +bool operator==(const AuroraWindowSize& lhs, const AuroraWindowSize& rhs) { + return lhs.width == rhs.width && lhs.height == rhs.height && lhs.fb_width == rhs.fb_width && + lhs.fb_height == rhs.fb_height && lhs.native_fb_height == rhs.native_fb_height && + lhs.native_fb_width == rhs.native_fb_width && lhs.scale == rhs.scale; +} + +Vec2 scale_frame_buffer_to_aspect(int w, int h, float scale, float aspect) { + if (w <= 0 || h <= 0) { + return {std::max(w, 1), std::max(h, 1)}; + } + const auto size = render_size_limits::scale_framebuffer_to_aspect( + static_cast(w), static_cast(h), scale, aspect); + return {static_cast(size.width), static_cast(size.height)}; +} + +void resize_swapchain() noexcept { +#if defined(SDL_PLATFORM_ANDROID) + SurfaceLock surfaceLock; +#endif + // Consume only requests that preceded this synchronization; a native resize arriving mid-rebuild + // stays flagged for the next ordered frame boundary. + g_nativeResizePending.exchange(false, std::memory_order_acq_rel); + const auto size = get_window_size(); + if (size == g_windowSize) { + return; + } + if (size.scale != g_windowSize.scale) { + if (g_windowSize.scale > 0.f) { + Log.info("Display scale changed to {}", size.scale); + } + } + g_windowSize = size; + if (g_renderer != nullptr) { + SDL_SetRenderLogicalPresentation(g_renderer, static_cast(size.native_fb_width), + static_cast(size.native_fb_height), SDL_LOGICAL_PRESENTATION_DISABLED); + SDL_SetRenderScale(g_renderer, size.scale, size.scale); + } +#ifdef AURORA_ENABLE_GX + webgpu::resize_swapchain(size.fb_width, size.fb_height, size.native_fb_width, size.native_fb_height); +#endif +} + +void set_window_icon() noexcept { + if (g_config.iconRGBA8 == nullptr) { + return; + } + auto* iconSurface = + SDL_CreateSurfaceFrom(static_cast(g_config.iconWidth), static_cast(g_config.iconHeight), + SDL_GetPixelFormatForMasks(32, 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000), + g_config.iconRGBA8, static_cast(4 * g_config.iconWidth)); + ASSERT(iconSurface != nullptr, "Failed to create icon surface: {}", SDL_GetError()); + TRY_WARN(SDL_SetWindowIcon(g_window, iconSurface), "Failed to set window icon: {}", SDL_GetError()); + SDL_DestroySurface(iconSurface); +} + +bool SDLCALL lifecycle_event_watch(void*, SDL_Event* event) { + switch (event->type) { + case SDL_EVENT_WINDOW_RESIZED: + case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: + case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: + g_nativeResizePending.store(true, std::memory_order_release); + break; +#if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_APPLE) + case SDL_EVENT_WINDOW_MINIMIZED: + g_backgrounded.store(true, std::memory_order_relaxed); + break; + case SDL_EVENT_WINDOW_RESTORED: + g_backgrounded.store(false, std::memory_order_relaxed); + break; +#endif + default: + break; + } + return true; +} + +void sync_paused() { + const bool paused = is_paused(); + if (g_lastPaused == paused) { + return; + } + g_lastPaused = paused; + g_events.push_back(AuroraEvent{ + .type = paused ? AURORA_PAUSED : AURORA_UNPAUSED, + }); +} + +bool is_alt_enter_event(const SDL_Event& event) noexcept { + if (event.type != SDL_EVENT_KEY_DOWN || event.key.repeat) { + return false; + } + + const SDL_Keymod altMask = static_cast(SDL_KMOD_LALT | SDL_KMOD_RALT | SDL_KMOD_ALT); + return event.key.key == SDLK_RETURN && (event.key.mod & altMask) != 0; +} + +void process_event(SDL_Event& event) { +#ifdef AURORA_ENABLE_GX + imgui::process_event(event); +#endif + + switch (event.type) { + case SDL_EVENT_KEY_DOWN: + if (is_alt_enter_event(event)) { + set_display_mode(get_fullscreen() ? AURORA_DISPLAY_MODE_WINDOWED : AURORA_DISPLAY_MODE_BORDERLESS); + g_nativeResizePending.store(true, std::memory_order_release); + } + break; + case SDL_EVENT_WINDOW_MOVED: { + g_events.push_back(AuroraEvent{ + .type = AURORA_WINDOW_MOVED, + .windowPos = {.x = event.window.data1, .y = event.window.data2}, + }); + break; + } + case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: { + g_events.push_back(AuroraEvent{ + .type = AURORA_DISPLAY_SCALE_CHANGED, + .windowSize = get_window_size(), + }); + break; + } + case SDL_EVENT_WINDOW_RESIZED: + case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: { + g_events.push_back(AuroraEvent{ + .type = AURORA_WINDOW_RESIZED, + .windowSize = get_window_size(), + }); + break; + } + case SDL_EVENT_GAMEPAD_ADDED: { + auto instance = input::add_controller(event.gdevice.which); + g_events.push_back(AuroraEvent{ + .type = AURORA_CONTROLLER_ADDED, + .controller = instance, + }); + break; + } + case SDL_EVENT_GAMEPAD_REMOVED: { + input::remove_controller(event.gdevice.which); + g_events.push_back(AuroraEvent{ + .type = AURORA_CONTROLLER_REMOVED, + .controller = event.gdevice.which, + }); + break; + } + case SDL_EVENT_MOUSE_WHEEL: + input::set_mouse_scroll(event.wheel.x, event.wheel.y); + break; + case SDL_EVENT_QUIT: + g_events.push_back(AuroraEvent{ + .type = AURORA_EXIT, + }); + break; + default: + if (event.type == g_sdlCustomEventsStart + CustomEvent::FutureResize) { + // The request flag is set before this wakeup is queued. Renderer resource mutation belongs to + // begin/end_frame_impl, which may be running on the frame worker. + g_events.push_back(AuroraEvent{ + .type = AURORA_WINDOW_RESIZED, + .windowSize = get_window_size(), + }); + } else if (event.type == g_sdlCustomEventsStart + CustomEvent::RefreshSurface) { + // This event only wakes the window loop; the frame worker applies the pending surface + // configuration at its next ordered frame boundary. + } + break; + } + + sync_paused(); + g_events.push_back(AuroraEvent{ + .type = AURORA_SDL_EVENT, + .sdl = event, + }); +} +} // namespace + +const AuroraEvent* poll_events() { + g_events.clear(); + SDL_Event event; + // Clear out the previous scroll values to prevent ghost input + input::set_mouse_scroll(0, 0); + if (is_paused()) { + if (SDL_WaitEvent(&event)) { + process_event(event); + } else { + Log.warn("SDL_WaitEvent failed: {}", SDL_GetError()); + } + } + while (SDL_PollEvent(&event)) { + process_event(event); + } + g_events.push_back(AuroraEvent{ + .type = AURORA_NONE, + }); + return g_events.data(); +} + +bool create_window(AuroraBackend backend) { + SDL_WindowFlags flags = SDL_WINDOW_HIGH_PIXEL_DENSITY; +#if TARGET_OS_IOS || TARGET_OS_TV + flags |= SDL_WINDOW_FULLSCREEN; +#else + flags |= SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE; + if (g_config.startFullscreen) { + flags |= SDL_WINDOW_FULLSCREEN; + } +#endif + switch (backend) { +#ifdef AURORA_ENABLE_GX +#ifdef DAWN_ENABLE_BACKEND_VULKAN + case BACKEND_VULKAN: + flags |= SDL_WINDOW_VULKAN; + break; +#endif +#ifdef DAWN_ENABLE_BACKEND_METAL + case BACKEND_METAL: + flags |= SDL_WINDOW_METAL; + break; +#endif +#ifdef DAWN_ENABLE_BACKEND_OPENGL + case BACKEND_OPENGL: + case BACKEND_OPENGLES: + flags |= SDL_WINDOW_OPENGL; + break; +#endif +#endif + default: + break; + } + auto width = static_cast(g_config.windowWidth); + auto height = static_cast(g_config.windowHeight); + if (width == 0 || height == 0) { + width = 1280; + height = 960; + } + if (width < 640) { + width = 640; + } + if (height < 480) { + height = 480; + } + + const Sint32 posX = g_config.hasWindowPosition ? g_config.windowPosX : SDL_WINDOWPOS_CENTERED; + const Sint32 posY = g_config.hasWindowPosition ? g_config.windowPosY : SDL_WINDOWPOS_CENTERED; + + const auto props = SDL_CreateProperties(); + TRY(SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, g_config.appName), "Failed to set {}: {}", + SDL_PROP_WINDOW_CREATE_TITLE_STRING, SDL_GetError()); + TRY(SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, posX), "Failed to set {}: {}", + SDL_PROP_WINDOW_CREATE_X_NUMBER, SDL_GetError()); + TRY(SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, posY), "Failed to set {}: {}", + SDL_PROP_WINDOW_CREATE_Y_NUMBER, SDL_GetError()); + TRY(SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, width), "Failed to set {}: {}", + SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, SDL_GetError()); + TRY(SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, height), "Failed to set {}: {}", + SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, SDL_GetError()); + TRY(SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_FLAGS_NUMBER, flags), "Failed to set {}: {}", + SDL_PROP_WINDOW_CREATE_FLAGS_NUMBER, SDL_GetError()); + g_window = SDL_CreateWindowWithProperties(props); + if (g_window == nullptr) { + Log.error("Failed to create window: {}", SDL_GetError()); + return false; + } + SDL_SetWindowMinimumSize(g_window, 640, 480); + g_displayMode.store(g_config.startFullscreen ? AURORA_DISPLAY_MODE_BORDERLESS : AURORA_DISPLAY_MODE_WINDOWED, + std::memory_order_release); +#if defined(_WIN32) + install_window_proc_hook(); +#endif + set_window_icon(); + return true; +} + +bool create_renderer() { + if (g_window == nullptr) { + return false; + } + const auto props = SDL_CreateProperties(); + TRY(SDL_SetPointerProperty(props, SDL_PROP_RENDERER_CREATE_WINDOW_POINTER, g_window), "Failed to set {}: {}", + SDL_PROP_RENDERER_CREATE_WINDOW_POINTER, SDL_GetError()); + // Never vsync: presentation must not throttle the guest's own pacing. + TRY(SDL_SetNumberProperty(props, SDL_PROP_RENDERER_CREATE_PRESENT_VSYNC_NUMBER, SDL_RENDERER_VSYNC_DISABLED), + "Failed to set {}: {}", SDL_PROP_RENDERER_CREATE_PRESENT_VSYNC_NUMBER, SDL_GetError()); + g_renderer = SDL_CreateRendererWithProperties(props); + if (g_renderer == nullptr) { + Log.error("Failed to create renderer: {}", SDL_GetError()); + return false; + } + return true; +} + +void destroy_window() { +#if defined(_WIN32) + uninstall_window_proc_hook(); +#endif + if (g_renderer != nullptr) { + SDL_DestroyRenderer(g_renderer); + g_renderer = nullptr; + } + if (g_window != nullptr) { + SDL_DestroyWindow(g_window); + g_window = nullptr; + } +} + +void show_window() { + if (g_window != nullptr) { + TRY_WARN(SDL_ShowWindow(g_window), "Failed to show window: {}", SDL_GetError()); + } +} + +bool initialize() { + /* We don't want to initialize anything input related here, otherwise the add events will get lost to the void */ + TRY(SDL_SetHint(SDL_HINT_ORIENTATIONS, "LandscapeLeft LandscapeRight"), "Error setting {}: {}", SDL_HINT_ORIENTATIONS, + SDL_GetError()); + TRY(SDL_InitSubSystem(SDL_INIT_EVENTS | SDL_INIT_VIDEO), "Error initializing SDL: {}", SDL_GetError()); + +#if !defined(_WIN32) && !defined(__APPLE__) + TRY(SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0"), "Error setting {}: {}", + SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, SDL_GetError()); +#endif + TRY(SDL_SetHint(SDL_HINT_SCREENSAVER_INHIBIT_ACTIVITY_NAME, g_config.appName), "Error setting {}: {}", + SDL_HINT_SCREENSAVER_INHIBIT_ACTIVITY_NAME, SDL_GetError()); + TRY(SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_GAMECUBE_RUMBLE_BRAKE, "1"), "Error setting {}: {}", + SDL_HINT_JOYSTICK_HIDAPI_GAMECUBE_RUMBLE_BRAKE, SDL_GetError()); + + TRY(SDL_DisableScreenSaver(), "Error disabling screensaver: {}", SDL_GetError()); + if (g_config.allowJoystickBackgroundEvents) { + TRY(SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"), "Error setting {}: {}", + SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, SDL_GetError()); + } + + return true; +} + +bool initialize_event_watch() { + TRY(SDL_AddEventWatch(lifecycle_event_watch, nullptr), "Error adding SDL event watch: {}", SDL_GetError()); + return true; +} + +void shutdown() { + SDL_RemoveEventWatch(lifecycle_event_watch, nullptr); + destroy_window(); + TRY_WARN(SDL_EnableScreenSaver(), "Error enabling screensaver: {}", SDL_GetError()); + SDL_Quit(); +} + +AuroraWindowSize get_window_size() { + int width = 0; + int height = 0; + int native_fb_w = 0; + int native_fb_h = 0; + ASSERT(SDL_GetWindowSize(g_window, &width, &height), "Failed to get window size: {}", SDL_GetError()); + ASSERT(SDL_GetWindowSizeInPixels(g_window, &native_fb_w, &native_fb_h), "Failed to get window size in pixels: {}", + SDL_GetError()); + + int fb_w = native_fb_w; + int fb_h = native_fb_h; + const auto [baseW, baseH] = vi::configured_fb_size(); + if (g_frameBufferAspectFit && baseW > 0 && baseH > 0) { + float renderScale = g_frameBufferScale > 0.f ? g_frameBufferScale : 1.f; + if (g_frameBufferScale <= 0.f) { + renderScale = std::min(static_cast(native_fb_w) / static_cast(baseW), + static_cast(native_fb_h) / static_cast(baseH)); + if (renderScale < 1.f) { + renderScale = 1.f; + } + } + fb_w = std::max(1, static_cast(std::lround(static_cast(baseW) * renderScale))); + fb_h = std::max(1, static_cast(std::lround(static_cast(baseH) * renderScale))); + } else if (g_frameBufferScale > 0.f && baseW > 0 && baseH > 0) { + const auto [scaledW, scaledH] = + scale_frame_buffer_to_aspect(static_cast(baseW), static_cast(baseH), g_frameBufferScale, + static_cast(fb_w) / static_cast(fb_h)); + fb_w = scaledW; + fb_h = scaledH; + } + + const float scale = SDL_GetWindowDisplayScale(g_window); + return { + .width = static_cast(width), + .height = static_cast(height), + .fb_width = static_cast(fb_w), + .fb_height = static_cast(fb_h), + .native_fb_width = static_cast(native_fb_w), + .native_fb_height = static_cast(native_fb_h), + .scale = scale, + }; +} + +SDL_Window* get_sdl_window() { return g_window; } + +SDL_Renderer* get_sdl_renderer() { return g_renderer; } + +bool is_paused() noexcept { + if (!is_presentable()) { + return true; + } + const auto flags = SDL_GetWindowFlags(g_window); + if ((flags & SDL_WINDOW_HIDDEN) != 0u) { + return true; + } + // Wait until the window has received focus before respecting pauseOnFocusLost + if (!g_gotFocus) { + g_gotFocus = (flags & SDL_WINDOW_INPUT_FOCUS) != 0u; + return false; + } + return g_config.pauseOnFocusLost && ((flags & SDL_WINDOW_INPUT_FOCUS) == 0u || (flags & SDL_WINDOW_MINIMIZED) != 0u); +} + +bool is_presentable() noexcept { + return g_window != nullptr && !g_backgrounded.load(std::memory_order_acquire) && + g_surfaceReady.load(std::memory_order_acquire); +} + +void pump_events() noexcept { + if (g_window != nullptr) { + SDL_SyncWindow(g_window); + } + SDL_PumpEvents(); +} + +bool native_resize_pending() noexcept { return g_nativeResizePending.load(std::memory_order_acquire); } + +bool native_window_size_matches(uint32_t width, uint32_t height) noexcept { + if (g_window == nullptr || width == 0 || height == 0) { + return true; + } + uint32_t nativeWidth = 0; + uint32_t nativeHeight = 0; + if (query_native_client_size(nativeWidth, nativeHeight)) { + return nativeWidth == width && nativeHeight == height; + } + const auto size = get_window_size(); + return size.native_fb_width == width && size.native_fb_height == height; +} + +void set_surface_ready(bool ready) noexcept { g_surfaceReady.store(ready, std::memory_order_release); } + +SurfaceLock::SurfaceLock() noexcept { +#if defined(SDL_PLATFORM_ANDROID) + Android_LockActivityMutex(); +#endif +} + +SurfaceLock::~SurfaceLock() { +#if defined(SDL_PLATFORM_ANDROID) + Android_UnlockActivityMutex(); +#endif +} + +bool push_custom_event(CustomEvent eventType) { + SDL_Event event{.type = g_sdlCustomEventsStart + eventType}; + return SDL_PushEvent(&event); +} + +void set_title(const char* title) { + TRY_WARN(SDL_SetWindowTitle(g_window, title), "Failed to set window title: {}", SDL_GetError()); +} + +void set_fullscreen(bool fullscreen) { + set_display_mode(fullscreen ? AURORA_DISPLAY_MODE_BORDERLESS : AURORA_DISPLAY_MODE_WINDOWED); +} + +bool get_fullscreen() { return (SDL_GetWindowFlags(g_window) & SDL_WINDOW_FULLSCREEN) != 0u; } + +void set_display_mode(AuroraDisplayMode mode) { + if (g_window == nullptr) { + return; + } + if (mode < AURORA_DISPLAY_MODE_WINDOWED || mode > AURORA_DISPLAY_MODE_EXCLUSIVE) { + Log.warn("Ignoring invalid display mode {}", static_cast(mode)); + return; + } + const AuroraDisplayMode currentMode = g_displayMode.load(std::memory_order_acquire); + // Exclusive mode is re-applied even when already active: the desired refresh + // rate tracks the frame interpolation target, which can change at runtime. + if (mode == currentMode && mode != AURORA_DISPLAY_MODE_EXCLUSIVE) { + return; + } + + bool success = true; + if (mode == AURORA_DISPLAY_MODE_WINDOWED) { + success = SDL_SetWindowFullscreen(g_window, false); + if (success) { + success = SDL_SetWindowFullscreenMode(g_window, nullptr); + } + } else if (mode == AURORA_DISPLAY_MODE_BORDERLESS) { + if (SDL_GetWindowFullscreenMode(g_window) != nullptr) { + success = SDL_SetWindowFullscreen(g_window, false); + } + if (success) { + success = SDL_SetWindowFullscreenMode(g_window, nullptr); + } + if (success) { + success = SDL_SetWindowFullscreen(g_window, true); + } + } else { + const SDL_DisplayID display = SDL_GetDisplayForWindow(g_window); + const SDL_DisplayMode* desktop = display != 0 ? SDL_GetDesktopDisplayMode(display) : nullptr; + const uint32_t interpolationFps = aurora_get_frame_interpolation_fps(); + const float targetHz = interpolationFps > 60 ? static_cast(interpolationFps) : 60.0f; + SDL_DisplayMode closest{}; + if (desktop == nullptr || + !SDL_GetClosestFullscreenDisplayMode(display, desktop->w, desktop->h, targetHz, true, &closest)) { + Log.warn("No exclusive fullscreen mode near {:.0f} Hz is available: {}", targetHz, SDL_GetError()); + return; + } + const SDL_DisplayMode* active = SDL_GetWindowFullscreenMode(g_window); + if (mode == currentMode && active != nullptr && active->w == closest.w && active->h == closest.h && + active->refresh_rate == closest.refresh_rate) { + return; + } + if (get_fullscreen() && mode != currentMode) { + success = SDL_SetWindowFullscreen(g_window, false); + } + if (success) { + success = SDL_SetWindowFullscreenMode(g_window, &closest); + } + if (success) { + success = SDL_SetWindowFullscreen(g_window, true); + } + if (success) { + Log.info("Exclusive fullscreen requested at {}x{} {:.3f} Hz", closest.w, closest.h, closest.refresh_rate); + } + } + + if (!success) { + Log.warn("Failed to change display mode: {}", SDL_GetError()); + return; + } + g_displayMode.store(mode, std::memory_order_release); + g_nativeResizePending.store(true, std::memory_order_release); + TRY_WARN(push_custom_event(CustomEvent::FutureResize), "Failed to queue fullscreen resize event: {}", + SDL_GetError()); +} + +AuroraDisplayMode get_display_mode() { return g_displayMode.load(std::memory_order_acquire); } + +void set_window_size(uint32_t width, uint32_t height) { + TRY_WARN(SDL_RestoreWindow(g_window), "Failed to un-maximize window: {}", SDL_GetError()); + TRY_WARN(SDL_SetWindowSize(g_window, width, height), "Failed to set window size: {}", SDL_GetError()); +} + +void set_window_position(uint32_t x, uint32_t y) { + TRY_WARN(SDL_SetWindowPosition(g_window, x, y), "Failed to set window position: {}", SDL_GetError()); +} + +void center_window() { + TRY_WARN(SDL_SetWindowPosition(g_window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED), + "Failed to center window: {}", SDL_GetError()); +} + +void sync_frame_buffer_size() noexcept { + if (g_window != nullptr) { + resize_swapchain(); + } +} + +static void push_future_resize_event() { + TRY_WARN(push_custom_event(CustomEvent::FutureResize), "Failed to push SDL event for future resize: {}", + SDL_GetError()); +} + +void request_frame_buffer_resize() { + if (g_window != nullptr) { + // Publish the request before waking the SDL loop: the render worker consumes it at an ordered + // frame boundary, and the main thread must never recreate WebGPU targets mid-frame. + g_nativeResizePending.store(true, std::memory_order_release); + push_future_resize_event(); + } +} + +void set_frame_buffer_scale(float scale) { + if (scale < 0.f) { + scale = 0.f; + } + if (g_frameBufferScale == scale) { + return; + } + g_frameBufferScale = scale; + request_frame_buffer_resize(); +} + +void set_frame_buffer_aspect_fit(bool fit) { + if (g_frameBufferAspectFit == fit) { + return; + } + + g_frameBufferAspectFit = fit; + request_frame_buffer_resize(); +} + +void set_present_surface_fill(bool fill) { + g_presentSurfaceFill = fill; +} + +void lock_present_aspect_ratio(int width, int height) { + if (width <= 0 || height <= 0) { + unlock_present_aspect_ratio(); + return; + } + if (g_presentAspectWidth == width && g_presentAspectHeight == height) { + return; + } + g_presentAspectWidth = width; + g_presentAspectHeight = height; +} + +void unlock_present_aspect_ratio() { + g_presentAspectWidth = 0; + g_presentAspectHeight = 0; +} + +bool get_present_aspect_ratio(float& aspect) noexcept { + if (g_presentSurfaceFill && g_window != nullptr) { + // Queried once per presentation snapshot; use the cached native client size + // instead of re-entering SDL for a value the window procedure already knows. + uint32_t nativeWidth = 0; + uint32_t nativeHeight = 0; + if (query_native_client_size(nativeWidth, nativeHeight) && nativeWidth > 0 && nativeHeight > 0) { + aspect = static_cast(nativeWidth) / static_cast(nativeHeight); + return true; + } + int width = 0; + int height = 0; + if (SDL_GetWindowSizeInPixels(g_window, &width, &height) && width > 0 && height > 0) { + aspect = static_cast(width) / static_cast(height); + return true; + } + } + if (g_presentAspectWidth <= 0 || g_presentAspectHeight <= 0) { + aspect = 0.f; + return false; + } + aspect = (static_cast(g_presentAspectWidth) / static_cast(g_presentAspectHeight)) * + vi::present_aspect_correction(); + return aspect > 0.f; +} + +void set_background_input(bool value) { SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, value ? "1" : "0"); } + +} // namespace aurora::window + +#if defined(SDL_PLATFORM_ANDROID) +extern "C" JNIEXPORT void JNICALL Java_org_libsdl_app_SDLSurface_auroraNativeSetSurfaceReady(JNIEnv*, jclass, + jboolean ready) { + aurora::window::set_surface_ready(ready == JNI_TRUE); +} +#endif diff --git a/aurora-main/lib/window.hpp b/aurora-main/lib/window.hpp new file mode 100644 index 0000000..22d249f --- /dev/null +++ b/aurora-main/lib/window.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include + +struct SDL_Window; +struct SDL_Renderer; + +namespace aurora::window { + +enum class CustomEvent { + FutureResize, + RefreshSurface, + End, +}; +static Uint32 operator+(Uint32 lhs, CustomEvent rhs) { return lhs + static_cast(rhs); } + +// On Android in particular, we need to hold a mutex around critical areas like surface creation +// and presentation, so that the SDLActivity doesn't destroy the surface out from underneath us. +class SurfaceLock { +public: + SurfaceLock() noexcept; + ~SurfaceLock(); + + SurfaceLock(const SurfaceLock&) = delete; + SurfaceLock& operator=(const SurfaceLock&) = delete; +}; + +bool initialize(); +bool initialize_event_watch(); +bool push_custom_event(CustomEvent eventType); +void shutdown(); +bool create_window(AuroraBackend backend); +bool create_renderer(); +void destroy_window(); +void show_window(); +AuroraWindowSize get_window_size(); +const AuroraEvent* poll_events(); +SDL_Window* get_sdl_window(); +SDL_Renderer* get_sdl_renderer(); +bool is_paused() noexcept; +bool is_presentable() noexcept; +void pump_events() noexcept; +bool native_resize_pending() noexcept; +bool native_window_size_matches(uint32_t width, uint32_t height) noexcept; +void set_surface_ready(bool ready) noexcept; +void set_title(const char* title); +void set_fullscreen(bool fullscreen); +bool get_fullscreen(); +void set_display_mode(AuroraDisplayMode mode); +AuroraDisplayMode get_display_mode(); +void set_window_size(uint32_t width, uint32_t height); +void set_window_position(uint32_t x, uint32_t y); +void center_window(); +void sync_frame_buffer_size() noexcept; +void request_frame_buffer_resize(); +void set_frame_buffer_scale(float scale); +void set_frame_buffer_aspect_fit(bool fit); +void set_present_surface_fill(bool fill); +void lock_present_aspect_ratio(int width, int height); +void unlock_present_aspect_ratio(); +bool get_present_aspect_ratio(float& aspect) noexcept; +void set_background_input(bool value); +}; // namespace aurora::window diff --git a/aurora-main/tests/CMakeLists.txt b/aurora-main/tests/CMakeLists.txt new file mode 100644 index 0000000..9e224a0 --- /dev/null +++ b/aurora-main/tests/CMakeLists.txt @@ -0,0 +1,95 @@ +include(FetchContent) +include(GoogleTest) + +if (NOT TARGET gtest) + FetchContent_Declare(googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.17.0.tar.gz + URL_HASH SHA256=65fab701d9829d38cb77c14acdc431d2108bfdbf8979e40eb8ae567edf10b27c + DOWNLOAD_EXTRACT_TIMESTAMP TRUE + EXCLUDE_FROM_ALL + ) + FetchContent_MakeAvailable(googletest) +endif () + +if (AURORA_ENABLE_GX) + # GX FIFO round-trip tests + # Compile GX sources directly to avoid pulling in the full renderer/WebGPU runtime + add_executable(gx_fifo_tests + gx_fifo_test.cpp + gx_test_stubs.cpp + texture_bind_group_cache_key_test.cpp + ../lib/gfx/efb_ram_encoder.cpp + # GX API implementations (encoders) + ../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/GXAurora.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 + # FIFO/command processor (encoder + decoder) + ../lib/gx/fifo.cpp + ../lib/gx/command_processor.cpp + ../lib/gx/shader_info.cpp + ../lib/gx/frame_interpolation.cpp + ) + + target_include_directories(gx_fifo_tests PRIVATE + ../include + ../lib + ../lib/dolphin/gx + ) + + target_compile_definitions(gx_fifo_tests PRIVATE AURORA TARGET_PC) + + target_link_libraries(gx_fifo_tests PRIVATE + gtest + gtest_main + fmt::fmt + xxhash + absl::flat_hash_map + absl::btree + dawn::dawncpp_headers + TracyClient + ${AURORA_SDL3_TARGET} + ) + + gtest_discover_tests(gx_fifo_tests) +endif () # AURORA_ENABLE_GX + +# DVD API tests +if (TARGET aurora::dvd) + add_executable(dvd_tests test_dvd.cpp) + target_link_libraries(dvd_tests PRIVATE aurora::dvd gtest gtest_main) + + # Pass disc image path as a compile definition if provided + if (DEFINED DVD_TEST_IMAGE AND EXISTS "${DVD_TEST_IMAGE}") + target_compile_definitions(dvd_tests PRIVATE DVD_TEST_IMAGE="${DVD_TEST_IMAGE}") + endif () + + gtest_discover_tests(dvd_tests) +endif () + +add_executable(os_alloc_tests + os_alloc_test.cpp + os_test_globals.cpp + ../lib/dolphin/os/OSAlloc.cpp + ../lib/logging.cpp +) +target_include_directories(os_alloc_tests PRIVATE + ../include +) +target_compile_definitions(os_alloc_tests PRIVATE AURORA TARGET_PC) +target_link_libraries(os_alloc_tests PRIVATE gtest gtest_main fmt::fmt) +gtest_discover_tests(os_alloc_tests) diff --git a/aurora-main/tests/gx_fifo_test.cpp b/aurora-main/tests/gx_fifo_test.cpp new file mode 100644 index 0000000..5aea9bb --- /dev/null +++ b/aurora-main/tests/gx_fifo_test.cpp @@ -0,0 +1,4948 @@ +// GX FIFO encode/decode round-trip tests: call a GX function, capture the FIFO bytes, reset +// g_gxState, feed them to command_processor::process(), then check the decoded state. + +#include "gx_test_common.hpp" +#include "gfx/efb_ram_encoder.hpp" +#include "gfx/tex_copy_format_contract.hpp" +#include "gx/shader_info.hpp" +#include "gx/pipeline.hpp" +#include "__gx.h" + +#include +#include +#include +#include + +using aurora::gx::g_gxState; + +TEST(GXPipelineConfig, RejectsInvalidDeserializedEnums) { + aurora::gx::PipelineConfig config{}; + EXPECT_TRUE(aurora::gx::valid_pipeline_config(config)); + + config.depthFunc = static_cast(0x009F01F2); + EXPECT_FALSE(aurora::gx::valid_pipeline_config(config)); +} + +TEST(GXPipelineConfig, FoggedLateZLogicOrPreservesEggDofMask) { + EXPECT_EQ(aurora::gx::effective_pipeline_fog_type(GX_FOG_PERSP_LIN, GX_ZT_REPLACE, + false, GX_BM_LOGIC, GX_LO_OR), + GX_FOG_PERSP_LIN); + + // Keep the existing defensive suppression for late-Z mask passes whose + // integer logic operation still has only an inexact WebGPU fallback. + EXPECT_EQ(aurora::gx::effective_pipeline_fog_type(GX_FOG_PERSP_LIN, GX_ZT_REPLACE, + false, GX_BM_LOGIC, GX_LO_COPY), + GX_FOG_NONE); + + // Logic fallbacks do not require this workaround when Z-texturing is early. + EXPECT_EQ(aurora::gx::effective_pipeline_fog_type(GX_FOG_PERSP_LIN, GX_ZT_REPLACE, + true, GX_BM_LOGIC, GX_LO_COPY), + GX_FOG_PERSP_LIN); +} + +TEST(GXShaderInfo, ShaderLightSanitizesNonFiniteDirectionForUniforms) { + aurora::gx::Light light{}; + light.dir = { + std::numeric_limits::quiet_NaN(), + std::numeric_limits::infinity(), + -std::numeric_limits::infinity(), + }; + + const auto sanitized = aurora::gx::prepare_shader_light(light); + EXPECT_FLOAT_EQ(sanitized.dir[0], 0.0f); + EXPECT_FLOAT_EQ(sanitized.dir[1], 0.0f); + EXPECT_FLOAT_EQ(sanitized.dir[2], 0.0f); + + light.dir = {3.0f, 4.0f, 0.0f}; + const auto normalized = aurora::gx::prepare_shader_light(light); + EXPECT_FLOAT_EQ(normalized.dir[0], 0.6f); + EXPECT_FLOAT_EQ(normalized.dir[1], 0.8f); + EXPECT_FLOAT_EQ(normalized.dir[2], 0.0f); + + light.dir = {0.0f, 0.0f, 0.0f}; + const auto zero = aurora::gx::prepare_shader_light(light); + EXPECT_FLOAT_EQ(zero.dir[0], 0.0f); + EXPECT_FLOAT_EQ(zero.dir[1], 0.0f); + EXPECT_FLOAT_EQ(zero.dir[2], 0.0f); +} + +TEST(GXLighting, SpotCoefficientsAndPositionGetterMatchRevolutionSdk) { + GXLightObj light{}; + GXInitLightSpot(&light, 60.0f, GX_SP_SHARP); + + float a0 = 0.0f; + float a1 = 0.0f; + float a2 = 0.0f; + GXGetLightAttnA(&light, &a0, &a1, &a2); + EXPECT_NEAR(a0, -3.0f, 0.00001f); + EXPECT_NEAR(a1, 8.0f, 0.00001f); + EXPECT_NEAR(a2, -4.0f, 0.00001f); + + GXInitLightSpot(&light, 60.0f, GX_SP_RING1); + GXGetLightAttnA(&light, &a0, &a1, &a2); + EXPECT_NEAR(a0, -8.0f, 0.00001f); + EXPECT_NEAR(a1, 24.0f, 0.00001f); + EXPECT_NEAR(a2, -16.0f, 0.00001f); + + GXInitLightPos(&light, 10.0f, 20.0f, 30.0f); + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + GXGetLightPos(&light, &x, &y, &z); + EXPECT_FLOAT_EQ(x, 10.0f); + EXPECT_FLOAT_EQ(y, 20.0f); + EXPECT_FLOAT_EQ(z, 30.0f); +} + +TEST(EfbRamEncoderContract, Z24X8WritesNativeFourByFourArGbPlanes) { + std::array rgba{}; + for (u32 i = 0; i < 16; ++i) { + rgba[i * 4 + 0] = static_cast(0x10 + i); // high Z + rgba[i * 4 + 1] = static_cast(0x40 + i); // middle Z + rgba[i * 4 + 2] = static_cast(0x80 + i); // low Z + rgba[i * 4 + 3] = 0xff; + } + + std::array encoded{}; + ASSERT_TRUE(aurora::gfx::efb_ram::encode( + encoded.data(), encoded.size(), GX_TF_Z24X8, 4, 4, rgba.data(), 4, 4, 16, + aurora::gfx::efb_ram::HostPixelOrder::RGBA)); + EXPECT_EQ(aurora::gfx::efb_ram::encoded_size(GX_TF_Z24X8, 4, 4), encoded.size()); + + for (u32 i = 0; i < 16; ++i) { + EXPECT_EQ(encoded[i * 2 + 0], 0xff) << "AR plane texel " << i; + EXPECT_EQ(encoded[i * 2 + 1], 0x10 + i) << "AR plane texel " << i; + EXPECT_EQ(encoded[32 + i * 2 + 0], 0x40 + i) << "GB plane texel " << i; + EXPECT_EQ(encoded[32 + i * 2 + 1], 0x80 + i) << "GB plane texel " << i; + + const u32 depth = (static_cast(encoded[i * 2 + 1]) << 16) | + (static_cast(encoded[32 + i * 2 + 0]) << 8) | + encoded[32 + i * 2 + 1]; + EXPECT_EQ(depth, ((0x10u + i) << 16) | ((0x40u + i) << 8) | (0x80u + i)); + } +} + +static bool has_bp_write(const std::vector& bytes, u8 reg) { + const std::array pattern{0x61, reg}; + return std::search(bytes.begin(), bytes.end(), pattern.begin(), pattern.end()) != bytes.end(); +} + +static void expect_fog_raw_fields_match_decoded_state() { + const float expectedA = std::ldexp(g_gxState.fog.aRaw, static_cast(g_gxState.fog.bShift)); + const float bMant = static_cast(g_gxState.fog.bMagnitude) / 8388638.0f; + const float expectedB = std::ldexp(bMant, static_cast(g_gxState.fog.bShift) - 1); + EXPECT_NEAR(g_gxState.fog.a, expectedA, std::max(std::abs(g_gxState.fog.a) * 1e-3f, 1e-6f)); + EXPECT_NEAR(g_gxState.fog.b, expectedB, std::max(std::abs(g_gxState.fog.b) * 1e-3f, 1e-6f)); +} + +static bool has_aurora_cmd(const std::vector& bytes, u16 cmd) { + const std::array pattern{GX_LOAD_AURORA, static_cast(cmd >> 8), static_cast(cmd & 0xFF)}; + return std::search(bytes.begin(), bytes.end(), pattern.begin(), pattern.end()) != bytes.end(); +} + +static std::vector bp_cmd(u8 reg, u32 value) { + return {0x61, reg, static_cast((value >> 16) & 0xFF), static_cast((value >> 8) & 0xFF), + static_cast(value & 0xFF)}; +} + +static std::vector cp_cmd(u8 reg, u32 value) { + return {0x08, reg, static_cast((value >> 24) & 0xFF), static_cast((value >> 16) & 0xFF), + static_cast((value >> 8) & 0xFF), static_cast(value & 0xFF)}; +} + +static std::vector xf_cmd(u16 addr, std::initializer_list values) { + std::vector bytes; + bytes.reserve(5 + values.size() * 4); + bytes.push_back(0x10); + + const u32 header = ((static_cast(values.size() - 1) & 0xFFFFu) << 16) | addr; + bytes.push_back(static_cast((header >> 24) & 0xFF)); + bytes.push_back(static_cast((header >> 16) & 0xFF)); + bytes.push_back(static_cast((header >> 8) & 0xFF)); + bytes.push_back(static_cast(header & 0xFF)); + + for (const u32 value : values) { + bytes.push_back(static_cast((value >> 24) & 0xFF)); + bytes.push_back(static_cast((value >> 16) & 0xFF)); + bytes.push_back(static_cast((value >> 8) & 0xFF)); + bytes.push_back(static_cast(value & 0xFF)); + } + return bytes; +} + +static u32 read_be32_at(const std::vector& bytes, size_t offset) { + return (static_cast(bytes[offset]) << 24) | (static_cast(bytes[offset + 1]) << 16) | + (static_cast(bytes[offset + 2]) << 8) | static_cast(bytes[offset + 3]); +} + +TEST(FrameInterpolationContract, RequiresStablePerspectiveDrawSequence) { + const auto resetInterpolation = [] { + aurora::gx::set_frame_interpolation_fps(0); + aurora::gx::begin_frame_interpolation(); + }; + const auto buildFrame = [](const aurora::gx::ShaderInfo& info, aurora::HashType signatureBase, + bool perspective, bool reverse = false) { + aurora::gx::begin_frame_interpolation(); + aurora::gx::BindGroupRanges ranges{}; + uint32_t interpolatedUniforms = 0; + for (uint32_t i = 0; i < 8; ++i) { + const uint32_t signatureOffset = reverse ? 7 - i : i; + const aurora::gx::FrameInterpolationDrawIdentity identity{ + .combined = signatureBase + signatureOffset, + .pipeline = signatureBase, + .texture = 1, + }; + const auto uniforms = aurora::gx::build_uniform(info, 0, ranges, identity, perspective); + interpolatedUniforms += static_cast(std::count_if( + uniforms.interpolated.begin(), uniforms.interpolated.end(), + [](const aurora::gfx::Range& range) { return range.size != 0; })); + } + aurora::gx::finalize_frame_interpolation(); + return interpolatedUniforms; + }; + + resetInterpolation(); + aurora::gx::set_frame_interpolation_fps(120); + const auto info = aurora::gx::build_shader_info({}); + + // Slots are inserted whenever interpolation is configured, so a frame with no matches just fills + // them with duplicates. The staged uniform counts below are what verify the matching. + EXPECT_EQ(buildFrame(info, 100, true), 0u); + EXPECT_TRUE(aurora::gx::has_interpolated_frame()); + + EXPECT_EQ(buildFrame(info, 100, true), 8u); + EXPECT_TRUE(aurora::gx::has_interpolated_frame()); + EXPECT_EQ(aurora::gx::interpolated_frame_count(), 1u); + EXPECT_TRUE(aurora::gx::frame_interpolation_replay_safe()); + + EXPECT_EQ(buildFrame(info, 100, true, true), 8u); + EXPECT_TRUE(aurora::gx::has_interpolated_frame()); + + aurora::gx::mark_frame_interpolation_replay_unsafe(); + EXPECT_FALSE(aurora::gx::frame_interpolation_replay_safe()); + EXPECT_EQ(aurora::gx::interpolated_frame_count(), 1u); + + aurora::gx::set_frame_interpolation_fps(180); + EXPECT_EQ(buildFrame(info, 250, true), 0u); + EXPECT_TRUE(aurora::gx::frame_interpolation_replay_safe()); + EXPECT_TRUE(aurora::gx::has_interpolated_frame()); + EXPECT_EQ(buildFrame(info, 250, true), 16u); + EXPECT_TRUE(aurora::gx::has_interpolated_frame()); + EXPECT_EQ(aurora::gx::interpolated_frame_count(), 2u); + + aurora::gx::set_frame_interpolation_fps(120); + EXPECT_EQ(buildFrame(info, 200, true), 0u); + EXPECT_TRUE(aurora::gx::has_interpolated_frame()); + + EXPECT_EQ(buildFrame(info, 200, false), 0u); + EXPECT_TRUE(aurora::gx::has_interpolated_frame()); + + aurora::gx::set_frame_interpolation_fps(240); + EXPECT_EQ(buildFrame(info, 300, true), 0u); + EXPECT_TRUE(aurora::gx::has_interpolated_frame()); + EXPECT_EQ(buildFrame(info, 300, true), 24u); + EXPECT_TRUE(aurora::gx::has_interpolated_frame()); + EXPECT_EQ(aurora::gx::interpolated_frame_count(), 3u); + + // CPU-deformed draws change their vertex bytes every frame, so their stable pipeline and texture + // identity still has to retain transform history for the camera and model matrices. + aurora::gx::set_frame_interpolation_fps(120); + const auto buildDeformedFrame = [&](aurora::HashType geometryEpoch) { + aurora::gx::begin_frame_interpolation(); + aurora::gx::BindGroupRanges ranges{}; + uint32_t interpolatedUniforms = 0; + for (uint32_t i = 0; i < 8; ++i) { + const aurora::gx::FrameInterpolationDrawIdentity identity{ + .combined = geometryEpoch + i, + .pipeline = 42, + .texture = 7, + }; + const auto uniforms = aurora::gx::build_uniform(info, 0, ranges, identity, true); + interpolatedUniforms += static_cast(std::count_if( + uniforms.interpolated.begin(), uniforms.interpolated.end(), + [](const aurora::gfx::Range& range) { return range.size != 0; })); + } + aurora::gx::finalize_frame_interpolation(); + return interpolatedUniforms; + }; + EXPECT_EQ(buildDeformedFrame(400), 0u); + EXPECT_EQ(buildDeformedFrame(500), 8u); + EXPECT_TRUE(aurora::gx::has_interpolated_frame()); + + resetInterpolation(); +} + +TEST(FrameInterpolationContract, DecomposesRotationScaleAndTranslation) { + const aurora::Mat3x4 previous{ + {1.0f, 0.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 1.0f, 0.0f}, + }; + const aurora::Mat3x4 current{ + {0.0f, -2.0f, 0.0f, 10.0f}, + {2.0f, 0.0f, 0.0f, 20.0f}, + {0.0f, 0.0f, 2.0f, 30.0f}, + }; + aurora::Mat3x4 midpoint{}; + ASSERT_TRUE(aurora::gx::interpolate_transform_midpoint(previous, current, midpoint)); + + const float expectedAxis = std::sqrt(0.5f) * 1.5f; + EXPECT_NEAR(midpoint.m0.x(), expectedAxis, 1.0e-5f); + EXPECT_NEAR(midpoint.m0.y(), -expectedAxis, 1.0e-5f); + EXPECT_NEAR(midpoint.m1.x(), expectedAxis, 1.0e-5f); + EXPECT_NEAR(midpoint.m1.y(), expectedAxis, 1.0e-5f); + EXPECT_NEAR(midpoint.m2.z(), 1.5f, 1.0e-5f); + EXPECT_FLOAT_EQ(midpoint.m0.w(), 5.0f); + EXPECT_FLOAT_EQ(midpoint.m1.w(), 10.0f); + EXPECT_FLOAT_EQ(midpoint.m2.w(), 15.0f); + + aurora::Mat3x4 quarter{}; + aurora::Mat3x4 threeQuarter{}; + ASSERT_TRUE(aurora::gx::interpolate_transform(previous, current, 0.25f, quarter)); + ASSERT_TRUE(aurora::gx::interpolate_transform(previous, current, 0.75f, threeQuarter)); + EXPECT_FLOAT_EQ(quarter.m0.w(), 2.5f); + EXPECT_FLOAT_EQ(quarter.m1.w(), 5.0f); + EXPECT_FLOAT_EQ(quarter.m2.w(), 7.5f); + EXPECT_FLOAT_EQ(threeQuarter.m0.w(), 7.5f); + EXPECT_FLOAT_EQ(threeQuarter.m1.w(), 15.0f); + EXPECT_FLOAT_EQ(threeQuarter.m2.w(), 22.5f); +} + +TEST(FrameInterpolationContract, IndexedPaletteInterpolationPreservesSharedSeams) { + const aurora::Mat3x4 identity{ + {1.0f, 0.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 1.0f, 0.0f}, + }; + const aurora::Mat3x4 quarterTurn{ + {0.0f, -1.0f, 0.0f, 0.0f}, + {1.0f, 0.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 1.0f, 0.0f}, + }; + const aurora::Mat3x4 translatedPrevious{ + {1.0f, 0.0f, 0.0f, 1.0f}, + {0.0f, 1.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 1.0f, 0.0f}, + }; + const aurora::Mat3x4 translatedCurrent{ + {1.0f, 0.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f, 1.0f}, + {0.0f, 0.0f, 1.0f, 0.0f}, + }; + + aurora::Mat3x4 rotatedMidpoint{}; + aurora::Mat3x4 translatedMidpoint{}; + ASSERT_TRUE(aurora::gx::interpolate_indexed_transform( + identity, quarterTurn, 0.5f, rotatedMidpoint)); + ASSERT_TRUE(aurora::gx::interpolate_indexed_transform( + translatedPrevious, translatedCurrent, 0.5f, translatedMidpoint)); + + const auto transformPoint = [](const aurora::Mat3x4& matrix, + const std::array& point) { + return std::array{ + matrix.m0.x() * point[0] + matrix.m0.y() * point[1] + + matrix.m0.z() * point[2] + matrix.m0.w(), + matrix.m1.x() * point[0] + matrix.m1.y() * point[1] + + matrix.m1.z() * point[2] + matrix.m1.w(), + matrix.m2.x() * point[0] + matrix.m2.y() * point[1] + + matrix.m2.z() * point[2] + matrix.m2.w(), + }; + }; + + // Two representations of the same seam point that agree at both real frames, so coefficient + // interpolation has to keep them coincident; a rigid decompose would pull them apart. + const auto rotatedSeam = transformPoint(rotatedMidpoint, {1.0f, 0.0f, 0.0f}); + const auto translatedSeam = transformPoint(translatedMidpoint, {0.0f, 0.0f, 0.0f}); + for (size_t component = 0; component < rotatedSeam.size(); ++component) { + EXPECT_FLOAT_EQ(rotatedSeam[component], translatedSeam[component]); + } + EXPECT_FLOAT_EQ(rotatedSeam[0], 0.5f); + EXPECT_FLOAT_EQ(rotatedSeam[1], 0.5f); + + // Composed palette matrices may legitimately contain shear, so the indexed path interpolates it + // instead of rejecting the matrix and snapping those vertices to the new frame. + const aurora::Mat3x4 sheared{ + {1.0f, 0.5f, 0.0f, 4.0f}, + {0.0f, 1.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 1.0f, 0.0f}, + }; + aurora::Mat3x4 shearedMidpoint{}; + ASSERT_TRUE(aurora::gx::interpolate_indexed_transform( + identity, sheared, 0.5f, shearedMidpoint)); + EXPECT_FLOAT_EQ(shearedMidpoint.m0.y(), 0.25f); + EXPECT_FLOAT_EQ(shearedMidpoint.m0.w(), 2.0f); +} + +TEST(FrameInterpolationContract, IndexedPaletteHistoryKeepsAbsoluteVertexSlots) { + constexpr uint16_t usedMask = (1u << 0) | (1u << 1); + constexpr size_t projectionOffset = 0; + constexpr size_t positionOffset = sizeof(aurora::Mat4x4); + constexpr size_t normalOffset = + positionOffset + aurora::gx::MaxPnMtx * sizeof(aurora::Mat3x4); + constexpr size_t uniformSize = + normalOffset + aurora::gx::MaxPnMtx * sizeof(aurora::Mat3x4); + const aurora::gx::FrameInterpolationDrawIdentity identity{ + .combined = 0x1234, + .pipeline = 0x5678, + .texture = 0x9abc, + .matrixTopology = 0xdef0, + }; + const aurora::Mat4x4 projection{}; + const auto matrixAt = [](float x) { + return aurora::Mat3x4{ + {1.0f, 0.0f, 0.0f, x}, + {0.0f, 1.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 1.0f, 0.0f}, + }; + }; + const auto recordFrame = [&](const aurora::gx::FrameInterpolationDrawIdentity& drawIdentity, + float slot0X, float slot1X, + std::array& source) { + g_gxState.pnMtx[0].pos = matrixAt(slot0X); + g_gxState.pnMtx[1].pos = matrixAt(slot1X); + g_gxState.pnMtx[0].nrm = matrixAt(0.0f); + g_gxState.pnMtx[1].nrm = matrixAt(0.0f); + std::memcpy(source.data() + positionOffset, &g_gxState.pnMtx[0].pos, + sizeof(aurora::Mat3x4)); + std::memcpy(source.data() + positionOffset + sizeof(aurora::Mat3x4), + &g_gxState.pnMtx[1].pos, sizeof(aurora::Mat3x4)); + std::memcpy(source.data() + normalOffset, &g_gxState.pnMtx[0].nrm, + sizeof(aurora::Mat3x4)); + std::memcpy(source.data() + normalOffset + sizeof(aurora::Mat3x4), + &g_gxState.pnMtx[1].nrm, sizeof(aurora::Mat3x4)); + return aurora::gx::record_interpolation_draw( + drawIdentity, projection, usedMask, + aurora::gx::InterpolatedUniformLayout{ + .sourceUniformData = source.data(), + .uniformSize = source.size(), + .projectionOffset = projectionOffset, + .positionOffset = positionOffset, + .normalOffset = normalOffset, + .currentMatrix = 0, + .indexedMatrices = true, + }); + }; + + aurora::gx::set_frame_interpolation_fps(0); + aurora::gx::begin_frame_interpolation(); + aurora::gx::set_frame_interpolation_fps(120); + + std::array previousSource{}; + aurora::gx::begin_frame_interpolation(); + recordFrame(identity, 0.0f, 100.0f, previousSource); + aurora::gx::finalize_frame_interpolation(); + + // Current slot 0 is spatially nearest previous slot 1 and vice versa. The old nearest-unused + // heuristic crossed them even though vertex PNMTXIDX bytes address absolute slots 0 and 1. + aurora::gfx::testing::reset_uniform_allocations(); + std::array currentSource{}; + aurora::gx::begin_frame_interpolation(); + const auto ranges = recordFrame(identity, 90.0f, 10.0f, currentSource); + ASSERT_NE(ranges[0].size, 0u); + aurora::gx::finalize_frame_interpolation(); + + const auto& interpolated = aurora::gfx::testing::uniform_allocation(0); + ASSERT_EQ(interpolated.size(), uniformSize); + aurora::Mat3x4 slot0Midpoint{}; + aurora::Mat3x4 slot1Midpoint{}; + std::memcpy(static_cast(&slot0Midpoint), interpolated.data() + positionOffset, + sizeof(slot0Midpoint)); + std::memcpy(static_cast(&slot1Midpoint), + interpolated.data() + positionOffset + sizeof(slot0Midpoint), + sizeof(slot1Midpoint)); + EXPECT_FLOAT_EQ(slot0Midpoint.m0.w(), 45.0f); + EXPECT_FLOAT_EQ(slot1Midpoint.m0.w(), 55.0f); + + // A changed vertex-to-slot topology is not recoverable from matrix values, so it must not fall + // into the coarse material-only bucket even when the used mask is unchanged. + auto changedTopology = identity; + ++changedTopology.combined; + ++changedTopology.matrixTopology; + aurora::gfx::testing::reset_uniform_allocations(); + std::array changedSource{}; + aurora::gx::begin_frame_interpolation(); + const auto changedRanges = recordFrame(changedTopology, 91.0f, 9.0f, changedSource); + EXPECT_EQ(changedRanges[0].size, 0u); + aurora::gx::finalize_frame_interpolation(); + + aurora::gx::set_frame_interpolation_fps(0); + aurora::gx::begin_frame_interpolation(); + aurora::gfx::testing::reset_uniform_allocations(); +} + +TEST(FrameInterpolationContract, RepeatedDrawsMatchByTransformInsteadOfDrawOrder) { + const aurora::Mat3x4 left{ + {1.0f, 0.0f, 0.0f, -500.0f}, + {0.0f, 1.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 1.0f, 1000.0f}, + }; + const aurora::Mat3x4 right{ + {1.0f, 0.0f, 0.0f, 500.0f}, + {0.0f, 1.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 1.0f, 1000.0f}, + }; + const aurora::Mat3x4 currentRight{ + {0.9998f, 0.0f, 0.02f, 496.0f}, + {0.0f, 1.0f, 0.0f, 1.0f}, + {-0.02f, 0.0f, 0.9998f, 997.0f}, + }; + + // A transparent sorter may submit right then left this frame after left then right last frame. + // The signatures are identical, so spatial continuity has to pick the historical transform. + EXPECT_LT(aurora::gx::transform_match_distance_squared(right, currentRight), + aurora::gx::transform_match_distance_squared(left, currentRight)); +} + +TEST(VertexColorContract, MissingAndSparseColorsFollowGxRasterDefaults) { + aurora::gx::ShaderConfig config{}; + + EXPECT_EQ(aurora::gx::shader_vertex_color_attr(config, 0), -1); + EXPECT_EQ(aurora::gx::shader_vertex_color_attr(config, 1), -1); + + config.attrs[GX_VA_CLR1].attrType = GX_DIRECT; + EXPECT_EQ(aurora::gx::shader_vertex_color_attr(config, 0), GX_VA_CLR1); + EXPECT_EQ(aurora::gx::shader_vertex_color_attr(config, 1), -1); + + config.attrs[GX_VA_CLR0].attrType = GX_DIRECT; + EXPECT_EQ(aurora::gx::shader_vertex_color_attr(config, 0), GX_VA_CLR0); + EXPECT_EQ(aurora::gx::shader_vertex_color_attr(config, 1), GX_VA_CLR1); + + config.attrs[GX_VA_CLR1].attrType = GX_NONE; + EXPECT_EQ(aurora::gx::shader_vertex_color_attr(config, 0), GX_VA_CLR0); + EXPECT_EQ(aurora::gx::shader_vertex_color_attr(config, 1), -1); +} + +TEST(TevTexcoordStateContract, DirectStageFeedsFollowingAddPrevInFixedPoint) { + aurora::gx::ShaderConfig config{}; + config.numTexGens = 2; + config.numIndStages = 1; + config.tevStageCount = 2; + config.indStages[0].texCoordId = GX_TEXCOORD0; + config.indStages[0].texMapId = GX_TEXMAP0; + config.indStages[0].scaleS = GX_ITS_1; + config.indStages[0].scaleT = GX_ITS_1; + + config.tevStages[0].texCoordId = GX_TEXCOORD1; + config.tevStages[0].texMapId = GX_TEXMAP_NULL; + config.tevStages[0].indTexMtxId = GX_ITM_OFF; + config.tevStages[1].indTexMtxId = GX_ITM_1; + config.tevStages[1].indTexAddPrev = true; + config.tevStages[1].indTexWrapS = GX_ITW_0; + config.tevStages[1].indTexWrapT = GX_ITW_0; + config.tevStages[1].texCoordId = GX_TEXCOORD_NULL; + config.tevStages[1].texMapId = GX_TEXMAP1; + config.tevStages[1].colorPass.d = GX_CC_TEXC; + + // Stage 0 is direct and stage 1 consumes the coordinate it establishes. Disabling stage 1's + // texture map does not stop that update, so its GX_TEXCOORD_NULL resolves to coord 0. + const auto directDependency = aurora::gx::tev_stage_texture_dependency(config, 0); + const auto indirectDependency = aurora::gx::tev_stage_texture_dependency(config, 1); + EXPECT_EQ(directDependency.texCoordId, 1); + EXPECT_EQ(directDependency.texMapId, -1); + EXPECT_TRUE(directDependency.needsFixedTexcoordState); + EXPECT_FALSE(directDependency.canSampleTexture); + EXPECT_EQ(indirectDependency.texCoordId, 0); + EXPECT_EQ(indirectDependency.texMapId, 1); + EXPECT_TRUE(indirectDependency.needsFixedTexcoordState); + EXPECT_TRUE(indirectDependency.combinerUsesTexture); + EXPECT_TRUE(indirectDependency.canSampleTexture); + EXPECT_TRUE(aurora::gx::shader_uses_fixed_texcoord_state(config)); + + // The production ShaderInfo implementation, not the old empty stub: both effective coords, the + // regular texture and the indirect lookup have to appear in the binding contract. + const auto info = aurora::gx::build_shader_info(config); + EXPECT_TRUE(info.sampledTexCoords.test(0)); + EXPECT_TRUE(info.sampledTexCoords.test(1)); + EXPECT_TRUE(info.sampledTextures.test(0)); + EXPECT_TRUE(info.sampledTextures.test(1)); + EXPECT_TRUE(info.usedIndStages.test(0)); + EXPECT_TRUE(info.usedIndTexMtxs.test(GX_ITM_1 - GX_ITM_0)); + EXPECT_EQ(aurora::gx::tev_indirect_wrap_mask(GX_ITW_256), 0x7fff); + EXPECT_EQ(aurora::gx::tev_indirect_wrap_mask(static_cast(GX_MAX_ITWRAP)), 0); + EXPECT_EQ(aurora::gx::indirect_matrix_mantissa(0.5f), 512); + EXPECT_EQ(aurora::gx::indirect_matrix_mantissa(-0.5f), -512); + EXPECT_EQ(aurora::gx::indirect_matrix_shift(3), -3); + EXPECT_EQ(aurora::gx::indirect_matrix_shift(-5), 5); + EXPECT_EQ(aurora::gx::tev_s24_wrap(0x007fffff), 0x007fffff); + EXPECT_EQ(aurora::gx::tev_s24_wrap(0x00800000), -0x00800000); + EXPECT_EQ(aurora::gx::tev_s24_wrap(-0x00800000), -0x00800000); + EXPECT_EQ(aurora::gx::tev_s24_wrap(-0x00800001), 0x007fffff); + + config.tevStages[1].texMapId = GX_TEXMAP_NULL; + EXPECT_FALSE(aurora::gx::tev_stage_texture_dependency(config, 1).canSampleTexture); + config.tevStages[1].texMapId = GX_TEXMAP1; + + // No regular texture lookup occurs with zero texgens, but an enabled + // indirect stage still samples its texture at the literal zero coordinate. + config.numTexGens = 0; + EXPECT_EQ(aurora::gx::tev_stage_texture_dependency(config, 1).texCoordId, -1); + EXPECT_FALSE(aurora::gx::tev_stage_texture_dependency(config, 1).canSampleTexture); + const auto zeroTexgenInfo = aurora::gx::build_shader_info(config); + EXPECT_FALSE(zeroTexgenInfo.sampledTexCoords.any()); + EXPECT_TRUE(zeroTexgenInfo.sampledTextures.test(0)); + EXPECT_FALSE(zeroTexgenInfo.sampledTextures.test(1)); + // WGSL must likewise avoid producing a regular-texture UV expression for + // stage 1: tex1_size_bias is intentionally absent from this uniform layout. + const auto zeroTexgenDependency = aurora::gx::tev_stage_texture_dependency(config, 1); + EXPECT_FALSE(aurora::gx::tev_texture_sample_enabled(zeroTexgenDependency, + zeroTexgenDependency.combinerUsesTexture)); + + // A standalone direct stage still uses the normalized fast path. + config.numTexGens = 2; + config.tevStageCount = 1; + config.tevStages[1] = {}; + config.tevStages[0].texMapId = GX_TEXMAP1; + config.tevStages[0].colorPass.d = GX_CC_TEXC; + const auto directSampleDependency = aurora::gx::tev_stage_texture_dependency(config, 0); + EXPECT_FALSE(directSampleDependency.needsFixedTexcoordState); + EXPECT_TRUE(directSampleDependency.combinerUsesTexture); + EXPECT_TRUE(directSampleDependency.canSampleTexture); + const auto directInfo = aurora::gx::build_shader_info(config); + EXPECT_TRUE(directInfo.sampledTexCoords.test(1)); + EXPECT_TRUE(directInfo.sampledTextures.test(1)); + + // Z-texture remains enabled even without a regular source stage; the shader + // applies its bias/op to a zero raw texture value in that case. + config.tevStages[0].texMapId = GX_TEXMAP_NULL; + config.tevStages[0].colorPass.d = GX_CC_ZERO; + config.zTexture = static_cast(GX_ZT_REPLACE) << 26; + EXPECT_TRUE(aurora::gx::tev_z_texture_enabled(config)); + EXPECT_EQ(aurora::gx::tev_z_texture_stage(config), -1); + +} + +TEST(TevRegisterLivenessContract, RgbWriteDoesNotHideSameStageOldAlphaRead) { + aurora::gx::ShaderConfig config{}; + config.tevStageCount = 1; + config.tevStages[0].colorOp.outReg = GX_TEVREG0; + config.tevStages[0].alphaPass.d = GX_CA_A0; + + const auto info = aurora::gx::build_shader_info(config); + EXPECT_TRUE(info.writesTevRegRgb.test(GX_TEVREG0)); + EXPECT_FALSE(info.writesTevRegAlpha.test(GX_TEVREG0)); + EXPECT_FALSE(info.loadsTevRegRgb.test(GX_TEVREG0)); + EXPECT_TRUE(info.loadsTevRegAlpha.test(GX_TEVREG0)); +} + +TEST(TevRegisterLivenessContract, TracksOppositeHalvesIndependentlyAcrossStages) { + aurora::gx::ShaderConfig config{}; + config.tevStageCount = 2; + config.tevStages[0].colorOp.outReg = GX_TEVREG0; + config.tevStages[0].alphaOp.outReg = GX_TEVREG1; + config.tevStages[1].colorPass.a = GX_CC_C1; + config.tevStages[1].alphaPass.a = GX_CA_A0; + + const auto info = aurora::gx::build_shader_info(config); + EXPECT_TRUE(info.loadsTevRegAlpha.test(GX_TEVREG0)); + EXPECT_FALSE(info.loadsTevRegRgb.test(GX_TEVREG0)); + EXPECT_TRUE(info.loadsTevRegRgb.test(GX_TEVREG1)); + EXPECT_FALSE(info.loadsTevRegAlpha.test(GX_TEVREG1)); +} + +TEST(TevRegisterLivenessContract, PacksOneUniformWhenBothHalvesNeedInitialValue) { + aurora::gx::ShaderConfig baseline{}; + baseline.tevStageCount = 1; + + auto config = baseline; + config.tevStages[0].colorPass.a = GX_CC_C0; + config.tevStages[0].alphaPass.a = GX_CA_A0; + + const auto baselineInfo = aurora::gx::build_shader_info(baseline); + const auto info = aurora::gx::build_shader_info(config); + EXPECT_TRUE(info.loadsTevRegRgb.test(GX_TEVREG0)); + EXPECT_TRUE(info.loadsTevRegAlpha.test(GX_TEVREG0)); + EXPECT_EQ(info.uniformSize, baselineInfo.uniformSize + sizeof(aurora::Vec4)); +} + +// BP registers (direct FIFO writes, no dirty state flush needed) + +// --- GXSetBlendMode (BP 0x41) --- + +TEST_F(GXFifoTest, BlendMode_Blend_SrcAlpha) { + GXSetBlendMode(GX_BM_BLEND, GX_BL_SRCALPHA, GX_BL_INVSRCALPHA, GX_LO_NOOP); + auto bytes = capture_fifo(); + + // Validate encoding: BP opcode 0x61, register ID 0x41 + ASSERT_GE(bytes.size(), 5u); + EXPECT_EQ(bytes[0], 0x61); + EXPECT_EQ(bytes[1], 0x41); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.blendMode, GX_BM_BLEND); + EXPECT_EQ(g_gxState.blendFacSrc, GX_BL_SRCALPHA); + EXPECT_EQ(g_gxState.blendFacDst, GX_BL_INVSRCALPHA); +} + +TEST_F(GXFifoTest, BlendMode_None) { + GXSetBlendMode(GX_BM_NONE, GX_BL_ZERO, GX_BL_ZERO, GX_LO_CLEAR); + auto bytes = capture_fifo(); + + reset_gx_state(); + // Pre-set to something else to prove the decode works + g_gxState.blendMode = GX_BM_BLEND; + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.blendMode, GX_BM_NONE); +} + +TEST_F(GXFifoTest, BlendMode_Subtract) { + GXSetBlendMode(GX_BM_SUBTRACT, GX_BL_ONE, GX_BL_ONE, GX_LO_NOOP); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.blendMode, GX_BM_SUBTRACT); +} + +TEST_F(GXFifoTest, BlendMode_Logic) { + GXSetBlendMode(GX_BM_LOGIC, GX_BL_ONE, GX_BL_ZERO, GX_LO_XOR); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.blendMode, GX_BM_LOGIC); + EXPECT_EQ(g_gxState.blendOp, GX_LO_XOR); +} + +TEST_F(GXFifoTest, BpMask_AppliesOnlyToNextWrite) { + std::vector bytes; + auto mask = bp_cmd(0xFE, 1u << 19); + auto genMode = bp_cmd(0x00, 1u << 19); + auto cullAndInd = bp_cmd(0x00, (2u << 14) | (3u << 16)); + bytes.insert(bytes.end(), mask.begin(), mask.end()); + bytes.insert(bytes.end(), genMode.begin(), genMode.end()); + bytes.insert(bytes.end(), cullAndInd.begin(), cullAndInd.end()); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.bpRegCache[0x00] & (1u << 19), 0u); + EXPECT_EQ(g_gxState.cullMode, GX_CULL_FRONT); + EXPECT_EQ(g_gxState.numIndStages, 3u); +} + +TEST_F(GXFifoTest, IndirectTextureMask_DecodesWithoutChangingBpWriteMask) { + auto bytes = bp_cmd(0x0F, 0x5A); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.indTexMask, 0x5Au); + EXPECT_EQ(g_gxState.bpRegCache[0x0F], 0x0F00005Au); + EXPECT_EQ(g_gxState.bpRegCache[0xFE], 0x00FFFFFFu); +} + +// --- GXSetColorUpdate / GXSetAlphaUpdate (BP 0x41 cmode0) --- + +TEST_F(GXFifoTest, ColorUpdate_Disabled) { + GXSetColorUpdate(GX_FALSE); + auto bytes = capture_fifo(); + + reset_gx_state(); + g_gxState.colorUpdate = true; + decode_fifo(bytes); + + EXPECT_FALSE(g_gxState.colorUpdate); +} + +TEST_F(GXFifoTest, AlphaUpdate_Disabled) { + GXSetAlphaUpdate(false); + auto bytes = capture_fifo(); + + reset_gx_state(); + g_gxState.alphaUpdate = true; + decode_fifo(bytes); + + EXPECT_FALSE(g_gxState.alphaUpdate); +} + +// --- GXSetZMode (BP 0x40) --- + +TEST_F(GXFifoTest, ZMode_LessNoUpdate) { + GXSetZMode(true, GX_LESS, false); + auto bytes = capture_fifo(); + + ASSERT_GE(bytes.size(), 5u); + EXPECT_EQ(bytes[0], 0x61); + EXPECT_EQ(bytes[1], 0x40); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_TRUE(g_gxState.depthCompare); + EXPECT_EQ(g_gxState.depthFunc, GX_LESS); + EXPECT_FALSE(g_gxState.depthUpdate); +} + +TEST_F(GXFifoTest, ZMode_AlwaysUpdate) { + GXSetZMode(true, GX_ALWAYS, true); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_TRUE(g_gxState.depthCompare); + EXPECT_EQ(g_gxState.depthFunc, GX_ALWAYS); + EXPECT_TRUE(g_gxState.depthUpdate); +} + +TEST_F(GXFifoTest, ZMode_Disabled) { + GXSetZMode(false, GX_NEVER, false); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_FALSE(g_gxState.depthCompare); + EXPECT_EQ(g_gxState.depthFunc, GX_NEVER); + EXPECT_FALSE(g_gxState.depthUpdate); +} + +TEST_F(GXFifoTest, ZTexture_ReplaceZ24X8) { + GXSetZTexture(GX_ZT_REPLACE, GX_TF_Z24X8, 0x123456); + auto bytes = capture_fifo(); + + EXPECT_TRUE(has_bp_write(bytes, 0xF4)); + EXPECT_TRUE(has_bp_write(bytes, 0xF5)); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.zTextureBias, 0x123456u); + EXPECT_EQ(g_gxState.zTextureFmt, 2u); + EXPECT_EQ(g_gxState.zTextureOp, GX_ZT_REPLACE); +} + +// --- GXSetAlphaCompare (BP 0xF3) --- + +TEST_F(GXFifoTest, AlphaCompare_GreaterThan128) { + GXSetAlphaCompare(GX_GREATER, 128, GX_AOP_AND, GX_ALWAYS, 0); + auto bytes = capture_fifo(); + + ASSERT_GE(bytes.size(), 5u); + EXPECT_EQ(bytes[0], 0x61); + EXPECT_EQ(bytes[1], 0xF3); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.alphaCompare.comp0, GX_GREATER); + EXPECT_EQ(g_gxState.alphaCompare.ref0, 128u); + EXPECT_EQ(g_gxState.alphaCompare.op, GX_AOP_AND); + EXPECT_EQ(g_gxState.alphaCompare.comp1, GX_ALWAYS); + EXPECT_EQ(g_gxState.alphaCompare.ref1, 0u); +} + +TEST_F(GXFifoTest, AlphaCompare_OrGequal) { + GXSetAlphaCompare(GX_GEQUAL, 64, GX_AOP_OR, GX_LEQUAL, 200); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.alphaCompare.comp0, GX_GEQUAL); + EXPECT_EQ(g_gxState.alphaCompare.ref0, 64u); + EXPECT_EQ(g_gxState.alphaCompare.op, GX_AOP_OR); + EXPECT_EQ(g_gxState.alphaCompare.comp1, GX_LEQUAL); + EXPECT_EQ(g_gxState.alphaCompare.ref1, 200u); +} + +// --- GXSetDstAlpha (BP 0x42) --- + +TEST_F(GXFifoTest, DstAlpha_Enabled) { + GXSetDstAlpha(true, 0x80); + auto bytes = capture_fifo(); + + reset_gx_state(); + g_gxState.dstAlpha = UINT32_MAX; + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.dstAlpha, 0x80u); +} + +TEST_F(GXFifoTest, DstAlpha_Disabled) { + GXSetDstAlpha(false, 0); + auto bytes = capture_fifo(); + + reset_gx_state(); + g_gxState.dstAlpha = 0x80; + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.dstAlpha, UINT32_MAX); +} + +// --- GXSetPixelFmt (BP 0x43, 0x42 + genMode flush) --- + +TEST_F(GXFifoTest, PixelFmt_Rgb565Z16_Decode) { + GXSetPixelFmt(GX_PF_RGB565_Z16, GX_ZC_FAR); + auto bytes = flush_and_capture(); + + EXPECT_TRUE(has_bp_write(bytes, 0x43)); + EXPECT_TRUE(has_bp_write(bytes, 0x00)); + + reset_gx_state(); + g_gxState.pixelFmt = GX_PF_RGB8_Z24; + g_gxState.zFmt = GX_ZC_LINEAR; + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.pixelFmt, GX_PF_RGB565_Z16); + EXPECT_EQ(g_gxState.zFmt, GX_ZC_FAR); + EXPECT_TRUE(g_gxState.zCompLocBeforeTex); +} + +TEST_F(GXFifoTest, PixelFmt_U8_Decode) { + GXSetPixelFmt(GX_PF_U8, GX_ZC_MID); + auto bytes = flush_and_capture(); + + EXPECT_TRUE(has_bp_write(bytes, 0x43)); + EXPECT_TRUE(has_bp_write(bytes, 0x42)); + EXPECT_TRUE(has_bp_write(bytes, 0x00)); + + reset_gx_state(); + g_gxState.pixelFmt = GX_PF_RGB8_Z24; + g_gxState.zFmt = GX_ZC_LINEAR; + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.pixelFmt, GX_PF_U8); + EXPECT_EQ(g_gxState.zFmt, GX_ZC_MID); + EXPECT_EQ(g_gxState.dstAlpha, UINT32_MAX); + EXPECT_TRUE(g_gxState.zCompLocBeforeTex); +} + +// TEV registers (direct FIFO writes) + +// --- GXSetTevColorIn / GXSetTevAlphaIn --- + +TEST_F(GXFifoTest, TevColorIn_Stage0) { + GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_TEXC, GX_CC_RASC, GX_CC_ZERO); + auto bytes = capture_fifo(); + + // BP opcode 0x61, register 0xC0 (stage 0 color) + ASSERT_GE(bytes.size(), 5u); + EXPECT_EQ(bytes[0], 0x61); + EXPECT_EQ(bytes[1], 0xC0); + + reset_gx_state(); + decode_fifo(bytes); + + auto& s = g_gxState.tevStages[0]; + EXPECT_EQ(s.colorPass.a, GX_CC_ZERO); + EXPECT_EQ(s.colorPass.b, GX_CC_TEXC); + EXPECT_EQ(s.colorPass.c, GX_CC_RASC); + EXPECT_EQ(s.colorPass.d, GX_CC_ZERO); +} + +TEST_F(GXFifoTest, TevAlphaIn_Stage0) { + GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_TEXA, GX_CA_RASA, GX_CA_ZERO); + auto bytes = capture_fifo(); + + // BP opcode 0x61, register 0xC1 (stage 0 alpha) + ASSERT_GE(bytes.size(), 5u); + EXPECT_EQ(bytes[0], 0x61); + EXPECT_EQ(bytes[1], 0xC1); + + reset_gx_state(); + decode_fifo(bytes); + + auto& s = g_gxState.tevStages[0]; + EXPECT_EQ(s.alphaPass.a, GX_CA_ZERO); + EXPECT_EQ(s.alphaPass.b, GX_CA_TEXA); + EXPECT_EQ(s.alphaPass.c, GX_CA_RASA); + EXPECT_EQ(s.alphaPass.d, GX_CA_ZERO); +} + +TEST_F(GXFifoTest, TevAlphaIn_Stage5) { + GXSetTevAlphaIn(GX_TEVSTAGE5, GX_CA_APREV, GX_CA_A0, GX_CA_KONST, GX_CA_ZERO); + auto bytes = capture_fifo(); + + // Stage 5 alpha register = 0xC1 + 5*2 = 0xCB + ASSERT_GE(bytes.size(), 5u); + EXPECT_EQ(bytes[0], 0x61); + EXPECT_EQ(bytes[1], 0xCB); + + reset_gx_state(); + decode_fifo(bytes); + + auto& s = g_gxState.tevStages[5]; + EXPECT_EQ(s.alphaPass.a, GX_CA_APREV); + EXPECT_EQ(s.alphaPass.b, GX_CA_A0); + EXPECT_EQ(s.alphaPass.c, GX_CA_KONST); + EXPECT_EQ(s.alphaPass.d, GX_CA_ZERO); +} + +TEST_F(GXFifoTest, TevColorIn_Stage7) { + GXSetTevColorIn(GX_TEVSTAGE7, GX_CC_C0, GX_CC_A0, GX_CC_KONST, GX_CC_CPREV); + auto bytes = capture_fifo(); + + // Stage 7 color register = 0xC0 + 7*2 = 0xCE + ASSERT_GE(bytes.size(), 5u); + EXPECT_EQ(bytes[0], 0x61); + EXPECT_EQ(bytes[1], 0xCE); + + reset_gx_state(); + decode_fifo(bytes); + + auto& s = g_gxState.tevStages[7]; + EXPECT_EQ(s.colorPass.a, GX_CC_C0); + EXPECT_EQ(s.colorPass.b, GX_CC_A0); + EXPECT_EQ(s.colorPass.c, GX_CC_KONST); + EXPECT_EQ(s.colorPass.d, GX_CC_CPREV); +} + +// --- GXSetTevOp (convenience wrapper over ColorIn/AlphaIn/ColorOp/AlphaOp) --- +// GXSetTevOp emits 4 BP writes: tevc (colorIn+colorOp) and teva (alphaIn+alphaOp). + +TEST_F(GXFifoTest, TevOp_Modulate_Stage0) { + GXSetTevOp(GX_TEVSTAGE0, GX_MODULATE); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& s = g_gxState.tevStages[0]; + // Modulate: color = ZERO, TEXC, RASC, ZERO (stage 0 uses RASC/RASA) + EXPECT_EQ(s.colorPass.a, GX_CC_ZERO); + EXPECT_EQ(s.colorPass.b, GX_CC_TEXC); + EXPECT_EQ(s.colorPass.c, GX_CC_RASC); + EXPECT_EQ(s.colorPass.d, GX_CC_ZERO); + // Modulate: alpha = ZERO, TEXA, RASA, ZERO + EXPECT_EQ(s.alphaPass.a, GX_CA_ZERO); + EXPECT_EQ(s.alphaPass.b, GX_CA_TEXA); + EXPECT_EQ(s.alphaPass.c, GX_CA_RASA); + EXPECT_EQ(s.alphaPass.d, GX_CA_ZERO); + // Op = ADD, bias = ZERO, scale = 1, clamp = true, outReg = TEVPREV + EXPECT_EQ(s.colorOp.op, GX_TEV_ADD); + EXPECT_EQ(s.colorOp.bias, GX_TB_ZERO); + EXPECT_EQ(s.colorOp.scale, GX_CS_SCALE_1); + EXPECT_TRUE(s.colorOp.clamp); + EXPECT_EQ(s.colorOp.outReg, GX_TEVPREV); + EXPECT_EQ(s.alphaOp.op, GX_TEV_ADD); + EXPECT_EQ(s.alphaOp.bias, GX_TB_ZERO); + EXPECT_EQ(s.alphaOp.scale, GX_CS_SCALE_1); + EXPECT_TRUE(s.alphaOp.clamp); + EXPECT_EQ(s.alphaOp.outReg, GX_TEVPREV); +} + +TEST_F(GXFifoTest, TevOp_Modulate_Stage1) { + // Non-stage-0 uses CPREV/APREV instead of RASC/RASA + GXSetTevOp(GX_TEVSTAGE1, GX_MODULATE); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& s = g_gxState.tevStages[1]; + EXPECT_EQ(s.colorPass.a, GX_CC_ZERO); + EXPECT_EQ(s.colorPass.b, GX_CC_TEXC); + EXPECT_EQ(s.colorPass.c, GX_CC_CPREV); + EXPECT_EQ(s.colorPass.d, GX_CC_ZERO); + EXPECT_EQ(s.alphaPass.a, GX_CA_ZERO); + EXPECT_EQ(s.alphaPass.b, GX_CA_TEXA); + EXPECT_EQ(s.alphaPass.c, GX_CA_APREV); + EXPECT_EQ(s.alphaPass.d, GX_CA_ZERO); +} + +TEST_F(GXFifoTest, TevOp_Replace) { + GXSetTevOp(GX_TEVSTAGE0, GX_REPLACE); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& s = g_gxState.tevStages[0]; + // Replace: color = ZERO, ZERO, ZERO, TEXC + EXPECT_EQ(s.colorPass.a, GX_CC_ZERO); + EXPECT_EQ(s.colorPass.b, GX_CC_ZERO); + EXPECT_EQ(s.colorPass.c, GX_CC_ZERO); + EXPECT_EQ(s.colorPass.d, GX_CC_TEXC); + // Replace: alpha = ZERO, ZERO, ZERO, TEXA + EXPECT_EQ(s.alphaPass.a, GX_CA_ZERO); + EXPECT_EQ(s.alphaPass.b, GX_CA_ZERO); + EXPECT_EQ(s.alphaPass.c, GX_CA_ZERO); + EXPECT_EQ(s.alphaPass.d, GX_CA_TEXA); +} + +TEST_F(GXFifoTest, TevOp_Decal) { + GXSetTevOp(GX_TEVSTAGE0, GX_DECAL); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& s = g_gxState.tevStages[0]; + // Decal: color = RASC, TEXC, TEXA, ZERO + EXPECT_EQ(s.colorPass.a, GX_CC_RASC); + EXPECT_EQ(s.colorPass.b, GX_CC_TEXC); + EXPECT_EQ(s.colorPass.c, GX_CC_TEXA); + EXPECT_EQ(s.colorPass.d, GX_CC_ZERO); + // Decal: alpha = ZERO, ZERO, ZERO, RASA + EXPECT_EQ(s.alphaPass.a, GX_CA_ZERO); + EXPECT_EQ(s.alphaPass.b, GX_CA_ZERO); + EXPECT_EQ(s.alphaPass.c, GX_CA_ZERO); + EXPECT_EQ(s.alphaPass.d, GX_CA_RASA); +} + +TEST_F(GXFifoTest, TevOp_Blend) { + GXSetTevOp(GX_TEVSTAGE0, GX_BLEND); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& s = g_gxState.tevStages[0]; + // Blend: color = RASC, ONE, TEXC, ZERO + EXPECT_EQ(s.colorPass.a, GX_CC_RASC); + EXPECT_EQ(s.colorPass.b, GX_CC_ONE); + EXPECT_EQ(s.colorPass.c, GX_CC_TEXC); + EXPECT_EQ(s.colorPass.d, GX_CC_ZERO); + // Blend: alpha = ZERO, TEXA, RASA, ZERO + EXPECT_EQ(s.alphaPass.a, GX_CA_ZERO); + EXPECT_EQ(s.alphaPass.b, GX_CA_TEXA); + EXPECT_EQ(s.alphaPass.c, GX_CA_RASA); + EXPECT_EQ(s.alphaPass.d, GX_CA_ZERO); +} + +TEST_F(GXFifoTest, TevOp_PassClr) { + GXSetTevOp(GX_TEVSTAGE0, GX_PASSCLR); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& s = g_gxState.tevStages[0]; + // PassClr: color = ZERO, ZERO, ZERO, RASC + EXPECT_EQ(s.colorPass.a, GX_CC_ZERO); + EXPECT_EQ(s.colorPass.b, GX_CC_ZERO); + EXPECT_EQ(s.colorPass.c, GX_CC_ZERO); + EXPECT_EQ(s.colorPass.d, GX_CC_RASC); + // PassClr: alpha = ZERO, ZERO, ZERO, RASA + EXPECT_EQ(s.alphaPass.a, GX_CA_ZERO); + EXPECT_EQ(s.alphaPass.b, GX_CA_ZERO); + EXPECT_EQ(s.alphaPass.c, GX_CA_ZERO); + EXPECT_EQ(s.alphaPass.d, GX_CA_RASA); +} + +// --- GXSetTevColorOp / GXSetTevAlphaOp --- + +TEST_F(GXFifoTest, TevColorOp_Sub_Scale2_Reg1) { + GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_SUB, GX_TB_ADDHALF, GX_CS_SCALE_2, GX_TRUE, GX_TEVREG1); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& s = g_gxState.tevStages[0]; + EXPECT_EQ(s.colorOp.op, GX_TEV_SUB); + EXPECT_EQ(s.colorOp.bias, GX_TB_ADDHALF); + EXPECT_EQ(s.colorOp.scale, GX_CS_SCALE_2); + EXPECT_TRUE(s.colorOp.clamp); + EXPECT_EQ(s.colorOp.outReg, GX_TEVREG1); +} + +TEST_F(GXFifoTest, TevAlphaOp_Add_NoClamp_Reg2) { + GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_SUBHALF, GX_CS_DIVIDE_2, GX_FALSE, GX_TEVREG2); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& s = g_gxState.tevStages[0]; + EXPECT_EQ(s.alphaOp.op, GX_TEV_ADD); + EXPECT_EQ(s.alphaOp.bias, GX_TB_SUBHALF); + EXPECT_EQ(s.alphaOp.scale, GX_CS_DIVIDE_2); + EXPECT_FALSE(s.alphaOp.clamp); + EXPECT_EQ(s.alphaOp.outReg, GX_TEVREG2); +} + +TEST_F(GXFifoTest, TevColorOp_CompareR8GT) { + // Compare ops (op > 1) use a different encoding: bias=3, scale encodes compare mode + GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_COMP_R8_GT, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& s = g_gxState.tevStages[0]; + EXPECT_EQ(s.colorOp.op, GX_TEV_COMP_R8_GT); + // Decoder normalizes compare mode: bias=ZERO, scale=SCALE_1 + EXPECT_EQ(s.colorOp.bias, GX_TB_ZERO); + EXPECT_EQ(s.colorOp.scale, GX_CS_SCALE_1); + EXPECT_EQ(s.colorOp.outReg, GX_TEVPREV); +} + +TEST_F(GXFifoTest, TevColorOp_CompareGR16EQ) { + GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_COMP_GR16_EQ, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVREG0); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& s = g_gxState.tevStages[0]; + EXPECT_EQ(s.colorOp.op, GX_TEV_COMP_GR16_EQ); + EXPECT_EQ(s.colorOp.outReg, GX_TEVREG0); +} + +TEST_F(GXFifoTest, TevAlphaOp_CompareRGB8GT) { + GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_COMP_RGB8_GT, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& s = g_gxState.tevStages[0]; + // GX_TEV_COMP_RGB8_GT is the same enum value for alpha as GX_TEV_COMP_A8_GT + EXPECT_EQ(s.alphaOp.op, GX_TEV_COMP_RGB8_GT); + EXPECT_EQ(s.alphaOp.bias, GX_TB_ZERO); + EXPECT_EQ(s.alphaOp.scale, GX_CS_SCALE_1); +} + +TEST_F(GXFifoTest, TevColorOp_CompareBGR24GT) { + GXSetTevColorOp(GX_TEVSTAGE2, GX_TEV_COMP_BGR24_GT, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVREG1); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& s = g_gxState.tevStages[2]; + EXPECT_EQ(s.colorOp.op, GX_TEV_COMP_BGR24_GT); + EXPECT_EQ(s.colorOp.bias, GX_TB_ZERO); + EXPECT_EQ(s.colorOp.scale, GX_CS_SCALE_1); + EXPECT_EQ(s.colorOp.outReg, GX_TEVREG1); +} + +TEST_F(GXFifoTest, TevColorOp_CompareRGB8EQ) { + GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_COMP_RGB8_EQ, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& s = g_gxState.tevStages[0]; + EXPECT_EQ(s.colorOp.op, GX_TEV_COMP_RGB8_EQ); + EXPECT_EQ(s.colorOp.outReg, GX_TEVPREV); +} + +TEST_F(GXFifoTest, TevAlphaOp_CompareA8EQ) { + GXSetTevAlphaOp(GX_TEVSTAGE1, GX_TEV_COMP_RGB8_EQ, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVREG2); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& s = g_gxState.tevStages[1]; + // For alpha, GX_TEV_COMP_RGB8_EQ maps to A8_EQ + EXPECT_EQ(s.alphaOp.op, GX_TEV_COMP_RGB8_EQ); + EXPECT_EQ(s.alphaOp.outReg, GX_TEVREG2); +} + +// --- GXSetTevColorS10 --- + +TEST_F(GXFifoTest, TevColorS10_Positive) { + GXColorS10 col = {511, 256, 100, 0}; + GXSetTevColorS10(GX_TEVREG0, col); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + // S10 values are encoded as 11-bit signed and decoded to float/255 + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][0], 511.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][1], 256.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][2], 100.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][3], 0.f / 255.f, 1.f / 255.f); +} + +TEST_F(GXFifoTest, TevColorS10_Negative) { + GXColorS10 col = {-128, -1, 0, 255}; + GXSetTevColorS10(GX_TEVPREV, col); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVPREV][0], -128.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVPREV][1], -1.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVPREV][2], 0.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVPREV][3], 255.f / 255.f, 1.f / 255.f); +} + +// --- GXSetTevKColorSel / GXSetTevKAlphaSel --- + +TEST_F(GXFifoTest, TevKColorSel_Stage0_K0) { + GXSetTevKColorSel(GX_TEVSTAGE0, GX_TEV_KCSEL_K0); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.tevStages[0].kcSel, GX_TEV_KCSEL_K0); +} + +TEST_F(GXFifoTest, TevKColorSel_Stage1_K2_R) { + GXSetTevKColorSel(GX_TEVSTAGE1, GX_TEV_KCSEL_K2_R); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.tevStages[1].kcSel, GX_TEV_KCSEL_K2_R); +} + +TEST_F(GXFifoTest, TevKAlphaSel_Stage0_K1_A) { + GXSetTevKAlphaSel(GX_TEVSTAGE0, GX_TEV_KASEL_K1_A); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.tevStages[0].kaSel, GX_TEV_KASEL_K1_A); +} + +TEST_F(GXFifoTest, TevKAlphaSel_Stage3_K3_B) { + GXSetTevKAlphaSel(GX_TEVSTAGE3, GX_TEV_KASEL_K3_B); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.tevStages[3].kaSel, GX_TEV_KASEL_K3_B); +} + +TEST_F(GXFifoTest, TevKColorSel_DoesNotCorruptSwapTable) { + // Regression: tevKsel shadow registers share bits with swap table entries. + // Setting K color selection must not zero out the swap table bits. + GXSetTevKColorSel(GX_TEVSTAGE0, GX_TEV_KCSEL_K0); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + // Swap table 0 should remain identity (initialized in GXInit) + EXPECT_EQ(g_gxState.tevSwapTable[0].red, GX_CH_RED); + EXPECT_EQ(g_gxState.tevSwapTable[0].green, GX_CH_GREEN); + EXPECT_EQ(g_gxState.tevSwapTable[0].blue, GX_CH_BLUE); + EXPECT_EQ(g_gxState.tevSwapTable[0].alpha, GX_CH_ALPHA); + // K color selection should still be set + EXPECT_EQ(g_gxState.tevStages[0].kcSel, GX_TEV_KCSEL_K0); +} + +// --- GXSetTevSwapMode --- + +TEST_F(GXFifoTest, TevSwapMode_Stage0) { + GXSetTevSwapMode(GX_TEVSTAGE0, GX_TEV_SWAP1, GX_TEV_SWAP2); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.tevStages[0].tevSwapRas, GX_TEV_SWAP1); + EXPECT_EQ(g_gxState.tevStages[0].tevSwapTex, GX_TEV_SWAP2); +} + +TEST_F(GXFifoTest, TevSwapMode_Stage3) { + GXSetTevSwapMode(GX_TEVSTAGE3, GX_TEV_SWAP3, GX_TEV_SWAP0); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.tevStages[3].tevSwapRas, GX_TEV_SWAP3); + EXPECT_EQ(g_gxState.tevStages[3].tevSwapTex, GX_TEV_SWAP0); +} + +// --- GXSetTevSwapModeTable --- + +TEST_F(GXFifoTest, TevSwapModeTable_Swap1_AllRed) { + GXSetTevSwapModeTable(GX_TEV_SWAP1, GX_CH_RED, GX_CH_RED, GX_CH_RED, GX_CH_ALPHA); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.tevSwapTable[GX_TEV_SWAP1].red, GX_CH_RED); + EXPECT_EQ(g_gxState.tevSwapTable[GX_TEV_SWAP1].green, GX_CH_RED); + EXPECT_EQ(g_gxState.tevSwapTable[GX_TEV_SWAP1].blue, GX_CH_RED); + EXPECT_EQ(g_gxState.tevSwapTable[GX_TEV_SWAP1].alpha, GX_CH_ALPHA); +} + +TEST_F(GXFifoTest, TevSwapModeTable_Swap2_Swizzle) { + GXSetTevSwapModeTable(GX_TEV_SWAP2, GX_CH_BLUE, GX_CH_GREEN, GX_CH_RED, GX_CH_ALPHA); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.tevSwapTable[GX_TEV_SWAP2].red, GX_CH_BLUE); + EXPECT_EQ(g_gxState.tevSwapTable[GX_TEV_SWAP2].green, GX_CH_GREEN); + EXPECT_EQ(g_gxState.tevSwapTable[GX_TEV_SWAP2].blue, GX_CH_RED); + EXPECT_EQ(g_gxState.tevSwapTable[GX_TEV_SWAP2].alpha, GX_CH_ALPHA); +} + +// --- GXSetTevOrder (BP 0x28-0x2F) --- + +TEST_F(GXFifoTest, TevOrder_Stage0) { + GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR0A0); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& s = g_gxState.tevStages[0]; + EXPECT_EQ(s.texMapId, GX_TEXMAP0); + EXPECT_EQ(s.texCoordId, GX_TEXCOORD0); + EXPECT_EQ(s.channelId, GX_COLOR0A0); +} + +TEST_F(GXFifoTest, TevOrder_Stage1_OddStage) { + // Odd stages use different bit positions within the tref register + GXSetTevOrder(GX_TEVSTAGE1, GX_TEXCOORD2, GX_TEXMAP3, GX_COLOR1A1); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& s = g_gxState.tevStages[1]; + EXPECT_EQ(s.texMapId, GX_TEXMAP3); + EXPECT_EQ(s.texCoordId, GX_TEXCOORD2); + EXPECT_EQ(s.channelId, GX_COLOR1A1); +} + +TEST_F(GXFifoTest, TevOrder_Stage0_TexNull) { + GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD_NULL, GX_TEXMAP_NULL, GX_COLOR0A0); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& s = g_gxState.tevStages[0]; + EXPECT_EQ(s.texMapId, GX_TEXMAP_NULL); + EXPECT_EQ(s.channelId, GX_COLOR0A0); +} + +// --- GXSetTevKColor (BP 0xE0-0xE7, K color flag) --- + +TEST_F(GXFifoTest, TevKColor_K0) { + GXColor kc = {255, 128, 64, 32}; + GXSetTevKColor(GX_KCOLOR0, kc); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + // K colors are stored as float (0-1 range), 8-bit precision + EXPECT_NEAR(g_gxState.kcolors[0][0], 255.f / 255.f, 1.f / 255.f); // R + EXPECT_NEAR(g_gxState.kcolors[0][1], 128.f / 255.f, 1.f / 255.f); // G + EXPECT_NEAR(g_gxState.kcolors[0][2], 64.f / 255.f, 1.f / 255.f); // B + EXPECT_NEAR(g_gxState.kcolors[0][3], 32.f / 255.f, 1.f / 255.f); // A +} + +TEST_F(GXFifoTest, TevKColor_K1) { + GXColor kc = {0, 255, 0, 128}; + GXSetTevKColor(GX_KCOLOR1, kc); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_NEAR(g_gxState.kcolors[1][0], 0.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.kcolors[1][1], 255.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.kcolors[1][2], 0.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.kcolors[1][3], 128.f / 255.f, 1.f / 255.f); +} + +TEST_F(GXFifoTest, TevKColor_K2) { + GXColor kc = {10, 20, 30, 40}; + GXSetTevKColor(GX_KCOLOR2, kc); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_NEAR(g_gxState.kcolors[2][0], 10.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.kcolors[2][1], 20.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.kcolors[2][2], 30.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.kcolors[2][3], 40.f / 255.f, 1.f / 255.f); +} + +TEST_F(GXFifoTest, TevKColor_K3) { + GXColor kc = {200, 150, 100, 50}; + GXSetTevKColor(GX_KCOLOR3, kc); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_NEAR(g_gxState.kcolors[3][0], 200.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.kcolors[3][1], 150.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.kcolors[3][2], 100.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.kcolors[3][3], 50.f / 255.f, 1.f / 255.f); +} + +// GXSetTevColor (BP 0xE0-0xE7): the side channel stores float while the FIFO encodes 11-bit +// signed, so the decoded value comes back with reduced precision. + +TEST_F(GXFifoTest, TevColor_Reg0) { + GXColor col = {200, 100, 50, 255}; + GXSetTevColor(GX_TEVREG0, col); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + // 11-bit signed encoding, so values should round-trip within 8-bit range + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][0], 200.f / 255.f, 1.f / 255.f); // R + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][1], 100.f / 255.f, 1.f / 255.f); // G + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][2], 50.f / 255.f, 1.f / 255.f); // B + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][3], 255.f / 255.f, 1.f / 255.f); // A +} + +TEST_F(GXFifoTest, TevColor_Prev) { + GXColor col = {128, 64, 32, 16}; + GXSetTevColor(GX_TEVPREV, col); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVPREV][0], 128.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVPREV][1], 64.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVPREV][2], 32.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVPREV][3], 16.f / 255.f, 1.f / 255.f); +} + +TEST_F(GXFifoTest, TevColor_Reg1) { + GXColor col = {0, 128, 255, 192}; + GXSetTevColor(GX_TEVREG1, col); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG1][0], 0.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG1][1], 128.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG1][2], 255.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG1][3], 192.f / 255.f, 1.f / 255.f); +} + +TEST_F(GXFifoTest, TevColor_Reg2) { + GXColor col = {1, 2, 3, 4}; + GXSetTevColor(GX_TEVREG2, col); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG2][0], 1.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG2][1], 2.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG2][2], 3.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG2][3], 4.f / 255.f, 1.f / 255.f); +} + +TEST_F(GXFifoTest, TevColorS10_Reg1) { + GXColorS10 col = {300, -50, 0, 255}; + GXSetTevColorS10(GX_TEVREG1, col); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG1][0], 300.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG1][1], -50.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG1][2], 0.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG1][3], 255.f / 255.f, 1.f / 255.f); +} + +TEST_F(GXFifoTest, TevColorS10_Reg2) { + GXColorS10 col = {-1024, 1023, 128, -256}; + GXSetTevColorS10(GX_TEVREG2, col); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG2][0], -1024.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG2][1], 1023.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG2][2], 128.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG2][3], -256.f / 255.f, 1.f / 255.f); +} + +// CP registers (require __GXSetDirtyState() flush) + +// --- GXClearVtxDesc --- + +TEST_F(GXFifoTest, ClearVtxDesc_ClearsAll) { + // Set every attribute to something non-default + GXSetVtxDesc(GX_VA_PNMTXIDX, GX_DIRECT); + GXSetVtxDesc(GX_VA_TEX0MTXIDX, GX_DIRECT); + GXSetVtxDesc(GX_VA_TEX1MTXIDX, GX_DIRECT); + GXSetVtxDesc(GX_VA_TEX2MTXIDX, GX_DIRECT); + GXSetVtxDesc(GX_VA_TEX3MTXIDX, GX_DIRECT); + GXSetVtxDesc(GX_VA_TEX4MTXIDX, GX_DIRECT); + GXSetVtxDesc(GX_VA_TEX5MTXIDX, GX_DIRECT); + GXSetVtxDesc(GX_VA_TEX6MTXIDX, GX_DIRECT); + GXSetVtxDesc(GX_VA_TEX7MTXIDX, GX_DIRECT); + GXSetVtxDesc(GX_VA_POS, GX_INDEX16); + GXSetVtxDesc(GX_VA_NRM, GX_INDEX8); + GXSetVtxDesc(GX_VA_CLR0, GX_DIRECT); + GXSetVtxDesc(GX_VA_CLR1, GX_DIRECT); + GXSetVtxDesc(GX_VA_TEX0, GX_INDEX16); + GXSetVtxDesc(GX_VA_TEX1, GX_INDEX8); + GXSetVtxDesc(GX_VA_TEX2, GX_DIRECT); + GXSetVtxDesc(GX_VA_TEX3, GX_DIRECT); + GXSetVtxDesc(GX_VA_TEX4, GX_INDEX16); + GXSetVtxDesc(GX_VA_TEX5, GX_INDEX8); + GXSetVtxDesc(GX_VA_TEX6, GX_DIRECT); + GXSetVtxDesc(GX_VA_TEX7, GX_DIRECT); + // Discard the dirty state from above + aurora::gx::fifo::clear_buffer(); + + // Now clear and flush + GXClearVtxDesc(); + auto bytes = flush_and_capture(); + + reset_gx_state(); + // Pre-fill g_gxState with non-zero to prove decode clears them + for (int i = 0; i < GX_VA_MAX_ATTR; ++i) { + g_gxState.vtxDesc[i] = GX_INDEX16; + } + decode_fifo(bytes); + + // After GXClearVtxDesc: POS = GX_DIRECT, everything else = GX_NONE + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_PNMTXIDX], GX_NONE); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX0MTXIDX], GX_NONE); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX1MTXIDX], GX_NONE); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX2MTXIDX], GX_NONE); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX3MTXIDX], GX_NONE); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX4MTXIDX], GX_NONE); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX5MTXIDX], GX_NONE); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX6MTXIDX], GX_NONE); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX7MTXIDX], GX_NONE); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_POS], GX_DIRECT); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_NRM], GX_NONE); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_CLR0], GX_NONE); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_CLR1], GX_NONE); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX0], GX_NONE); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX1], GX_NONE); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX2], GX_NONE); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX3], GX_NONE); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX4], GX_NONE); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX5], GX_NONE); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX6], GX_NONE); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX7], GX_NONE); +} + +// --- GXSetVtxDesc / GXClearVtxDesc --- + +TEST_F(GXFifoTest, VtxDesc_PosAndNrm_Direct) { + GXClearVtxDesc(); + GXSetVtxDesc(GX_VA_POS, GX_DIRECT); + GXSetVtxDesc(GX_VA_NRM, GX_DIRECT); + auto bytes = flush_and_capture(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_POS], GX_DIRECT); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_NRM], GX_DIRECT); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_CLR0], GX_NONE); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX0], GX_NONE); +} + +TEST_F(GXFifoTest, VtxDesc_Indexed) { + GXClearVtxDesc(); + GXSetVtxDesc(GX_VA_POS, GX_INDEX16); + GXSetVtxDesc(GX_VA_NRM, GX_INDEX16); + GXSetVtxDesc(GX_VA_CLR0, GX_DIRECT); + GXSetVtxDesc(GX_VA_TEX0, GX_INDEX8); + auto bytes = flush_and_capture(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_POS], GX_INDEX16); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_NRM], GX_INDEX16); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_CLR0], GX_DIRECT); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX0], GX_INDEX8); + EXPECT_EQ(g_gxState.sourceVtxDesc[GX_VA_POS], GX_INDEX16); + EXPECT_EQ(g_gxState.sourceVtxDesc[GX_VA_NRM], GX_INDEX16); + EXPECT_EQ(g_gxState.sourceVtxDesc[GX_VA_CLR0], GX_DIRECT); + EXPECT_EQ(g_gxState.sourceVtxDesc[GX_VA_TEX0], GX_INDEX8); +} + +TEST_F(GXFifoTest, SourceVtxDesc_DoesNotChangeEmittedLayout) { + GXClearVtxDesc(); + GXSetVtxDesc(GX_VA_POS, GX_DIRECT); + auto bytes = flush_and_capture(); + + reset_gx_state(); + decode_fifo(bytes); + GXSetSourceVtxDesc(GX_VA_POS, GX_INDEX16); + + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_POS], GX_DIRECT); + EXPECT_EQ(g_gxState.sourceVtxDesc[GX_VA_POS], GX_INDEX16); +} + +TEST_F(GXFifoTest, VtxDesc_MtxIdx) { + GXClearVtxDesc(); + GXSetVtxDesc(GX_VA_PNMTXIDX, GX_DIRECT); + GXSetVtxDesc(GX_VA_TEX0MTXIDX, GX_DIRECT); + GXSetVtxDesc(GX_VA_POS, GX_DIRECT); + auto bytes = flush_and_capture(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_PNMTXIDX], GX_DIRECT); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX0MTXIDX], GX_DIRECT); + EXPECT_EQ(g_gxState.vtxDesc[GX_VA_POS], GX_DIRECT); +} + +TEST_F(GXFifoTest, GetVtxDesc_UsesShadowState) { + GXClearVtxDesc(); + GXSetVtxDesc(GX_VA_POS, GX_INDEX16); + GXSetVtxDesc(GX_VA_NBT, GX_DIRECT); + + GXAttrType posType = GX_NONE; + GXAttrType nbtType = GX_NONE; + GXVtxDescList vcd[24]{}; + GXGetVtxDesc(GX_VA_POS, &posType); + GXGetVtxDesc(GX_VA_NBT, &nbtType); + GXGetVtxDescv(vcd); + + EXPECT_EQ(posType, GX_INDEX16); + EXPECT_EQ(nbtType, GX_DIRECT); + EXPECT_EQ(vcd[GX_VA_POS].attr, GX_VA_POS); + EXPECT_EQ(vcd[GX_VA_POS].type, GX_INDEX16); + EXPECT_EQ(vcd[GX_VA_TEX7 + 1].attr, GX_VA_NBT); + EXPECT_EQ(vcd[GX_VA_TEX7 + 1].type, GX_DIRECT); + EXPECT_EQ(vcd[GX_VA_TEX7 + 2].attr, GX_VA_NULL); +} + +// --- GXSetVtxAttrFmt --- + +TEST_F(GXFifoTest, VtxAttrFmt_PosF32) { + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); + auto bytes = flush_and_capture(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& vf = g_gxState.vtxFmts[GX_VTXFMT0]; + EXPECT_EQ(vf.attrs[GX_VA_POS].cnt, GX_POS_XYZ); + EXPECT_EQ(vf.attrs[GX_VA_POS].type, GX_F32); + EXPECT_EQ(vf.attrs[GX_VA_POS].frac, 0); +} + +TEST_F(GXFifoTest, VtxAttrFmt_NrmS16) { + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_NRM, GX_NRM_XYZ, GX_S16, 0); + auto bytes = flush_and_capture(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& vf = g_gxState.vtxFmts[GX_VTXFMT0]; + EXPECT_EQ(vf.attrs[GX_VA_NRM].cnt, GX_NRM_XYZ); + EXPECT_EQ(vf.attrs[GX_VA_NRM].type, GX_S16); + EXPECT_EQ(vf.attrs[GX_VA_NRM].frac, 14); +} + +TEST_F(GXFifoTest, VtxAttrFmt_NrmU8UsesUnsignedScale) { + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_NRM, GX_NRM_XYZ, GX_S16, 0); + (void)flush_and_capture(); + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_NRM, GX_NRM_XYZ, GX_U8, 0); + auto bytes = flush_and_capture(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& vf = g_gxState.vtxFmts[GX_VTXFMT0]; + EXPECT_EQ(vf.attrs[GX_VA_NRM].cnt, GX_NRM_XYZ); + EXPECT_EQ(vf.attrs[GX_VA_NRM].type, GX_U8); + EXPECT_EQ(vf.attrs[GX_VA_NRM].frac, 7); +} + +TEST_F(GXFifoTest, NormalU8DirectPreservesUnsignedRawBytes) { + GXClearVtxDesc(); + GXSetVtxDesc(GX_VA_NRM, GX_DIRECT); + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_NRM, GX_NRM_XYZ, GX_U8, 0); + aurora::gx::fifo::clear_buffer(); + + GXNormal3u8(0xFF, 0x80, 0x01); + const auto bytes = capture_fifo(); + + ASSERT_EQ(bytes.size(), 3u); + EXPECT_EQ(bytes[0], 0xFF); + EXPECT_EQ(bytes[1], 0x80); + EXPECT_EQ(bytes[2], 0x01); +} + +TEST_F(GXFifoTest, VtxAttrFmt_NrmNBT3PreservesIndex3Bit) { + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_NRM, GX_NRM_NBT3, GX_S16, 0); + auto bytes = flush_and_capture(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& vf = g_gxState.vtxFmts[GX_VTXFMT0]; + EXPECT_EQ(vf.attrs[GX_VA_NRM].cnt, GX_NRM_NBT3); + EXPECT_EQ(vf.attrs[GX_VA_NRM].type, GX_S16); +} + +TEST_F(GXFifoTest, VtxAttrFmt_Tex0_S16_Frac8) { + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_TEX0, GX_TEX_ST, GX_S16, 8); + auto bytes = flush_and_capture(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& vf = g_gxState.vtxFmts[GX_VTXFMT0]; + EXPECT_EQ(vf.attrs[GX_VA_TEX0].cnt, GX_TEX_ST); + EXPECT_EQ(vf.attrs[GX_VA_TEX0].type, GX_S16); + EXPECT_EQ(vf.attrs[GX_VA_TEX0].frac, 8); +} + +TEST_F(GXFifoTest, VtxAttrFmt_Clr0_RGBA8) { + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_CLR0, GX_CLR_RGBA, GX_RGBA8, 0); + auto bytes = flush_and_capture(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& vf = g_gxState.vtxFmts[GX_VTXFMT0]; + EXPECT_EQ(vf.attrs[GX_VA_CLR0].cnt, GX_CLR_RGBA); + EXPECT_EQ(vf.attrs[GX_VA_CLR0].type, GX_RGBA8); +} + +TEST_F(GXFifoTest, VtxAttrFmt_MultipleTexCoords) { + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_TEX0, GX_TEX_ST, GX_F32, 0); + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_TEX1, GX_TEX_ST, GX_U16, 15); + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_TEX2, GX_TEX_ST, GX_S16, 8); + auto bytes = flush_and_capture(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& vf = g_gxState.vtxFmts[GX_VTXFMT1]; + EXPECT_EQ(vf.attrs[GX_VA_TEX0].type, GX_F32); + EXPECT_EQ(vf.attrs[GX_VA_TEX1].type, GX_U16); + EXPECT_EQ(vf.attrs[GX_VA_TEX1].frac, 15); + EXPECT_EQ(vf.attrs[GX_VA_TEX2].type, GX_S16); + EXPECT_EQ(vf.attrs[GX_VA_TEX2].frac, 8); +} + +TEST_F(GXFifoTest, GetVtxAttrFmt_UsesShadowState) { + GXSetVtxAttrFmt(GX_VTXFMT2, GX_VA_NRM, GX_NRM_NBT3, GX_S16, 0); + GXSetVtxAttrFmt(GX_VTXFMT2, GX_VA_TEX4, GX_TEX_ST, GX_U16, 11); + + GXCompCnt cnt = GX_POS_XY; + GXCompType type = GX_U8; + u8 frac = 0; + GXVtxAttrFmtList vat[13]{}; + + GXGetVtxAttrFmt(GX_VTXFMT2, GX_VA_NRM, &cnt, &type, &frac); + EXPECT_EQ(cnt, GX_NRM_NBT3); + EXPECT_EQ(type, GX_S16); + EXPECT_EQ(frac, 14); + + GXGetVtxAttrFmtv(GX_VTXFMT2, vat); + EXPECT_EQ(vat[GX_VA_NRM - GX_VA_POS].attr, GX_VA_NRM); + EXPECT_EQ(vat[GX_VA_NRM - GX_VA_POS].cnt, GX_NRM_NBT3); + EXPECT_EQ(vat[GX_VA_NRM - GX_VA_POS].type, GX_S16); + EXPECT_EQ(vat[GX_VA_TEX4 - GX_VA_POS].attr, GX_VA_TEX4); + EXPECT_EQ(vat[GX_VA_TEX4 - GX_VA_POS].type, GX_U16); + EXPECT_EQ(vat[GX_VA_TEX4 - GX_VA_POS].frac, 11); + EXPECT_EQ(vat[12].attr, GX_VA_NULL); +} + +// --- GXSetArray (Aurora array-base command + CP stride command) --- + +TEST_F(GXFifoTest, SetArray_Pos_EncodesAuroraArrayBaseAndStride) { + u8 posData[32]{}; + u8 oldData[8]{}; + + GXSetArray(GX_VA_POS, posData, sizeof(posData), 12, false); + auto bytes = capture_fifo(); + + ASSERT_EQ(bytes.size(), 22u); + EXPECT_EQ(bytes[0], GX_LOAD_AURORA); + EXPECT_EQ(bytes[1], 0x00); + EXPECT_EQ(bytes[2], GX_LOAD_AURORA_ARRAYBASE); + + const auto expect_be64 = [&](size_t offset, u64 value) { + for (size_t i = 0; i < 8; ++i) { + EXPECT_EQ(bytes[offset + i], static_cast((value >> (56 - i * 8)) & 0xFF)); + } + }; + const auto expect_be32 = [&](size_t offset, u32 value) { + for (size_t i = 0; i < 4; ++i) { + EXPECT_EQ(bytes[offset + i], static_cast((value >> (24 - i * 8)) & 0xFF)); + } + }; + + expect_be64(3, static_cast(reinterpret_cast(posData))); + expect_be32(11, sizeof(posData)); + EXPECT_EQ(bytes[15], 0); + EXPECT_EQ(bytes[16], GX_LOAD_CP_REG); + EXPECT_EQ(bytes[17], GX_CP_REG_ARRAYSTRIDE); + expect_be32(18, 12); + + reset_gx_state(); + gxState().arrays[GX_VA_POS].data = oldData; + gxState().arrays[GX_VA_POS].size = sizeof(oldData); + gxState().arrays[GX_VA_POS].stride = 2; + gxState().arrays[GX_VA_POS].cachedRange.offset = 4; + gxState().arrays[GX_VA_POS].cachedRange.size = 8; + gxState().stateDirty = false; + decode_fifo(bytes); + + EXPECT_EQ(gxState().arrays[GX_VA_POS].data, posData); + EXPECT_EQ(gxState().arrays[GX_VA_POS].size, sizeof(posData)); + EXPECT_EQ(gxState().arrays[GX_VA_POS].stride, 12); + EXPECT_FALSE(gxState().arrays[GX_VA_POS].le); + EXPECT_EQ(gxState().arrays[GX_VA_POS].cachedRange.offset, 0u); + EXPECT_EQ(gxState().arrays[GX_VA_POS].cachedRange.size, 0u); + EXPECT_TRUE(gxState().stateDirty); +} + +TEST_F(GXFifoTest, SetArray_Nbt_UsesNrmCommandSlotAndState) { + u8 nbtData[96]{}; + u8 untouchedData[24]{}; + + GXSetArray(GX_VA_NBT, nbtData, sizeof(nbtData), 36, false); + auto bytes = capture_fifo(); + + ASSERT_EQ(bytes.size(), 22u); + EXPECT_EQ(bytes[0], GX_LOAD_AURORA); + EXPECT_EQ(bytes[1], 0x00); + EXPECT_EQ(bytes[2], GX_LOAD_AURORA_ARRAYBASE | 0x01); + EXPECT_EQ(bytes[15], 0); + EXPECT_EQ(bytes[16], GX_LOAD_CP_REG); + EXPECT_EQ(bytes[17], GX_CP_REG_ARRAYSTRIDE | 0x01); + + reset_gx_state(); + gxState().arrays[GX_VA_NRM].cachedRange.offset = 12; + gxState().arrays[GX_VA_NRM].cachedRange.size = 48; + gxState().arrays[GX_VA_NBT].data = untouchedData; + gxState().arrays[GX_VA_NBT].size = sizeof(untouchedData); + gxState().arrays[GX_VA_NBT].stride = 24; + gxState().stateDirty = false; + decode_fifo(bytes); + + EXPECT_EQ(gxState().arrays[GX_VA_NRM].data, nbtData); + EXPECT_EQ(gxState().arrays[GX_VA_NRM].size, sizeof(nbtData)); + EXPECT_EQ(gxState().arrays[GX_VA_NRM].stride, 36); + EXPECT_FALSE(gxState().arrays[GX_VA_NRM].le); + EXPECT_EQ(gxState().arrays[GX_VA_NRM].cachedRange.offset, 0u); + EXPECT_EQ(gxState().arrays[GX_VA_NRM].cachedRange.size, 0u); + EXPECT_TRUE(gxState().stateDirty); + + EXPECT_EQ(gxState().arrays[GX_VA_NBT].data, untouchedData); + EXPECT_EQ(gxState().arrays[GX_VA_NBT].size, sizeof(untouchedData)); + EXPECT_EQ(gxState().arrays[GX_VA_NBT].stride, 24); +} + +TEST_F(GXFifoTest, SetArray_LittleEndianFlag_UpdatesStateAndClearsCachedRange) { + u8 clrData[16]{}; + + GXSetArray(GX_VA_CLR0, clrData, sizeof(clrData), 4, true); + auto bytes = capture_fifo(); + + ASSERT_EQ(bytes.size(), 22u); + EXPECT_EQ(bytes[0], GX_LOAD_AURORA); + EXPECT_EQ(bytes[1], 0x00); + EXPECT_EQ(bytes[2], GX_LOAD_AURORA_ARRAYBASE | (GX_VA_CLR0 - GX_VA_POS)); + EXPECT_EQ(bytes[15], 1); + EXPECT_EQ(bytes[16], GX_LOAD_CP_REG); + EXPECT_EQ(bytes[17], GX_CP_REG_ARRAYSTRIDE | (GX_VA_CLR0 - GX_VA_POS)); + + reset_gx_state(); + gxState().arrays[GX_VA_CLR0].data = clrData; + gxState().arrays[GX_VA_CLR0].size = sizeof(clrData); + gxState().arrays[GX_VA_CLR0].stride = 4; + gxState().arrays[GX_VA_CLR0].le = false; + gxState().arrays[GX_VA_CLR0].cachedRange.offset = 3; + gxState().arrays[GX_VA_CLR0].cachedRange.size = 9; + gxState().stateDirty = false; + decode_fifo(bytes); + + EXPECT_EQ(gxState().arrays[GX_VA_CLR0].data, clrData); + EXPECT_EQ(gxState().arrays[GX_VA_CLR0].size, sizeof(clrData)); + EXPECT_EQ(gxState().arrays[GX_VA_CLR0].stride, 4); + EXPECT_TRUE(gxState().arrays[GX_VA_CLR0].le); + EXPECT_EQ(gxState().arrays[GX_VA_CLR0].cachedRange.offset, 0u); + EXPECT_EQ(gxState().arrays[GX_VA_CLR0].cachedRange.size, 0u); + EXPECT_TRUE(gxState().stateDirty); +} + +TEST_F(GXFifoTest, InvalidateVtxCache_DropsAllIndexedArrayUploads) { + for (int i = GX_VA_POS; i <= GX_VA_TEX7; ++i) { + gxState().arrays[i].cachedRange.offset = static_cast(i * 32); + gxState().arrays[i].cachedRange.size = static_cast(i + 1); + } + + const std::vector command{GX_CMD_INVL_VC}; + decode_fifo(command); + + for (int i = GX_VA_POS; i <= GX_VA_TEX7; ++i) { + EXPECT_EQ(gxState().arrays[i].cachedRange.offset, 0u); + EXPECT_EQ(gxState().arrays[i].cachedRange.size, 0u); + } +} + +TEST_F(GXFifoTest, LoadTexObj_EncodesSdkBpBurstAndAuroraMetadata) { + alignas(32) u8 image[64]{}; + GXTexObj obj{}; + GXInitTexObj(&obj, image, 8, 8, GX_TF_RGB5A3, GX_REPEAT, GX_MIRROR, GX_FALSE); + + GXLoadTexObj(&obj, GX_TEXMAP2); + auto bytes = capture_fifo(); + + EXPECT_TRUE(has_bp_write(bytes, 0x82)); + EXPECT_TRUE(has_bp_write(bytes, 0x86)); + EXPECT_TRUE(has_bp_write(bytes, 0x8A)); + EXPECT_TRUE(has_bp_write(bytes, 0x8E)); + EXPECT_TRUE(has_bp_write(bytes, 0x92)); + EXPECT_TRUE(has_bp_write(bytes, 0x96)); + EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_TEXOBJ)); + + reset_gx_state(); + decode_fifo(bytes); + + const auto& slot = gxState().loadedTextures[GX_TEXMAP2]; + EXPECT_EQ(slot.data, image); + EXPECT_EQ(slot.width(), 8u); + EXPECT_EQ(slot.height(), 8u); + EXPECT_EQ(slot.format(), GX_TF_RGB5A3); + EXPECT_FALSE(slot.has_mips()); + EXPECT_EQ(slot.mode0 >> 24, 0x82u); + EXPECT_EQ(slot.mode1 >> 24, 0x86u); + EXPECT_EQ(slot.image0 >> 24, 0x8Au); + EXPECT_EQ(slot.image3 >> 24, 0x96u); + EXPECT_NE(slot.texObjId, 0u); + EXPECT_EQ(slot.texDataVersion, 1u); +} + +TEST(GXTextureMipCount, ClampsLodToTheDimensionsFullMipChain) { + alignas(32) u8 image[64]{}; + + GXTexObj small{}; + GXInitTexObj(&small, image, 8, 8, GX_TF_I4, GX_CLAMP, GX_CLAMP, GX_TRUE); + GXInitTexObjMaxLOD(&small, 10.0f); + const auto& smallRef = reinterpret_cast(small); + EXPECT_EQ(smallRef.mip_count(), 4u); + + GXTexObj large{}; + GXInitTexObj(&large, image, 1024, 1024, GX_TF_I4, GX_CLAMP, GX_CLAMP, GX_TRUE); + GXInitTexObjMaxLOD(&large, 10.0f); + const auto& largeRef = reinterpret_cast(large); + EXPECT_EQ(largeRef.mip_count(), 11u); +} + +TEST_F(GXFifoTest, LoadTexObjPcFormat_PreservesFullFormatMetadata) { + alignas(32) u8 image[64]{}; + GXTexObj obj{}; + GXInitTexObj(&obj, image, 8, 8, GX_TF_RGBA8_PC, GX_REPEAT, GX_REPEAT, GX_FALSE); + + EXPECT_EQ(GXGetTexObjFmt(&obj), GX_TF_RGBA8_PC); + + GXLoadTexObj(&obj, GX_TEXMAP3); + auto bytes = capture_fifo(); + + EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_TEXOBJ)); + + reset_gx_state(); + decode_fifo(bytes); + + const auto& slot = gxState().loadedTextures[GX_TEXMAP3]; + EXPECT_EQ(slot.width(), 8u); + EXPECT_EQ(slot.height(), 8u); + EXPECT_EQ(slot.format(), GX_TF_RGBA8_PC); + EXPECT_EQ(slot.raw_format(), static_cast(GX_TF_RGBA8)); +} + +TEST_F(GXFifoTest, RawDrawDrainsQueuedMaterialStateWithoutDeferredDirtyBits) { + __GXSetDirtyState(); + aurora::gx::fifo::clear_buffer(); + + constexpr GXColor color = {200, 100, 50, 255}; + GXSetTevColor(GX_TEVREG0, color); + + // TEV register writes are complete FIFO commands, not deferred SDK state. + // They therefore leave dirtyState clear while still preceding the draw. + ASSERT_EQ(__gx->dirtyState, 0u); + ASSERT_GT(aurora::gx::fifo::get_buffer_size(), 0u); + + // The raw bridge receives vertices that are already packed according to the + // active VAT. Seed its size cache so this test can isolate FIFO ordering. + g_gxState.lastVtxFmt = GX_VTXFMT0; + g_gxState.lastVtxSize = 1; + const std::array vertices{}; + + ASSERT_TRUE(aurora::gx::fifo::submit_raw_draw(GX_QUADS, GX_VTXFMT0, vertices.data(), 4, + static_cast(vertices.size()))); + EXPECT_EQ(aurora::gx::fifo::get_buffer_size(), 0u); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][0], 200.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][1], 100.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][2], 50.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][3], 255.f / 255.f, 1.f / 255.f); +} + +TEST_F(GXFifoTest, DisplayListCallWhileRecordingInlinesNestedCommands) { + std::array outer{}; + const std::array nested{GX_NOP, GX_NOP, GX_NOP}; + + __GXSetDirtyState(); + aurora::gx::fifo::clear_buffer(); + GXBeginDisplayList(outer.data(), static_cast(outer.size())); + GXCallDisplayList(nested.data(), static_cast(nested.size())); + const u32 bytes = GXEndDisplayList(); + + EXPECT_EQ(bytes, 32u); + EXPECT_TRUE(std::equal(nested.begin(), nested.end(), outer.begin())); + EXPECT_EQ(aurora::gx::fifo::get_buffer_size(), 0u); +} + +TEST_F(GXFifoTest, DirectEfbCopiesDrainQueuedCommands) { + aurora::gx::fifo::write_u8(GX_NOP); + ASSERT_GT(aurora::gx::fifo::get_buffer_size(), 0u); + GXCopyDisp(nullptr, GX_FALSE); + EXPECT_EQ(aurora::gx::fifo::get_buffer_size(), 0u); + + std::array copyDest{}; + aurora::gx::fifo::write_u8(GX_NOP); + ASSERT_GT(aurora::gx::fifo::get_buffer_size(), 0u); + GXCopyTex(copyDest.data(), GX_FALSE); + EXPECT_EQ(aurora::gx::fifo::get_buffer_size(), 0u); +} + +TEST_F(GXFifoTest, RawDrawPreservesHorizontalXzQuadVertices) { + __GXSetDirtyState(); + aurora::gx::fifo::clear_buffer(); + aurora::gfx::testing::use_real_vertex_format_helpers(true); + + g_gxState.lastVtxFmt = GX_VTXFMT0; + g_gxState.lastVtxSize = 14; + g_gxState.vtxDesc[GX_VA_POS] = GX_DIRECT; + g_gxState.vtxDesc[GX_VA_TEX0] = GX_DIRECT; + auto& fmt = g_gxState.vtxFmts[GX_VTXFMT0]; + fmt.attrs[GX_VA_POS].cnt = GX_POS_XYZ; + fmt.attrs[GX_VA_POS].type = GX_F32; + fmt.attrs[GX_VA_TEX0].cnt = GX_TEX_ST; + fmt.attrs[GX_VA_TEX0].type = GX_U8; + + std::vector vertices; + const auto append_f32_be = [&](float value) { + u32 bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + vertices.push_back(static_cast(bits >> 24)); + vertices.push_back(static_cast(bits >> 16)); + vertices.push_back(static_cast(bits >> 8)); + vertices.push_back(static_cast(bits)); + }; + const auto append_vertex = [&](float x, float y, float z, u8 s, u8 t) { + append_f32_be(x); + append_f32_be(y); + append_f32_be(z); + vertices.push_back(s); + vertices.push_back(t); + }; + + append_vertex(-53.659431f, 0.27f, -53.659435f, 0, 1); + append_vertex(-53.659431f, 0.27f, 53.659435f, 0, 0); + append_vertex(53.659431f, 0.27f, 53.659435f, 1, 0); + append_vertex(53.659431f, 0.27f, -53.659435f, 1, 1); + const auto expected = vertices; + + ASSERT_TRUE(aurora::gx::fifo::submit_raw_draw( + GX_QUADS, GX_VTXFMT0, vertices.data(), 4, static_cast(vertices.size()))); + EXPECT_EQ(aurora::gfx::testing::last_pushed_vertices(), expected); +} + +static void append_test_draw(std::vector& fifo, GXPrimitive primitive, u16 count) { + fifo.push_back(static_cast(primitive) | static_cast(GX_VTXFMT0)); + fifo.push_back(static_cast(count >> 8)); + fifo.push_back(static_cast(count)); + for (u16 vertex = 0; vertex < count; ++vertex) { + fifo.push_back(static_cast(vertex)); + } +} + +TEST_F(GXFifoTest, DrawTopologyTemplatesPreserveExactGxIndexOrder) { + g_gxState.lastVtxFmt = GX_VTXFMT0; + g_gxState.lastVtxSize = 1; + g_gxState.stateDirty = true; + + const auto decodeAndReadIndices = [&](GXPrimitive primitive, u16 count) { + std::vector fifo; + append_test_draw(fifo, primitive, count); + decode_fifo(fifo); + return aurora::gfx::testing::last_pushed_indices(); + }; + + EXPECT_EQ(decodeAndReadIndices(GX_QUADS, 8), + (std::vector{0, 1, 2, 2, 3, 0, 4, 5, 6, 6, 7, 4})); + g_gxState.stateDirty = true; + EXPECT_EQ(decodeAndReadIndices(GX_TRIANGLES, 6), + (std::vector{0, 1, 2, 3, 4, 5})); + g_gxState.stateDirty = true; + EXPECT_EQ(decodeAndReadIndices(GX_TRIANGLEFAN, 5), + (std::vector{0, 1, 2, 0, 2, 3, 0, 3, 4})); + g_gxState.stateDirty = true; + EXPECT_EQ(decodeAndReadIndices(GX_TRIANGLEFAN, 2), + (std::vector{0, 1})); + g_gxState.stateDirty = true; + EXPECT_EQ(decodeAndReadIndices(GX_TRIANGLESTRIP, 6), + (std::vector{0, 1, 2, 2, 1, 3, 2, 3, 4, 4, 3, 5})); + g_gxState.stateDirty = true; + EXPECT_TRUE(decodeAndReadIndices(GX_TRIANGLESTRIP, 0).empty()); + g_gxState.stateDirty = true; + EXPECT_EQ(decodeAndReadIndices(GX_LINES, 2), + (std::vector{0, 1, 3, 3, 2, 0})); +} + +TEST_F(GXFifoTest, MergedDrawOffsetsCachedTopologyWithoutJoiningPrimitives) { + g_gxState.lastVtxFmt = GX_VTXFMT0; + g_gxState.lastVtxSize = 1; + g_gxState.stateDirty = true; + aurora::gfx::testing::use_draw_command_tracking(true); + + std::vector fifo; + append_test_draw(fifo, GX_TRIANGLES, 3); + append_test_draw(fifo, GX_TRIANGLES, 3); + decode_fifo(fifo); + + EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 1u); + EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), + (std::vector{3, 4, 5})); +} + +TEST_F(GXFifoTest, TexBufferSize_UsesExactLinearPcFormatSizes) { + EXPECT_EQ(GXGetTexBufferSize(8, 4, GX_TF_R8_PC, GX_FALSE, 0), 32u); + EXPECT_EQ(GXGetTexBufferSize(8, 4, GX_TF_RGBA8_PC, GX_FALSE, 0), 128u); + EXPECT_EQ(GXGetTexBufferSize(8, 4, GX_TF_R8_PC, GX_TRUE, 2), 42u); + EXPECT_EQ(GXGetTexBufferSize(8, 4, GX_TF_RGBA8_PC, GX_TRUE, 2), 168u); +} + +TEST_F(GXFifoTest, InvalidateTexAll_EmitsAuroraCacheInvalidationAndDirtiesState) { + GXInvalidateTexAll(); + auto bytes = capture_fifo(); + + EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_INVALIDATE_TEX_ALL)); + + reset_gx_state(); + gxState().stateDirty = false; + decode_fifo(bytes); + EXPECT_TRUE(gxState().stateDirty); +} + +TEST_F(GXFifoTest, TexObjRawDimensions_WrapAtTenBitBoundary) { + auto& slot = gxState().loadedTextures[GX_TEXMAP0]; + slot.image0 = (0x3FFu << 0) | (0x3FFu << 10); + slot.mWidth = 0; + slot.mHeight = 0; + + EXPECT_EQ(slot.width(), 0u); + EXPECT_EQ(slot.height(), 0u); +} + +TEST_F(GXFifoTest, TexObjExplicitDimensions_DoNotWrapAtTenBitBoundary) { + auto& slot = gxState().loadedTextures[GX_TEXMAP0]; + slot.image0 = (0x3FFu << 0) | (0x3FFu << 10); + slot.mWidth = 1024; + slot.mHeight = 1024; + + EXPECT_EQ(slot.width(), 1024u); + EXPECT_EQ(slot.height(), 1024u); +} + +TEST_F(GXFifoTest, LoadTexObjCiAndTlut_PopulatesTextureAndTlutSlots) { + alignas(32) u8 image[64]{}; + alignas(32) u16 palette[16]{}; + GXTexObj texObj{}; + GXTlutObj tlutObj{}; + + GXInitTexObjCI(&texObj, image, 8, 8, GX_TF_C4, GX_CLAMP, GX_CLAMP, GX_FALSE, GX_TLUT3); + GXInitTlutObj(&tlutObj, palette, GX_TL_RGB565, 16); + + GXLoadTexObj(&texObj, GX_TEXMAP1); + GXLoadTlut(&tlutObj, GX_TLUT3); + auto bytes = capture_fifo(); + + EXPECT_TRUE(has_bp_write(bytes, 0x81)); + EXPECT_TRUE(has_bp_write(bytes, 0x85)); + EXPECT_TRUE(has_bp_write(bytes, 0x89)); + EXPECT_TRUE(has_bp_write(bytes, 0x8D)); + EXPECT_TRUE(has_bp_write(bytes, 0x91)); + EXPECT_TRUE(has_bp_write(bytes, 0x95)); + EXPECT_TRUE(has_bp_write(bytes, 0x99)); + EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_TEXOBJ)); + EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_TLUT)); + + reset_gx_state(); + decode_fifo(bytes); + + const auto& texSlot = gxState().loadedTextures[GX_TEXMAP1]; + EXPECT_EQ(texSlot.data, image); + EXPECT_EQ(texSlot.width(), 8u); + EXPECT_EQ(texSlot.height(), 8u); + EXPECT_EQ(texSlot.format(), GX_TF_C4); + EXPECT_EQ(texSlot.tlut, GX_TLUT3); + + const auto& tlutSlot = gxState().loadedTluts[GX_TLUT3]; + EXPECT_EQ(tlutSlot.data, palette); + EXPECT_EQ(tlutSlot.format, GX_TL_RGB565); + EXPECT_EQ(tlutSlot.numEntries, 16u); + EXPECT_NE(tlutSlot.tlutObjId, 0u); + EXPECT_EQ(tlutSlot.tlutDataVersion, 1u); +} + +TEST_F(GXFifoTest, DestroyTexObj_DoesNotEvictTextureDataAndClearsIdentity) { + alignas(32) u8 image[64]{}; + GXTexObj obj{}; + GXInitTexObj(&obj, image, 8, 8, GX_TF_RGB5A3, GX_REPEAT, GX_REPEAT, GX_FALSE); + GXDestroyTexObj(&obj); + auto bytes = capture_fifo(); + + EXPECT_FALSE(has_aurora_cmd(bytes, GX_LOAD_AURORA_DESTROY_TEXOBJ)); + EXPECT_EQ(reinterpret_cast(&obj)->texObjId, 0u); + + reset_gx_state(); + decode_fifo(bytes); +} + +TEST_F(GXFifoTest, DestroyTexObj_DoesNotPoisonLoadedSlotCache) { + alignas(32) u8 imageA[64]{}; + alignas(32) u8 imageB[64]{}; + GXTexObj objA{}; + GXTexObj objB{}; + + GXInitTexObj(&objA, imageA, 8, 8, GX_TF_RGB5A3, GX_REPEAT, GX_REPEAT, GX_FALSE); + GXLoadTexObj(&objA, GX_TEXMAP2); + auto loadABytes = capture_fifo(); + const auto destroyedTexObjId = reinterpret_cast(&objA)->texObjId; + + GXDestroyTexObj(&objA); + auto destroyBytes = capture_fifo(); + + GXInitTexObj(&objB, imageB, 8, 8, GX_TF_RGB565, GX_CLAMP, GX_CLAMP, GX_FALSE); + GXLoadTexObj(&objB, GX_TEXMAP2); + auto loadBBytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(loadABytes); + auto& slot = gxState().loadedTextures[GX_TEXMAP2]; + EXPECT_EQ(slot.texObjId, destroyedTexObjId); + EXPECT_FALSE(slot.no_cache()); + + decode_fifo(destroyBytes); + EXPECT_EQ(slot.texObjId, destroyedTexObjId); + EXPECT_FALSE(slot.no_cache()); + + decode_fifo(loadBBytes); + EXPECT_EQ(slot.data, imageB); + EXPECT_EQ(slot.format(), GX_TF_RGB565); + EXPECT_FALSE(slot.no_cache()); +} + +TEST_F(GXFifoTest, DestroyTlutObj_EmitsAuroraDestroyCommandAndClearsIdentity) { + alignas(32) u16 palette[16]{}; + GXTlutObj obj{}; + GXInitTlutObj(&obj, palette, GX_TL_RGB565, 16); + GXDestroyTlutObj(&obj); + auto bytes = capture_fifo(); + + EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_DESTROY_TLUT)); + EXPECT_EQ(reinterpret_cast(&obj)->tlutObjId, 0u); + + reset_gx_state(); + decode_fifo(bytes); +} + +TEST_F(GXFifoTest, DestroyTlutObj_MarksLoadedSlotNoCacheUntilReloaded) { + alignas(32) u16 paletteA[16]{}; + alignas(32) u16 paletteB[16]{}; + GXTlutObj objA{}; + GXTlutObj objB{}; + + GXInitTlutObj(&objA, paletteA, GX_TL_RGB565, 16); + GXLoadTlut(&objA, GX_TLUT3); + auto loadABytes = capture_fifo(); + const auto destroyedTlutObjId = reinterpret_cast(&objA)->tlutObjId; + + GXDestroyTlutObj(&objA); + auto destroyBytes = capture_fifo(); + + GXInitTlutObj(&objB, paletteB, GX_TL_IA8, 16); + GXLoadTlut(&objB, GX_TLUT3); + auto loadBBytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(loadABytes); + auto& slot = gxState().loadedTluts[GX_TLUT3]; + EXPECT_EQ(slot.tlutObjId, destroyedTlutObjId); + EXPECT_FALSE(slot.no_cache()); + + decode_fifo(destroyBytes); + EXPECT_EQ(slot.tlutObjId, destroyedTlutObjId); + EXPECT_TRUE(slot.no_cache()); + + decode_fifo(loadBBytes); + EXPECT_EQ(slot.data, paletteB); + EXPECT_EQ(slot.format, GX_TL_IA8); + EXPECT_FALSE(slot.no_cache()); +} + +TEST_F(GXFifoTest, DestroyCopyTex_EmitsAuroraDestroyCommand) { + alignas(32) u8 image[32]{}; + + GXDestroyCopyTex(image); + auto bytes = capture_fifo(); + + EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_DESTROY_COPY_TEX)); + + reset_gx_state(); + decode_fifo(bytes); +} + +TEST_F(GXFifoTest, DestroyCopyTex_RemovesActiveCopyTextureAndCacheEntriesForPointer) { + alignas(32) u8 imageA[32]{}; + alignas(32) u8 imageB[32]{}; + + const aurora::gx::GXState::CopyTextureRef ref{.revision = 1}; + gxState().copyTextures[imageA] = ref; + gxState().copyTextures[imageB] = ref; + gxState().copyTextureCache.emplace( + aurora::gx::GXState::CopyTextureKey{.dest = imageA, .width = 32, .height = 32, .format = GX_TF_I4}, ref); + gxState().copyTextureCache.emplace( + aurora::gx::GXState::CopyTextureKey{.dest = imageA, .width = 64, .height = 64, .format = GX_TF_I8}, ref); + gxState().copyTextureCache.emplace( + aurora::gx::GXState::CopyTextureKey{.dest = imageB, .width = 32, .height = 32, .format = GX_TF_I4}, ref); + + GXDestroyCopyTex(imageA); + auto bytes = capture_fifo(); + + decode_fifo(bytes); + + EXPECT_FALSE(gxState().copyTextures.contains(imageA)); + EXPECT_TRUE(gxState().copyTextures.contains(imageB)); + for (const auto& [key, _] : gxState().copyTextureCache) { + EXPECT_NE(key.dest, imageA); + } + EXPECT_EQ(gxState().copyTextureCache.size(), 1u); +} + +// BP genMode (requires __GXSetDirtyState() flush) + +// --- GXSetCullMode --- + +TEST_F(GXFifoTest, CullMode_Back) { + GXSetCullMode(GX_CULL_BACK); + auto bytes = flush_and_capture(); + + reset_gx_state(); + g_gxState.cullMode = GX_CULL_NONE; + decode_fifo(bytes); + + // The encoder swaps front/back for hardware, and decoder swaps back + EXPECT_EQ(g_gxState.cullMode, GX_CULL_BACK); +} + +TEST_F(GXFifoTest, CullMode_Front) { + GXSetCullMode(GX_CULL_FRONT); + auto bytes = flush_and_capture(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.cullMode, GX_CULL_FRONT); +} + +TEST_F(GXFifoTest, CullMode_None) { + GXSetCullMode(GX_CULL_NONE); + auto bytes = flush_and_capture(); + + reset_gx_state(); + g_gxState.cullMode = GX_CULL_BACK; + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.cullMode, GX_CULL_NONE); +} + +TEST_F(GXFifoTest, GetLinePointCullShadowState) { + GXSetLineWidth(12, GX_TO_ZERO); + GXSetPointSize(34, GX_TO_ONE); + GXSetCullMode(GX_CULL_FRONT); + + u8 lineWidth = 0; + u8 pointSize = 0; + GXTexOffset lineOffs = GX_TO_ZERO; + GXTexOffset pointOffs = GX_TO_ZERO; + GXCullMode cullMode = GX_CULL_NONE; + + GXGetLineWidth(&lineWidth, &lineOffs); + GXGetPointSize(&pointSize, &pointOffs); + GXGetCullMode(&cullMode); + + EXPECT_EQ(lineWidth, 12); + EXPECT_EQ(lineOffs, GX_TO_ZERO); + EXPECT_EQ(pointSize, 34); + EXPECT_EQ(pointOffs, GX_TO_ONE); + EXPECT_EQ(cullMode, GX_CULL_FRONT); +} + +TEST_F(GXFifoTest, LinePointSize_Decode) { + GXSetLineWidth(12, GX_TO_ZERO); + GXSetPointSize(34, GX_TO_ONE); + auto bytes = capture_fifo(); + + ASSERT_EQ(bytes.size(), 10u); + EXPECT_EQ(bytes[0], 0x61); + EXPECT_EQ(bytes[1], 0x22); + EXPECT_EQ(bytes[5], 0x61); + EXPECT_EQ(bytes[6], 0x22); + + reset_gx_state(); + g_gxState.lineWidth = 1; + g_gxState.pointSize = 2; + g_gxState.lineTexOffset = GX_TO_ONE; + g_gxState.pointTexOffset = GX_TO_ZERO; + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.lineWidth, 12u); + EXPECT_EQ(g_gxState.lineTexOffset, GX_TO_ZERO); + EXPECT_EQ(g_gxState.pointSize, 34u); + EXPECT_EQ(g_gxState.pointTexOffset, GX_TO_ONE); +} + +// --- GXSetNumTevStages / GXSetNumTexGens / GXSetNumChans --- + +TEST_F(GXFifoTest, NumTevStages) { + GXSetNumTevStages(4); + auto bytes = flush_and_capture(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.numTevStages, 4u); +} + +TEST_F(GXFifoTest, NumTexGens) { + GXSetNumTexGens(3); + auto bytes = flush_and_capture(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.numTexGens, 3u); +} + +TEST_F(GXFifoTest, NumChans) { + GXSetNumChans(2); + auto bytes = flush_and_capture(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.numChans, 2u); +} + +// XF registers (direct FIFO writes) + +// --- GXLoadPosMtxImm (XF 0x000-0x077) --- + +TEST_F(GXFifoTest, LoadPosMtxImm_Identity) { + // 3x4 identity matrix + aurora::Mat3x4 mtx{}; + mtx.m0[0] = 1.0f; + mtx.m1[1] = 1.0f; + mtx.m2[2] = 1.0f; + + GXLoadPosMtxImm(&mtx, GX_PNMTX0); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& decoded = g_gxState.pnMtx[0].pos; + EXPECT_FLOAT_EQ(decoded.m0[0], 1.0f); + EXPECT_FLOAT_EQ(decoded.m0[1], 0.0f); + EXPECT_FLOAT_EQ(decoded.m0[2], 0.0f); + EXPECT_FLOAT_EQ(decoded.m0[3], 0.0f); + EXPECT_FLOAT_EQ(decoded.m1[0], 0.0f); + EXPECT_FLOAT_EQ(decoded.m1[1], 1.0f); + EXPECT_FLOAT_EQ(decoded.m1[2], 0.0f); + EXPECT_FLOAT_EQ(decoded.m1[3], 0.0f); + EXPECT_FLOAT_EQ(decoded.m2[0], 0.0f); + EXPECT_FLOAT_EQ(decoded.m2[1], 0.0f); + EXPECT_FLOAT_EQ(decoded.m2[2], 1.0f); + EXPECT_FLOAT_EQ(decoded.m2[3], 0.0f); +} + +TEST_F(GXFifoTest, LoadPosMtxImm_Translation) { + aurora::Mat3x4 mtx{}; + mtx.m0[0] = 1.0f; + mtx.m1[1] = 1.0f; + mtx.m2[2] = 1.0f; + mtx.m0[3] = 10.0f; + mtx.m1[3] = 20.0f; + mtx.m2[3] = 30.0f; + + GXLoadPosMtxImm(&mtx, GX_PNMTX3); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& decoded = g_gxState.pnMtx[3].pos; + EXPECT_FLOAT_EQ(decoded.m0[0], 1.0f); + EXPECT_FLOAT_EQ(decoded.m0[3], 10.0f); + EXPECT_FLOAT_EQ(decoded.m1[3], 20.0f); + EXPECT_FLOAT_EQ(decoded.m2[3], 30.0f); +} + +// --- GXLoadNrmMtxImm (XF 0x400-0x459) --- + +TEST_F(GXFifoTest, LoadNrmMtxImm_Identity) { + // 3x4 matrix with 3x3 identity (translation column ignored by encoder) + aurora::Mat3x4 mtx{}; + mtx.m0[0] = 1.0f; + mtx.m1[1] = 1.0f; + mtx.m2[2] = 1.0f; + + GXLoadNrmMtxImm(&mtx, GX_PNMTX0); + auto bytes = capture_fifo(); + + // XF opcode 0x10 + ASSERT_GE(bytes.size(), 5u); + EXPECT_EQ(bytes[0], 0x10); + + reset_gx_state(); + decode_fifo(bytes); + + auto& decoded = g_gxState.pnMtx[0].nrm; + EXPECT_FLOAT_EQ(decoded.m0[0], 1.0f); + EXPECT_FLOAT_EQ(decoded.m0[1], 0.0f); + EXPECT_FLOAT_EQ(decoded.m0[2], 0.0f); + EXPECT_FLOAT_EQ(decoded.m1[0], 0.0f); + EXPECT_FLOAT_EQ(decoded.m1[1], 1.0f); + EXPECT_FLOAT_EQ(decoded.m1[2], 0.0f); + EXPECT_FLOAT_EQ(decoded.m2[0], 0.0f); + EXPECT_FLOAT_EQ(decoded.m2[1], 0.0f); + EXPECT_FLOAT_EQ(decoded.m2[2], 1.0f); +} + +TEST_F(GXFifoTest, LoadNrmMtxImm_ArbitraryValues) { + aurora::Mat3x4 mtx{}; + mtx.m0[0] = 0.5f; mtx.m0[1] = -0.5f; mtx.m0[2] = 0.7f; mtx.m0[3] = 999.0f; + mtx.m1[0] = 0.3f; mtx.m1[1] = 0.8f; mtx.m1[2] = -0.1f; mtx.m1[3] = 888.0f; + mtx.m2[0] = -0.6f; mtx.m2[1] = 0.2f; mtx.m2[2] = 0.9f; mtx.m2[3] = 777.0f; + + GXLoadNrmMtxImm(&mtx, GX_PNMTX0); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& decoded = g_gxState.pnMtx[0].nrm; + // 3x3 portion should round-trip + EXPECT_FLOAT_EQ(decoded.m0[0], 0.5f); + EXPECT_FLOAT_EQ(decoded.m0[1], -0.5f); + EXPECT_FLOAT_EQ(decoded.m0[2], 0.7f); + EXPECT_FLOAT_EQ(decoded.m1[0], 0.3f); + EXPECT_FLOAT_EQ(decoded.m1[1], 0.8f); + EXPECT_FLOAT_EQ(decoded.m1[2], -0.1f); + EXPECT_FLOAT_EQ(decoded.m2[0], -0.6f); + EXPECT_FLOAT_EQ(decoded.m2[1], 0.2f); + EXPECT_FLOAT_EQ(decoded.m2[2], 0.9f); + // Translation column is NOT written by the encoder, so it stays zeroed + EXPECT_FLOAT_EQ(decoded.m0[3], 0.0f); + EXPECT_FLOAT_EQ(decoded.m1[3], 0.0f); + EXPECT_FLOAT_EQ(decoded.m2[3], 0.0f); +} + +TEST_F(GXFifoTest, LoadNrmMtxImm_DifferentSlot) { + aurora::Mat3x4 mtx{}; + mtx.m0[0] = 2.0f; + mtx.m1[1] = 3.0f; + mtx.m2[2] = 4.0f; + + GXLoadNrmMtxImm(&mtx, GX_PNMTX3); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& decoded = g_gxState.pnMtx[3].nrm; + EXPECT_FLOAT_EQ(decoded.m0[0], 2.0f); + EXPECT_FLOAT_EQ(decoded.m1[1], 3.0f); + EXPECT_FLOAT_EQ(decoded.m2[2], 4.0f); +} + +TEST_F(GXFifoTest, LoadNrmMtxImm_Isolation) { + // Loading nrm into slot 0 should not affect slot 1 or the pos matrix + aurora::Mat3x4 mtx{}; + mtx.m0[0] = 11.0f; + mtx.m1[1] = 22.0f; + mtx.m2[2] = 33.0f; + + GXLoadNrmMtxImm(&mtx, GX_PNMTX0); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + // Slot 0 nrm should have our values + EXPECT_FLOAT_EQ(g_gxState.pnMtx[0].nrm.m0[0], 11.0f); + // Slot 0 pos should remain zeroed (nrm write doesn't touch pos) + EXPECT_FLOAT_EQ(g_gxState.pnMtx[0].pos.m0[0], 0.0f); + // Slot 1 nrm should remain zeroed + EXPECT_FLOAT_EQ(g_gxState.pnMtx[1].nrm.m0[0], 0.0f); +} + +TEST_F(GXFifoTest, LoadNrmMtxImm_WithPosMtx) { + // Load both pos and nrm into the same slot, verify both decode correctly + aurora::Mat3x4 posMtx{}; + posMtx.m0[0] = 1.0f; + posMtx.m1[1] = 1.0f; + posMtx.m2[2] = 1.0f; + posMtx.m0[3] = 5.0f; + posMtx.m1[3] = 10.0f; + posMtx.m2[3] = 15.0f; + + aurora::Mat3x4 nrmMtx{}; + nrmMtx.m0[0] = 0.5f; + nrmMtx.m1[1] = 0.5f; + nrmMtx.m2[2] = 0.5f; + + GXLoadPosMtxImm(&posMtx, GX_PNMTX0); + GXLoadNrmMtxImm(&nrmMtx, GX_PNMTX0); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + // Position matrix + EXPECT_FLOAT_EQ(g_gxState.pnMtx[0].pos.m0[0], 1.0f); + EXPECT_FLOAT_EQ(g_gxState.pnMtx[0].pos.m0[3], 5.0f); + EXPECT_FLOAT_EQ(g_gxState.pnMtx[0].pos.m1[3], 10.0f); + EXPECT_FLOAT_EQ(g_gxState.pnMtx[0].pos.m2[3], 15.0f); + // Normal matrix + EXPECT_FLOAT_EQ(g_gxState.pnMtx[0].nrm.m0[0], 0.5f); + EXPECT_FLOAT_EQ(g_gxState.pnMtx[0].nrm.m1[1], 0.5f); + EXPECT_FLOAT_EQ(g_gxState.pnMtx[0].nrm.m2[2], 0.5f); +} + +// --- GXLoadTexMtxImm 3x4 (XF 0x078-0x0EF) --- + +TEST_F(GXFifoTest, LoadTexMtx3x4_Identity) { + aurora::Mat3x4 mtx{}; + mtx.m0[0] = 1.0f; + mtx.m1[1] = 1.0f; + mtx.m2[2] = 1.0f; + + GXLoadTexMtxImm(&mtx, GX_TEXMTX0, GX_MTX3x4); + auto bytes = capture_fifo(); + + ASSERT_GE(bytes.size(), 5u); + EXPECT_EQ(bytes[0], 0x10); + + reset_gx_state(); + decode_fifo(bytes); + + auto& decoded = g_gxState.texMtxs[0]; + EXPECT_FLOAT_EQ(decoded.m0[0], 1.0f); + EXPECT_FLOAT_EQ(decoded.m0[1], 0.0f); + EXPECT_FLOAT_EQ(decoded.m0[2], 0.0f); + EXPECT_FLOAT_EQ(decoded.m0[3], 0.0f); + EXPECT_FLOAT_EQ(decoded.m1[0], 0.0f); + EXPECT_FLOAT_EQ(decoded.m1[1], 1.0f); + EXPECT_FLOAT_EQ(decoded.m1[2], 0.0f); + EXPECT_FLOAT_EQ(decoded.m1[3], 0.0f); + EXPECT_FLOAT_EQ(decoded.m2[0], 0.0f); + EXPECT_FLOAT_EQ(decoded.m2[1], 0.0f); + EXPECT_FLOAT_EQ(decoded.m2[2], 1.0f); + EXPECT_FLOAT_EQ(decoded.m2[3], 0.0f); +} + +TEST_F(GXFifoTest, LoadTexMtx3x4_ArbitraryValues) { + aurora::Mat3x4 mtx{}; + mtx.m0[0] = 2.0f; mtx.m0[1] = 0.5f; mtx.m0[2] = 0.0f; mtx.m0[3] = 10.0f; + mtx.m1[0] = -0.5f; mtx.m1[1] = 3.0f; mtx.m1[2] = 0.0f; mtx.m1[3] = 20.0f; + mtx.m2[0] = 0.0f; mtx.m2[1] = 0.0f; mtx.m2[2] = 1.5f; mtx.m2[3] = -5.0f; + + GXLoadTexMtxImm(&mtx, GX_TEXMTX0, GX_MTX3x4); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& decoded = g_gxState.texMtxs[0]; + EXPECT_FLOAT_EQ(decoded.m0[0], 2.0f); + EXPECT_FLOAT_EQ(decoded.m0[1], 0.5f); + EXPECT_FLOAT_EQ(decoded.m0[2], 0.0f); + EXPECT_FLOAT_EQ(decoded.m0[3], 10.0f); + EXPECT_FLOAT_EQ(decoded.m1[0], -0.5f); + EXPECT_FLOAT_EQ(decoded.m1[1], 3.0f); + EXPECT_FLOAT_EQ(decoded.m1[2], 0.0f); + EXPECT_FLOAT_EQ(decoded.m1[3], 20.0f); + EXPECT_FLOAT_EQ(decoded.m2[0], 0.0f); + EXPECT_FLOAT_EQ(decoded.m2[1], 0.0f); + EXPECT_FLOAT_EQ(decoded.m2[2], 1.5f); + EXPECT_FLOAT_EQ(decoded.m2[3], -5.0f); +} + +TEST_F(GXFifoTest, LoadTexMtx3x4_DifferentSlot) { + aurora::Mat3x4 mtx{}; + mtx.m0[0] = 7.0f; + mtx.m1[1] = 8.0f; + mtx.m2[2] = 9.0f; + mtx.m2[3] = 42.0f; + + GXLoadTexMtxImm(&mtx, GX_TEXMTX5, GX_MTX3x4); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + // GX_TEXMTX5 = 45, addr = 45*4 = 180 = 0xB4, index = (0xB4 - 0x78) / 12 = 5 + auto& decoded = g_gxState.texMtxs[5]; + EXPECT_FLOAT_EQ(decoded.m0[0], 7.0f); + EXPECT_FLOAT_EQ(decoded.m1[1], 8.0f); + EXPECT_FLOAT_EQ(decoded.m2[2], 9.0f); + EXPECT_FLOAT_EQ(decoded.m2[3], 42.0f); +} + +TEST_F(GXFifoTest, LoadTexMtx3x4_LastSlot) { + aurora::Mat3x4 mtx{}; + mtx.m0[0] = 11.0f; + mtx.m1[1] = 22.0f; + mtx.m2[2] = 33.0f; + + GXLoadTexMtxImm(&mtx, GX_TEXMTX9, GX_MTX3x4); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& decoded = g_gxState.texMtxs[9]; + EXPECT_FLOAT_EQ(decoded.m0[0], 11.0f); + EXPECT_FLOAT_EQ(decoded.m1[1], 22.0f); + EXPECT_FLOAT_EQ(decoded.m2[2], 33.0f); +} + +TEST_F(GXFifoTest, LoadTexMtx3x4_Isolation) { + aurora::Mat3x4 mtx{}; + mtx.m0[0] = 50.0f; + mtx.m1[1] = 60.0f; + mtx.m2[2] = 70.0f; + + GXLoadTexMtxImm(&mtx, GX_TEXMTX0, GX_MTX3x4); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_FLOAT_EQ(g_gxState.texMtxs[0].m0[0], 50.0f); + // Slot 1 should remain zeroed + EXPECT_FLOAT_EQ(g_gxState.texMtxs[1].m0[0], 0.0f); + EXPECT_FLOAT_EQ(g_gxState.texMtxs[1].m1[1], 0.0f); +} + +// --- GXLoadTexMtxImm 2x4 (XF 0x078-0x0EF) --- + +TEST_F(GXFifoTest, LoadTexMtx2x4_Identity) { + // 2x4 identity: row0 = [1,0,0,0], row1 = [0,1,0,0] + aurora::Mat3x4 mtx{}; + mtx.m0[0] = 1.0f; + mtx.m1[1] = 1.0f; + + GXLoadTexMtxImm(&mtx, GX_TEXMTX0, GX_MTX2x4); + auto bytes = capture_fifo(); + + ASSERT_GE(bytes.size(), 5u); + EXPECT_EQ(bytes[0], 0x10); + + reset_gx_state(); + decode_fifo(bytes); + + auto& decoded = g_gxState.texMtxs[0]; + // First two rows should round-trip + EXPECT_FLOAT_EQ(decoded.m0[0], 1.0f); + EXPECT_FLOAT_EQ(decoded.m0[1], 0.0f); + EXPECT_FLOAT_EQ(decoded.m0[2], 0.0f); + EXPECT_FLOAT_EQ(decoded.m0[3], 0.0f); + EXPECT_FLOAT_EQ(decoded.m1[0], 0.0f); + EXPECT_FLOAT_EQ(decoded.m1[1], 1.0f); + EXPECT_FLOAT_EQ(decoded.m1[2], 0.0f); + EXPECT_FLOAT_EQ(decoded.m1[3], 0.0f); + // Third row not written by 2x4, should be zeroed + EXPECT_FLOAT_EQ(decoded.m2[0], 0.0f); + EXPECT_FLOAT_EQ(decoded.m2[1], 0.0f); + EXPECT_FLOAT_EQ(decoded.m2[2], 0.0f); + EXPECT_FLOAT_EQ(decoded.m2[3], 0.0f); +} + +TEST_F(GXFifoTest, LoadTexMtx2x4_ArbitraryValues) { + aurora::Mat3x4 mtx{}; + mtx.m0[0] = 0.5f; mtx.m0[1] = -1.0f; mtx.m0[2] = 0.25f; mtx.m0[3] = 100.0f; + mtx.m1[0] = 3.0f; mtx.m1[1] = 0.0f; mtx.m1[2] = -2.5f; mtx.m1[3] = -50.0f; + // Row 2 values should be ignored by the encoder + mtx.m2[0] = 999.0f; + + GXLoadTexMtxImm(&mtx, GX_TEXMTX0, GX_MTX2x4); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& decoded = g_gxState.texMtxs[0]; + EXPECT_FLOAT_EQ(decoded.m0[0], 0.5f); + EXPECT_FLOAT_EQ(decoded.m0[1], -1.0f); + EXPECT_FLOAT_EQ(decoded.m0[2], 0.25f); + EXPECT_FLOAT_EQ(decoded.m0[3], 100.0f); + EXPECT_FLOAT_EQ(decoded.m1[0], 3.0f); + EXPECT_FLOAT_EQ(decoded.m1[1], 0.0f); + EXPECT_FLOAT_EQ(decoded.m1[2], -2.5f); + EXPECT_FLOAT_EQ(decoded.m1[3], -50.0f); + // Row 2 should be zeroed (only 8 floats written) + EXPECT_FLOAT_EQ(decoded.m2[0], 0.0f); +} + +TEST_F(GXFifoTest, LoadTexMtx2x4_DifferentSlot) { + aurora::Mat3x4 mtx{}; + mtx.m0[0] = 4.0f; + mtx.m0[3] = 15.0f; + mtx.m1[1] = 5.0f; + mtx.m1[3] = 25.0f; + + GXLoadTexMtxImm(&mtx, GX_TEXMTX3, GX_MTX2x4); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& decoded = g_gxState.texMtxs[3]; + EXPECT_FLOAT_EQ(decoded.m0[0], 4.0f); + EXPECT_FLOAT_EQ(decoded.m0[3], 15.0f); + EXPECT_FLOAT_EQ(decoded.m1[1], 5.0f); + EXPECT_FLOAT_EQ(decoded.m1[3], 25.0f); +} + +TEST_F(GXFifoTest, LoadTexMtx2x4_Isolation) { + aurora::Mat3x4 mtx{}; + mtx.m0[0] = 10.0f; + mtx.m1[1] = 20.0f; + + GXLoadTexMtxImm(&mtx, GX_TEXMTX0, GX_MTX2x4); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_FLOAT_EQ(g_gxState.texMtxs[0].m0[0], 10.0f); + EXPECT_FLOAT_EQ(g_gxState.texMtxs[0].m1[1], 20.0f); + // Slot 1 should remain zeroed + EXPECT_FLOAT_EQ(g_gxState.texMtxs[1].m0[0], 0.0f); + EXPECT_FLOAT_EQ(g_gxState.texMtxs[1].m1[1], 0.0f); +} + +// --- GXSetProjection (XF 0x1020-0x1026) --- + +TEST_F(GXFifoTest, Projection_Perspective) { + aurora::Mat4x4 proj{}; + proj.m0[0] = 1.5f; // near / (right - left) * 2 + proj.m0[2] = 0.1f; + proj.m1[1] = 2.0f; // near / (top - bottom) * 2 + proj.m1[2] = 0.2f; + proj.m2[2] = -1.002f; + proj.m2[3] = -0.2002f; + proj.m3[2] = -1.0f; + + GXSetProjection(&proj, GX_PERSPECTIVE); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.projType, GX_PERSPECTIVE); + EXPECT_FLOAT_EQ(g_gxState.proj.m0[0], 1.5f); + EXPECT_FLOAT_EQ(g_gxState.proj.m0[2], 0.1f); + EXPECT_FLOAT_EQ(g_gxState.proj.m1[1], 2.0f); + EXPECT_FLOAT_EQ(g_gxState.proj.m1[2], 0.2f); + EXPECT_FLOAT_EQ(g_gxState.proj.m2[2], -1.002f); + EXPECT_FLOAT_EQ(g_gxState.proj.m2[3], -0.2002f); + EXPECT_FLOAT_EQ(g_gxState.proj.m3[2], -1.0f); +} + +TEST_F(GXFifoTest, Projection_Orthographic) { + aurora::Mat4x4 proj{}; + proj.m0[0] = 2.0f / 640.0f; + proj.m0[3] = -1.0f; + proj.m1[1] = 2.0f / 480.0f; + proj.m1[3] = -1.0f; + proj.m2[2] = -1.0f / 10000.0f; + proj.m2[3] = 0.0f; + proj.m3[3] = 1.0f; + + GXSetProjection(&proj, GX_ORTHOGRAPHIC); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.projType, GX_ORTHOGRAPHIC); + EXPECT_FLOAT_EQ(g_gxState.proj.m0[0], 2.0f / 640.0f); + EXPECT_FLOAT_EQ(g_gxState.proj.m0[3], -1.0f); + EXPECT_FLOAT_EQ(g_gxState.proj.m1[1], 2.0f / 480.0f); + EXPECT_FLOAT_EQ(g_gxState.proj.m1[3], -1.0f); + EXPECT_FLOAT_EQ(g_gxState.proj.m3[3], 1.0f); +} + +TEST_F(GXFifoTest, GetProjectionAndScissorShadowState) { + const f32 proj[] = {0.0f, 1.5f, 0.1f, 2.0f, 0.2f, -1.002f, -0.2002f}; + f32 outProj[7]{}; + u32 left = 0, top = 0, width = 0, height = 0; + + GXSetProjectionv(proj); + GXSetScissor(16, 24, 320, 240); + GXGetProjectionv(outProj); + GXGetScissor(&left, &top, &width, &height); + + for (size_t i = 0; i < 7; ++i) { + EXPECT_FLOAT_EQ(outProj[i], proj[i]); + } + EXPECT_EQ(left, 16u); + EXPECT_EQ(top, 24u); + EXPECT_EQ(width, 320u); + EXPECT_EQ(height, 240u); +} + +TEST_F(GXFifoTest, Scissor_EncodesBpAndDecodesLogicalState) { + GXSetScissor(16, 24, 320, 240); + auto bytes = capture_fifo(); + + EXPECT_TRUE(has_bp_write(bytes, 0x20)); + EXPECT_TRUE(has_bp_write(bytes, 0x21)); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.logicalScissor.x, 16); + EXPECT_EQ(g_gxState.logicalScissor.y, 24); + EXPECT_EQ(g_gxState.logicalScissor.width, 320); + EXPECT_EQ(g_gxState.logicalScissor.height, 240); +} + +TEST_F(GXFifoTest, ScissorBoxOffset_EncodesBp59AndDecodesState) { + GXSetScissorBoxOffset(1024, 0); + auto bytes = capture_fifo(); + + EXPECT_TRUE(has_bp_write(bytes, 0x59)); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.scissorOffsetX, 1024); + EXPECT_EQ(g_gxState.scissorOffsetY, 0); +} + +TEST_F(GXFifoTest, GetViewportShadowState) { + f32 vp[6]{}; + + GXSetViewport(10.0f, 20.0f, 640.0f, 480.0f, 0.1f, 1.0f); + GXGetViewportv(vp); + EXPECT_FLOAT_EQ(vp[0], 10.0f); + EXPECT_FLOAT_EQ(vp[1], 20.0f); + EXPECT_FLOAT_EQ(vp[2], 640.0f); + EXPECT_FLOAT_EQ(vp[3], 480.0f); + EXPECT_FLOAT_EQ(vp[4], 0.1f); + EXPECT_FLOAT_EQ(vp[5], 1.0f); + + GXSetViewportJitter(30.0f, 40.0f, 320.0f, 240.0f, 0.2f, 0.9f, 0); + GXGetViewportv(vp); + EXPECT_FLOAT_EQ(vp[0], 30.0f); + EXPECT_FLOAT_EQ(vp[1], 39.5f); + EXPECT_FLOAT_EQ(vp[2], 320.0f); + EXPECT_FLOAT_EQ(vp[3], 240.0f); + EXPECT_FLOAT_EQ(vp[4], 0.2f); + EXPECT_FLOAT_EQ(vp[5], 0.9f); +} + +TEST_F(GXFifoTest, Viewport_DecodesLogicalViewportState) { + GXSetViewport(10.0f, 20.0f, 640.0f, 480.0f, 0.1f, 1.0f); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_FLOAT_EQ(g_gxState.logicalViewport.left, 10.0f); + EXPECT_FLOAT_EQ(g_gxState.logicalViewport.top, 20.0f); + EXPECT_FLOAT_EQ(g_gxState.logicalViewport.width, 640.0f); + EXPECT_FLOAT_EQ(g_gxState.logicalViewport.height, 480.0f); + EXPECT_FLOAT_EQ(g_gxState.renderViewport.left, 10.0f); + EXPECT_FLOAT_EQ(g_gxState.renderViewport.top, 20.0f); + EXPECT_FLOAT_EQ(g_gxState.renderViewport.width, 640.0f); + EXPECT_FLOAT_EQ(g_gxState.renderViewport.height, 480.0f); +} + +TEST_F(GXFifoTest, ViewportRender_EncodesAuroraOverride) { + GXSetViewportRender(100.0f, 50.0f, 1280.0f, 720.0f, 0.0f, 1.0f); + auto bytes = capture_fifo(); + + EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_VIEWPORT_RENDER)); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_FLOAT_EQ(g_gxState.renderViewport.left, 100.0f); + EXPECT_FLOAT_EQ(g_gxState.renderViewport.top, 50.0f); + EXPECT_FLOAT_EQ(g_gxState.renderViewport.width, 1280.0f); + EXPECT_FLOAT_EQ(g_gxState.renderViewport.height, 720.0f); +} + +TEST_F(GXFifoTest, ScissorRender_EncodesAuroraOverride) { + GXSetScissorRender(100, 40, 800, 600); + auto bytes = capture_fifo(); + + EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_SCISSOR_RENDER)); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.renderScissor.x, 100); + EXPECT_EQ(g_gxState.renderScissor.y, 40); + EXPECT_EQ(g_gxState.renderScissor.width, 800); + EXPECT_EQ(g_gxState.renderScissor.height, 600); +} + +// --- GXLoadLightObjImm (XF 0x600-0x67F) --- + +TEST_F(GXFifoTest, LoadLightObjImm_Light0_BasicColor) { + GXLightObj lightObj; + GXInitLightPos(&lightObj, 100.0f, 200.0f, 300.0f); + GXInitLightDir(&lightObj, 0.0f, -1.0f, 0.0f); + GXInitLightColor(&lightObj, {255, 128, 64, 255}); + GXInitLightAttn(&lightObj, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f); + + GXLoadLightObjImm(&lightObj, GX_LIGHT0); + auto bytes = capture_fifo(); + + // XF bulk write: opcode 0x10 + ASSERT_GE(bytes.size(), 5u); + EXPECT_EQ(bytes[0], 0x10); + + reset_gx_state(); + g_gxState.preparedLightsDirty = false; + decode_fifo(bytes); + EXPECT_TRUE(g_gxState.preparedLightsDirty); + + auto& light = g_gxState.lights[0]; + // Color + EXPECT_NEAR(light.color[0], 255.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(light.color[1], 128.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(light.color[2], 64.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(light.color[3], 255.f / 255.f, 1.f / 255.f); + // Position + EXPECT_FLOAT_EQ(light.pos[0], 100.0f); + EXPECT_FLOAT_EQ(light.pos[1], 200.0f); + EXPECT_FLOAT_EQ(light.pos[2], 300.0f); + // Direction (GXInitLightDir negates) + EXPECT_FLOAT_EQ(light.dir[0], 0.0f); + EXPECT_FLOAT_EQ(light.dir[1], 1.0f); + EXPECT_FLOAT_EQ(light.dir[2], 0.0f); + // Cosine attenuation + EXPECT_FLOAT_EQ(light.cosAtt[0], 1.0f); + EXPECT_FLOAT_EQ(light.cosAtt[1], 0.0f); + EXPECT_FLOAT_EQ(light.cosAtt[2], 0.0f); + // Distance attenuation + EXPECT_FLOAT_EQ(light.distAtt[0], 1.0f); + EXPECT_FLOAT_EQ(light.distAtt[1], 0.0f); + EXPECT_FLOAT_EQ(light.distAtt[2], 0.0f); +} + +TEST_F(GXFifoTest, LoadLightObjImm_Light3_Attenuation) { + GXLightObj lightObj; + GXInitLightPos(&lightObj, -50.0f, 0.0f, 75.0f); + GXInitLightDir(&lightObj, 1.0f, 0.0f, 0.0f); + GXInitLightColor(&lightObj, {0, 255, 0, 128}); + GXInitLightAttn(&lightObj, 0.5f, 0.3f, 0.2f, 1.0f, 0.01f, 0.001f); + + GXLoadLightObjImm(&lightObj, GX_LIGHT3); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& light = g_gxState.lights[3]; + EXPECT_NEAR(light.color[0], 0.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(light.color[1], 255.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(light.color[2], 0.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(light.color[3], 128.f / 255.f, 1.f / 255.f); + EXPECT_FLOAT_EQ(light.pos[0], -50.0f); + EXPECT_FLOAT_EQ(light.pos[1], 0.0f); + EXPECT_FLOAT_EQ(light.pos[2], 75.0f); + EXPECT_FLOAT_EQ(light.dir[0], -1.0f); + EXPECT_FLOAT_EQ(light.dir[1], 0.0f); + EXPECT_FLOAT_EQ(light.dir[2], 0.0f); + EXPECT_FLOAT_EQ(light.cosAtt[0], 0.5f); + EXPECT_FLOAT_EQ(light.cosAtt[1], 0.3f); + EXPECT_FLOAT_EQ(light.cosAtt[2], 0.2f); + EXPECT_FLOAT_EQ(light.distAtt[0], 1.0f); + EXPECT_FLOAT_EQ(light.distAtt[1], 0.01f); + EXPECT_FLOAT_EQ(light.distAtt[2], 0.001f); +} + +TEST_F(GXFifoTest, LoadLightObjImm_Light7_LastLight) { + GXLightObj lightObj; + GXInitLightPos(&lightObj, 0.0f, 1000.0f, 0.0f); + GXInitLightDir(&lightObj, 0.0f, 0.0f, -1.0f); + GXInitLightColor(&lightObj, {128, 128, 128, 255}); + GXInitLightAttn(&lightObj, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f); + + GXLoadLightObjImm(&lightObj, GX_LIGHT7); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& light = g_gxState.lights[7]; + EXPECT_NEAR(light.color[0], 128.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(light.color[1], 128.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(light.color[2], 128.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(light.color[3], 255.f / 255.f, 1.f / 255.f); + EXPECT_FLOAT_EQ(light.pos[0], 0.0f); + EXPECT_FLOAT_EQ(light.pos[1], 1000.0f); + EXPECT_FLOAT_EQ(light.pos[2], 0.0f); + EXPECT_FLOAT_EQ(light.dir[0], 0.0f); + EXPECT_FLOAT_EQ(light.dir[1], 0.0f); + EXPECT_FLOAT_EQ(light.dir[2], 1.0f); +} + +TEST_F(GXFifoTest, LoadLightObjImm_SpotLight) { + GXLightObj lightObj; + GXInitLightPos(&lightObj, 10.0f, 20.0f, 30.0f); + GXInitLightDir(&lightObj, 0.0f, -1.0f, 0.0f); + GXInitLightSpot(&lightObj, 45.0f, GX_SP_COS); + GXInitLightDistAttn(&lightObj, 100.0f, 0.5f, GX_DA_MEDIUM); + GXInitLightColor(&lightObj, {255, 255, 255, 255}); + + GXLoadLightObjImm(&lightObj, GX_LIGHT1); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& light = g_gxState.lights[1]; + EXPECT_NEAR(light.color[0], 1.0f, 1.f / 255.f); + EXPECT_NEAR(light.color[1], 1.0f, 1.f / 255.f); + EXPECT_NEAR(light.color[2], 1.0f, 1.f / 255.f); + EXPECT_FLOAT_EQ(light.pos[0], 10.0f); + EXPECT_FLOAT_EQ(light.pos[1], 20.0f); + EXPECT_FLOAT_EQ(light.pos[2], 30.0f); + // GX_SP_COS with cutoff=45: cr = cos(45 * pi / 180) + // a0 = -cr/(1-cr), a1 = 1/(1-cr), a2 = 0 + float cr = std::cos(45.0f * M_PIF / 180.0f); + EXPECT_FLOAT_EQ(light.cosAtt[0], -cr / (1.0f - cr)); + EXPECT_FLOAT_EQ(light.cosAtt[1], 1.0f / (1.0f - cr)); + EXPECT_FLOAT_EQ(light.cosAtt[2], 0.0f); + // GX_DA_MEDIUM with refDist=100, refBright=0.5: + // k0 = 1, k1 = 0.5*(1-b)/(b*d), k2 = 0.5*(1-b)/(b*d*d) + EXPECT_FLOAT_EQ(light.distAtt[0], 1.0f); + EXPECT_FLOAT_EQ(light.distAtt[1], 0.5f * 0.5f / (0.5f * 100.0f)); + EXPECT_FLOAT_EQ(light.distAtt[2], 0.5f * 0.5f / (0.5f * 100.0f * 100.0f)); +} + +// --- GXSetChanCtrl (XF 0x100E-0x1011) --- + +TEST_F(GXFifoTest, ChanCtrl_Color0_LightingEnabled) { + GXSetChanCtrl(GX_COLOR0, true, GX_SRC_REG, GX_SRC_VTX, GX_LIGHT0 | GX_LIGHT1, GX_DF_CLAMP, GX_AF_SPOT); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& cfg = g_gxState.colorChannelConfig[GX_COLOR0]; + EXPECT_TRUE(cfg.lightingEnabled); + EXPECT_EQ(cfg.matSrc, GX_SRC_VTX); + EXPECT_EQ(cfg.ambSrc, GX_SRC_REG); + EXPECT_EQ(cfg.diffFn, GX_DF_CLAMP); + EXPECT_EQ(cfg.attnFn, GX_AF_SPOT); + + // Light mask should be 0x03 (lights 0 and 1) + auto& state = g_gxState.colorChannelState[GX_COLOR0]; + EXPECT_TRUE(state.lightMask[0]); + EXPECT_TRUE(state.lightMask[1]); + EXPECT_FALSE(state.lightMask[2]); +} + +TEST_F(GXFifoTest, ChanCtrl_Alpha0_NoLighting) { + GXSetChanCtrl(GX_ALPHA0, false, GX_SRC_VTX, GX_SRC_REG, 0, GX_DF_NONE, GX_AF_NONE); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& cfg = g_gxState.colorChannelConfig[GX_ALPHA0]; + EXPECT_FALSE(cfg.lightingEnabled); + EXPECT_EQ(cfg.matSrc, GX_SRC_REG); + EXPECT_EQ(cfg.ambSrc, GX_SRC_VTX); + EXPECT_EQ(cfg.attnFn, GX_AF_NONE); +} + +TEST_F(GXFifoTest, ChanCtrl_Color1_SpecularLighting) { + GXSetChanCtrl(GX_COLOR1, true, GX_SRC_REG, GX_SRC_REG, GX_LIGHT2 | GX_LIGHT5, GX_DF_SIGN, GX_AF_SPEC); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& cfg = g_gxState.colorChannelConfig[GX_COLOR1]; + EXPECT_TRUE(cfg.lightingEnabled); + EXPECT_EQ(cfg.matSrc, GX_SRC_REG); + EXPECT_EQ(cfg.ambSrc, GX_SRC_REG); + EXPECT_EQ(cfg.diffFn, GX_DF_NONE); + EXPECT_EQ(cfg.attnFn, GX_AF_SPEC); + + auto& state = g_gxState.colorChannelState[GX_COLOR1]; + EXPECT_FALSE(state.lightMask[0]); + EXPECT_FALSE(state.lightMask[1]); + EXPECT_TRUE(state.lightMask[2]); + EXPECT_FALSE(state.lightMask[3]); + EXPECT_FALSE(state.lightMask[4]); + EXPECT_TRUE(state.lightMask[5]); +} + +TEST_F(GXFifoTest, ChanCtrl_EncodesHardwareAttenuationField) { + GXSetChanCtrl(GX_COLOR0, true, GX_SRC_REG, GX_SRC_REG, GX_LIGHT0, GX_DF_SIGN, GX_AF_SPEC); + auto specBytes = capture_fifo(); + ASSERT_GE(specBytes.size(), 9u); + EXPECT_EQ((read_be32_at(specBytes, 5) >> 7) & 0x3u, static_cast(GX_DF_NONE)); + EXPECT_EQ((read_be32_at(specBytes, 5) >> 9) & 0x3u, 1u); + + GXSetChanCtrl(GX_COLOR0, true, GX_SRC_REG, GX_SRC_REG, GX_LIGHT0, GX_DF_CLAMP, GX_AF_SPOT); + auto spotBytes = capture_fifo(); + ASSERT_GE(spotBytes.size(), 9u); + EXPECT_EQ((read_be32_at(spotBytes, 5) >> 9) & 0x3u, 3u); + + GXSetChanCtrl(GX_COLOR0, true, GX_SRC_REG, GX_SRC_REG, GX_LIGHT0, GX_DF_NONE, GX_AF_NONE); + auto noneBytes = capture_fifo(); + ASSERT_GE(noneBytes.size(), 9u); + EXPECT_EQ((read_be32_at(noneBytes, 5) >> 9) & 0x3u, 2u); +} + +TEST_F(GXFifoTest, ChanCtrl_DecodesRawHardwareAttenuationField) { + reset_gx_state(); + decode_fifo(xf_cmd(0x100E, {1u << 9})); + EXPECT_EQ(g_gxState.colorChannelConfig[GX_COLOR0].attnFn, GX_AF_SPEC); + + reset_gx_state(); + decode_fifo(xf_cmd(0x100E, {2u << 9})); + EXPECT_EQ(g_gxState.colorChannelConfig[GX_COLOR0].attnFn, GX_AF_NONE); + + reset_gx_state(); + decode_fifo(xf_cmd(0x100E, {3u << 9})); + EXPECT_EQ(g_gxState.colorChannelConfig[GX_COLOR0].attnFn, GX_AF_SPOT); + + reset_gx_state(); + decode_fifo(xf_cmd(0x100E, {0u << 9})); + EXPECT_EQ(g_gxState.colorChannelConfig[GX_COLOR0].attnFn, GX_AF_NONE); +} + +TEST_F(GXFifoTest, ChanCtrl_Color0A0_Compound) { + // GX_COLOR0A0 should set both GX_COLOR0 and GX_ALPHA0 + GXSetChanCtrl(GX_COLOR0A0, true, GX_SRC_REG, GX_SRC_VTX, GX_LIGHT0, GX_DF_CLAMP, GX_AF_SPOT); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + // Both COLOR0 and ALPHA0 should be configured identically + auto& cfgC = g_gxState.colorChannelConfig[GX_COLOR0]; + EXPECT_TRUE(cfgC.lightingEnabled); + EXPECT_EQ(cfgC.matSrc, GX_SRC_VTX); + EXPECT_EQ(cfgC.ambSrc, GX_SRC_REG); + EXPECT_EQ(cfgC.diffFn, GX_DF_CLAMP); + EXPECT_EQ(cfgC.attnFn, GX_AF_SPOT); + + auto& cfgA = g_gxState.colorChannelConfig[GX_ALPHA0]; + EXPECT_TRUE(cfgA.lightingEnabled); + EXPECT_EQ(cfgA.matSrc, GX_SRC_VTX); + EXPECT_EQ(cfgA.ambSrc, GX_SRC_REG); + EXPECT_EQ(cfgA.attnFn, GX_AF_SPOT); + + EXPECT_TRUE(g_gxState.colorChannelState[GX_COLOR0].lightMask[0]); + EXPECT_TRUE(g_gxState.colorChannelState[GX_ALPHA0].lightMask[0]); +} + +TEST_F(GXFifoTest, ChanCtrl_Color1A1_Compound) { + GXSetChanCtrl(GX_COLOR1A1, false, GX_SRC_VTX, GX_SRC_VTX, 0, GX_DF_NONE, GX_AF_NONE); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& cfgC = g_gxState.colorChannelConfig[GX_COLOR1]; + EXPECT_FALSE(cfgC.lightingEnabled); + EXPECT_EQ(cfgC.ambSrc, GX_SRC_VTX); + EXPECT_EQ(cfgC.matSrc, GX_SRC_VTX); + + auto& cfgA = g_gxState.colorChannelConfig[GX_ALPHA1]; + EXPECT_FALSE(cfgA.lightingEnabled); + EXPECT_EQ(cfgA.ambSrc, GX_SRC_VTX); + EXPECT_EQ(cfgA.matSrc, GX_SRC_VTX); +} + +// --- GXSetTexCoordGen2 (XF 0x1040-0x105F) --- + +TEST_F(GXFifoTest, TexCoordGen_Mtx2x4_Tex0) { + GXSetTexCoordGen2(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_TEXMTX0, GX_FALSE, GX_PTIDENTITY); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& tcg = g_gxState.tcgs[GX_TEXCOORD0]; + EXPECT_EQ(tcg.type, GX_TG_MTX2x4); + EXPECT_EQ(tcg.src, GX_TG_TEX0); + EXPECT_EQ(tcg.mtx, GX_TEXMTX0); + EXPECT_EQ(tcg.postMtx, GX_PTIDENTITY); +} + +TEST_F(GXFifoTest, TexCoordGen_Mtx3x4_Nrm) { + GXSetTexCoordGen2(GX_TEXCOORD1, GX_TG_MTX3x4, GX_TG_NRM, GX_TEXMTX0, GX_TRUE, GX_PTTEXMTX0); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& tcg = g_gxState.tcgs[GX_TEXCOORD1]; + EXPECT_EQ(tcg.type, GX_TG_MTX3x4); + EXPECT_EQ(tcg.src, GX_TG_NRM); + EXPECT_EQ(tcg.mtx, GX_TEXMTX0); + EXPECT_TRUE(tcg.normalize); + EXPECT_EQ(tcg.postMtx, GX_PTTEXMTX0); +} + +TEST_F(GXFifoTest, TexCoordGen_SRTG_Color0) { + GXSetTexCoordGen2(GX_TEXCOORD0, GX_TG_SRTG, GX_TG_COLOR0, GX_TEXMTX0, GX_FALSE, GX_PTIDENTITY); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& tcg = g_gxState.tcgs[GX_TEXCOORD0]; + EXPECT_EQ(tcg.type, GX_TG_SRTG); + EXPECT_EQ(tcg.mtx, GX_TEXMTX0); +} + +TEST_F(GXFifoTest, TexCoordGen_NonZeroMtx) { + GXSetTexCoordGen2(GX_TEXCOORD0, GX_TG_MTX3x4, GX_TG_TEX0, GX_TEXMTX3, GX_FALSE, GX_PTTEXMTX5); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& tcg = g_gxState.tcgs[GX_TEXCOORD0]; + EXPECT_EQ(tcg.type, GX_TG_MTX3x4); + EXPECT_EQ(tcg.src, GX_TG_TEX0); + EXPECT_EQ(tcg.mtx, GX_TEXMTX3); + EXPECT_EQ(tcg.postMtx, GX_PTTEXMTX5); +} + +TEST_F(GXFifoTest, TexCoordGen_MultipleCoords) { + GXSetTexCoordGen2(GX_TEXCOORD0, GX_TG_MTX3x4, GX_TG_TEX0, GX_TEXMTX0, GX_FALSE, GX_PTTEXMTX0); + GXSetTexCoordGen2(GX_TEXCOORD1, GX_TG_MTX3x4, GX_TG_TEX1, GX_TEXMTX1, GX_FALSE, GX_PTTEXMTX1); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.tcgs[0].mtx, GX_TEXMTX0); + EXPECT_EQ(g_gxState.tcgs[0].postMtx, GX_PTTEXMTX0); + EXPECT_EQ(g_gxState.tcgs[0].src, GX_TG_TEX0); + EXPECT_EQ(g_gxState.tcgs[1].mtx, GX_TEXMTX1); + EXPECT_EQ(g_gxState.tcgs[1].postMtx, GX_PTTEXMTX1); + EXPECT_EQ(g_gxState.tcgs[1].src, GX_TG_TEX1); +} + +TEST_F(GXFifoTest, TexCoordGen_HighCoord_MatIdxB) { + // TexCoord4+ uses matIdxB + GXSetTexCoordGen2(GX_TEXCOORD4, GX_TG_MTX2x4, GX_TG_TEX4, GX_TEXMTX5, GX_FALSE, GX_PTTEXMTX3); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& tcg = g_gxState.tcgs[GX_TEXCOORD4]; + EXPECT_EQ(tcg.type, GX_TG_MTX2x4); + EXPECT_EQ(tcg.src, GX_TG_TEX4); + EXPECT_EQ(tcg.mtx, GX_TEXMTX5); + EXPECT_EQ(tcg.postMtx, GX_PTTEXMTX3); +} + +TEST_F(GXFifoTest, TexCoordGen_Identity) { + GXSetTexCoordGen2(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_IDENTITY, GX_FALSE, GX_PTIDENTITY); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& tcg = g_gxState.tcgs[GX_TEXCOORD0]; + EXPECT_EQ(tcg.mtx, GX_IDENTITY); + EXPECT_EQ(tcg.postMtx, GX_PTIDENTITY); +} + +TEST_F(GXFifoTest, MatrixIndexA_DecodesTexMatricesFromCpPacket) { + const u32 value = (GX_PNMTX3 << 0) | (GX_TEXMTX3 << 6) | (GX_TEXMTX4 << 12) | (GX_IDENTITY << 18) | + (GX_TEXMTX7 << 24); + auto bytes = cp_cmd(0x30, value); + + reset_gx_state(); + g_gxState.tcgs[0].mtx = GX_IDENTITY; + g_gxState.tcgs[1].mtx = GX_IDENTITY; + g_gxState.tcgs[2].mtx = GX_TEXMTX0; + g_gxState.tcgs[3].mtx = GX_IDENTITY; + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.currentPnMtx, 3u); + EXPECT_EQ(g_gxState.tcgs[0].mtx, GX_TEXMTX3); + EXPECT_EQ(g_gxState.tcgs[1].mtx, GX_TEXMTX4); + EXPECT_EQ(g_gxState.tcgs[2].mtx, GX_IDENTITY); + EXPECT_EQ(g_gxState.tcgs[3].mtx, GX_TEXMTX7); +} + +TEST_F(GXFifoTest, MatrixIndexB_DecodesTexMatricesFromCpPacket) { + const u32 value = (GX_TEXMTX4 << 0) | (GX_TEXMTX5 << 6) | (GX_IDENTITY << 12) | (GX_TEXMTX9 << 18); + auto bytes = cp_cmd(0x40, value); + + reset_gx_state(); + g_gxState.tcgs[4].mtx = GX_IDENTITY; + g_gxState.tcgs[5].mtx = GX_IDENTITY; + g_gxState.tcgs[6].mtx = GX_TEXMTX0; + g_gxState.tcgs[7].mtx = GX_IDENTITY; + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.tcgs[4].mtx, GX_TEXMTX4); + EXPECT_EQ(g_gxState.tcgs[5].mtx, GX_TEXMTX5); + EXPECT_EQ(g_gxState.tcgs[6].mtx, GX_IDENTITY); + EXPECT_EQ(g_gxState.tcgs[7].mtx, GX_TEXMTX9); +} + +// --- GXSetChanAmbColor / GXSetChanMatColor (XF 0x100A-0x100D) --- + +TEST_F(GXFifoTest, ChanAmbColor_Color0) { + GXColor amb = {64, 128, 192, 255}; + GXSetChanAmbColor(GX_COLOR0, amb); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& state = g_gxState.colorChannelState[GX_COLOR0]; + EXPECT_NEAR(state.ambColor[0], 64.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(state.ambColor[1], 128.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(state.ambColor[2], 192.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(state.ambColor[3], 255.f / 255.f, 1.f / 255.f); +} + +TEST_F(GXFifoTest, ChanMatColor_Color0) { + GXColor mat = {255, 0, 128, 64}; + GXSetChanMatColor(GX_COLOR0, mat); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& state = g_gxState.colorChannelState[GX_COLOR0]; + EXPECT_NEAR(state.matColor[0], 255.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(state.matColor[1], 0.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(state.matColor[2], 128.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(state.matColor[3], 64.f / 255.f, 1.f / 255.f); +} + +TEST_F(GXFifoTest, ChanAmbColor_Color1) { + GXColor amb = {10, 20, 30, 40}; + GXSetChanAmbColor(GX_COLOR1, amb); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& state = g_gxState.colorChannelState[GX_COLOR1]; + EXPECT_NEAR(state.ambColor[0], 10.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(state.ambColor[1], 20.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(state.ambColor[2], 30.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(state.ambColor[3], 40.f / 255.f, 1.f / 255.f); +} + +TEST_F(GXFifoTest, ChanMatColor_Color1) { + GXColor mat = {100, 150, 200, 250}; + GXSetChanMatColor(GX_COLOR1, mat); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& state = g_gxState.colorChannelState[GX_COLOR1]; + EXPECT_NEAR(state.matColor[0], 100.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(state.matColor[1], 150.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(state.matColor[2], 200.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(state.matColor[3], 250.f / 255.f, 1.f / 255.f); +} + +TEST_F(GXFifoTest, ChanAmbColor_Color0A0_Compound) { + // GX_COLOR0A0 should write to both COLOR0 and ALPHA0 XF registers + GXColor amb = {80, 160, 240, 128}; + GXSetChanAmbColor(GX_COLOR0A0, amb); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& stateC = g_gxState.colorChannelState[GX_COLOR0]; + EXPECT_NEAR(stateC.ambColor[0], 80.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(stateC.ambColor[1], 160.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(stateC.ambColor[2], 240.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(stateC.ambColor[3], 128.f / 255.f, 1.f / 255.f); + // ALPHA0 shares the same XF register as COLOR0, so should match + auto& stateA = g_gxState.colorChannelState[GX_ALPHA0]; + EXPECT_NEAR(stateA.ambColor[0], 80.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(stateA.ambColor[3], 128.f / 255.f, 1.f / 255.f); +} + +TEST_F(GXFifoTest, ChanMatColor_Color1A1_Compound) { + GXColor mat = {32, 64, 96, 128}; + GXSetChanMatColor(GX_COLOR1A1, mat); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& stateC = g_gxState.colorChannelState[GX_COLOR1]; + EXPECT_NEAR(stateC.matColor[0], 32.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(stateC.matColor[1], 64.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(stateC.matColor[2], 96.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(stateC.matColor[3], 128.f / 255.f, 1.f / 255.f); + + auto& stateA = g_gxState.colorChannelState[GX_ALPHA1]; + EXPECT_NEAR(stateA.matColor[0], 32.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(stateA.matColor[3], 128.f / 255.f, 1.f / 255.f); +} + +// GXSetFog (BP 0xEE-0xF2): fog A/B/C parameters, type and color + +// --- Fog with perspective linear fog, typical parameters --- +TEST_F(GXFifoTest, Fog_PerspLin_Typical) { + GXColor fogColor = {128, 200, 255, 255}; + GXSetFog(GX_FOG_PERSP_LIN, 100.f, 900.f, 0.1f, 1000.f, fogColor); + auto bytes = capture_fifo(); + + // Should produce 5 BP writes (0xEE-0xF2): 5 * 5 = 25 bytes + ASSERT_EQ(bytes.size(), 25u); + // Verify BP opcodes and register IDs + EXPECT_EQ(bytes[0], 0x61); + EXPECT_EQ(bytes[1], 0xEE); + EXPECT_EQ(bytes[5], 0x61); + EXPECT_EQ(bytes[6], 0xEF); + EXPECT_EQ(bytes[10], 0x61); + EXPECT_EQ(bytes[11], 0xF0); + EXPECT_EQ(bytes[15], 0x61); + EXPECT_EQ(bytes[16], 0xF1); + EXPECT_EQ(bytes[20], 0x61); + EXPECT_EQ(bytes[21], 0xF2); + + reset_gx_state(); + decode_fifo(bytes); + + // Compute expected A, B, C from the SDK formula + float nearZ = 0.1f, farZ = 1000.f, startZ = 100.f, endZ = 900.f; + float A = (farZ * nearZ) / ((farZ - nearZ) * (endZ - startZ)); + float B = farZ / (farZ - nearZ); + float C = startZ / (endZ - startZ); + + // Allow tolerance for encoding precision loss (11-bit mantissa) + EXPECT_NEAR(g_gxState.fog.a, A, std::abs(A) * 1e-3f); + EXPECT_NEAR(g_gxState.fog.b, B, std::abs(B) * 1e-3f); + EXPECT_NEAR(g_gxState.fog.c, C, std::abs(C) * 1e-3f); + expect_fog_raw_fields_match_decoded_state(); + EXPECT_EQ(g_gxState.fog.type, GX_FOG_PERSP_LIN); + EXPECT_NEAR(g_gxState.fog.color[0], 128.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.fog.color[1], 200.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.fog.color[2], 255.f / 255.f, 1.f / 255.f); +} + +// --- Orthographic fog keeps the projection bit and uses the SDK ortho coefficients --- +TEST_F(GXFifoTest, Fog_OrthoLin_Typical) { + GXColor fogColor = {128, 200, 255, 255}; + GXSetFog(GX_FOG_ORTHO_LIN, 100.f, 900.f, 0.1f, 1000.f, fogColor); + auto bytes = capture_fifo(); + + ASSERT_EQ(bytes.size(), 25u); + const u32 fog3 = (static_cast(bytes[16]) << 24) | (static_cast(bytes[17]) << 16) | + (static_cast(bytes[18]) << 8) | static_cast(bytes[19]); + EXPECT_EQ((fog3 >> 20) & 1u, 1u); + EXPECT_EQ((fog3 >> 21) & 7u, static_cast(GX_FOG_LIN)); + + reset_gx_state(); + decode_fifo(bytes); + + float nearZ = 0.1f, farZ = 1000.f, startZ = 100.f, endZ = 900.f; + float A = (farZ - nearZ) / (endZ - startZ); + float C = (startZ - nearZ) / (endZ - startZ); + + EXPECT_NEAR(g_gxState.fog.a, A, std::abs(A) * 1e-3f); + EXPECT_FLOAT_EQ(g_gxState.fog.b, 0.f); + EXPECT_NEAR(g_gxState.fog.c, C, std::abs(C) * 1e-3f); + expect_fog_raw_fields_match_decoded_state(); + EXPECT_EQ(g_gxState.fog.type, GX_FOG_ORTHO_LIN); +} + +// --- Fog with degenerate parameters (nearZ == farZ) --- +TEST_F(GXFifoTest, Fog_Degenerate_EqualDepths) { + GXColor fogColor = {0, 0, 0, 255}; + GXSetFog(GX_FOG_PERSP_EXP, 0.f, 100.f, 10.f, 10.f, fogColor); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + // When nearZ == farZ, SDK sets A=0, B=0.5, C=0 + EXPECT_FLOAT_EQ(g_gxState.fog.a, 0.f); + EXPECT_NEAR(g_gxState.fog.b, 0.5f, 1e-3f); + EXPECT_FLOAT_EQ(g_gxState.fog.c, 0.f); + expect_fog_raw_fields_match_decoded_state(); + EXPECT_EQ(g_gxState.fog.type, GX_FOG_PERSP_EXP); +} + +// --- Fog type: none --- +TEST_F(GXFifoTest, Fog_None) { + GXColor fogColor = {64, 64, 64, 255}; + GXSetFog(GX_FOG_NONE, 0.f, 0.f, 0.f, 0.f, fogColor); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.fog.type, GX_FOG_NONE); + EXPECT_FLOAT_EQ(g_gxState.fog.a, 0.f); + EXPECT_NEAR(g_gxState.fog.b, 0.5f, 1e-3f); + EXPECT_FLOAT_EQ(g_gxState.fog.c, 0.f); + expect_fog_raw_fields_match_decoded_state(); + EXPECT_NEAR(g_gxState.fog.color[0], 64.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.fog.color[1], 64.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.fog.color[2], 64.f / 255.f, 1.f / 255.f); +} + +// --- Fog with perspective reverse exponential squared type --- +TEST_F(GXFifoTest, Fog_PerspRevExp2) { + GXColor fogColor = {255, 0, 0, 255}; + GXSetFog(GX_FOG_PERSP_REVEXP2, 50.f, 500.f, 1.f, 1000.f, fogColor); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + float nearZ = 1.f, farZ = 1000.f, startZ = 50.f, endZ = 500.f; + float A = (farZ * nearZ) / ((farZ - nearZ) * (endZ - startZ)); + float B = farZ / (farZ - nearZ); + float C = startZ / (endZ - startZ); + + EXPECT_NEAR(g_gxState.fog.a, A, std::abs(A) * 1e-3f); + EXPECT_NEAR(g_gxState.fog.b, B, std::abs(B) * 1e-3f); + EXPECT_NEAR(g_gxState.fog.c, C, std::abs(C) * 1e-3f); + expect_fog_raw_fields_match_decoded_state(); + EXPECT_EQ(g_gxState.fog.type, GX_FOG_PERSP_REVEXP2); + EXPECT_NEAR(g_gxState.fog.color[0], 1.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.fog.color[1], 0.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.fog.color[2], 0.f, 1.f / 255.f); +} + +// GXSetIndTexMtx (BP 0x06-0x0E): indirect texture matrix parameters + +// --- IndTexMtx 0 with half-scale diagonal matrix --- +// Note: 11-bit signed range limits values to [-1.0, 0.999], so 1.0 is not representable. +TEST_F(GXFifoTest, IndTexMtx0_HalfScale) { + f32 mtx[2][3] = { + {0.5f, 0.0f, 0.0f}, + {0.0f, 0.5f, 0.0f}, + }; + GXSetIndTexMtx(GX_ITM_0, mtx, 0); + auto bytes = capture_fifo(); + + // Should produce 3 BP writes: 3 * 5 = 15 bytes + ASSERT_EQ(bytes.size(), 15u); + // Verify BP opcodes and register IDs (0x06, 0x07, 0x08 for matrix 0) + EXPECT_EQ(bytes[0], 0x61); + EXPECT_EQ(bytes[1], 0x06); + EXPECT_EQ(bytes[5], 0x61); + EXPECT_EQ(bytes[6], 0x07); + EXPECT_EQ(bytes[10], 0x61); + EXPECT_EQ(bytes[11], 0x08); + + reset_gx_state(); + decode_fifo(bytes); + + const auto& info = g_gxState.indTexMtxs[0]; + // 11-bit fixed-point (1/1024) precision + float tol = 1.0f / 1024.0f; + EXPECT_NEAR(info.mtx.m0.x, 0.5f, tol); + EXPECT_NEAR(info.mtx.m0.y, 0.0f, tol); + EXPECT_NEAR(info.mtx.m1.x, 0.0f, tol); + EXPECT_NEAR(info.mtx.m1.y, 0.5f, tol); + EXPECT_NEAR(info.mtx.m2.x, 0.0f, tol); + EXPECT_NEAR(info.mtx.m2.y, 0.0f, tol); + EXPECT_EQ(info.scaleExp, 0); +} + +// --- IndTexMtx 1 with fractional values and positive scale --- +TEST_F(GXFifoTest, IndTexMtx1_FractionalWithScale) { + f32 mtx[2][3] = { + {0.5f, 0.25f, -0.125f}, + {-0.5f, 0.75f, 0.0f}, + }; + GXSetIndTexMtx(GX_ITM_1, mtx, 3); + auto bytes = capture_fifo(); + + // Register IDs for matrix 1: 0x09, 0x0A, 0x0B + ASSERT_EQ(bytes.size(), 15u); + EXPECT_EQ(bytes[1], 0x09); + EXPECT_EQ(bytes[6], 0x0A); + EXPECT_EQ(bytes[11], 0x0B); + + reset_gx_state(); + decode_fifo(bytes); + + const auto& info = g_gxState.indTexMtxs[1]; + float tol = 1.0f / 1024.0f; + EXPECT_NEAR(info.mtx.m0.x, 0.5f, tol); + EXPECT_NEAR(info.mtx.m0.y, -0.5f, tol); + EXPECT_NEAR(info.mtx.m1.x, 0.25f, tol); + EXPECT_NEAR(info.mtx.m1.y, 0.75f, tol); + EXPECT_NEAR(info.mtx.m2.x, -0.125f, tol); + EXPECT_NEAR(info.mtx.m2.y, 0.0f, tol); + EXPECT_EQ(info.scaleExp, 3); +} + +// --- IndTexMtx 2 with negative scale exponent --- +TEST_F(GXFifoTest, IndTexMtx2_NegativeScale) { + f32 mtx[2][3] = { + {0.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 0.0f}, + }; + GXSetIndTexMtx(GX_ITM_2, mtx, -5); + auto bytes = capture_fifo(); + + // Register IDs for matrix 2: 0x0C, 0x0D, 0x0E + ASSERT_EQ(bytes.size(), 15u); + EXPECT_EQ(bytes[1], 0x0C); + EXPECT_EQ(bytes[6], 0x0D); + EXPECT_EQ(bytes[11], 0x0E); + + reset_gx_state(); + decode_fifo(bytes); + + const auto& info = g_gxState.indTexMtxs[2]; + EXPECT_EQ(info.scaleExp, -5); +} + +TEST_F(GXFifoTest, IndTexMtxScaleMultiplier_MatchesHardwareExponent) { + EXPECT_FLOAT_EQ(aurora::gx::indirect_matrix_scale_multiplier(0), 1.0f); + EXPECT_FLOAT_EQ(aurora::gx::indirect_matrix_scale_multiplier(3), 8.0f); + EXPECT_FLOAT_EQ(aurora::gx::indirect_matrix_scale_multiplier(-5), 0.03125f); +} + +TEST_F(GXFifoTest, IndTexMtxDynamicAliases_MapToSameSlotsAsSdk) { + f32 mtxS[2][3] = { + {0.25f, 0.0f, 0.0f}, + {0.0f, 0.25f, 0.0f}, + }; + f32 mtxT[2][3] = { + {0.5f, 0.0f, 0.0f}, + {0.0f, 0.5f, 0.0f}, + }; + + GXSetIndTexMtx(GX_ITM_S1, mtxS, 2); + GXSetIndTexMtx(GX_ITM_T2, mtxT, -3); + auto bytes = capture_fifo(); + + ASSERT_EQ(bytes.size(), 30u); + EXPECT_EQ(bytes[1], 0x09); + EXPECT_EQ(bytes[6], 0x0A); + EXPECT_EQ(bytes[11], 0x0B); + EXPECT_EQ(bytes[16], 0x0C); + EXPECT_EQ(bytes[21], 0x0D); + EXPECT_EQ(bytes[26], 0x0E); + + reset_gx_state(); + decode_fifo(bytes); + + constexpr float tol = 1.0f / 1024.0f; + EXPECT_NEAR(g_gxState.indTexMtxs[1].mtx.m0.x, 0.25f, tol); + EXPECT_NEAR(g_gxState.indTexMtxs[1].mtx.m1.y, 0.25f, tol); + EXPECT_EQ(g_gxState.indTexMtxs[1].scaleExp, 2); + EXPECT_NEAR(g_gxState.indTexMtxs[2].mtx.m0.x, 0.5f, tol); + EXPECT_NEAR(g_gxState.indTexMtxs[2].mtx.m1.y, 0.5f, tol); + EXPECT_EQ(g_gxState.indTexMtxs[2].scaleExp, -3); +} + +// --- IndTexMtx 0 does not affect matrix 1 --- +TEST_F(GXFifoTest, IndTexMtx0_Isolation) { + f32 mtx0[2][3] = { + {0.5f, 0.0f, 0.0f}, + {0.0f, 0.5f, 0.0f}, + }; + f32 mtx1[2][3] = { + {-1.0f, 0.0f, 0.0f}, + {0.0f, -1.0f, 0.0f}, + }; + GXSetIndTexMtx(GX_ITM_0, mtx0, 1); + GXSetIndTexMtx(GX_ITM_1, mtx1, -2); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + float tol = 1.0f / 1024.0f; + // Matrix 0 + EXPECT_NEAR(g_gxState.indTexMtxs[0].mtx.m0.x, 0.5f, tol); + EXPECT_NEAR(g_gxState.indTexMtxs[0].mtx.m1.y, 0.5f, tol); + EXPECT_EQ(g_gxState.indTexMtxs[0].scaleExp, 1); + // Matrix 1 + EXPECT_NEAR(g_gxState.indTexMtxs[1].mtx.m0.x, -1.0f, tol); + EXPECT_NEAR(g_gxState.indTexMtxs[1].mtx.m1.y, -1.0f, tol); + EXPECT_EQ(g_gxState.indTexMtxs[1].scaleExp, -2); +} + +// SU texture coordinate scale (BP 0x30-0x3F) + +// --- GXSetTexCoordScaleManually sets width/height --- +TEST_F(GXFifoTest, TexCoordScale_Manual_Coord0) { + GXSetTexCoordScaleManually(GX_TEXCOORD0, GX_TRUE, 256, 128); + auto bytes = capture_fifo(); + + // Two BP writes (suTs0 + suTs1): 2 * 5 = 10 bytes + ASSERT_EQ(bytes.size(), 10u); + EXPECT_EQ(bytes[0], 0x61); + EXPECT_EQ(bytes[1], 0x30); // suTs0[0] + EXPECT_EQ(bytes[5], 0x61); + EXPECT_EQ(bytes[6], 0x31); // suTs1[0] + + reset_gx_state(); + decode_fifo(bytes); + + const auto& tcs = g_gxState.texCoordScales[0]; + EXPECT_EQ(tcs.scaleS, 255u); // width - 1 + EXPECT_EQ(tcs.scaleT, 127u); // height - 1 +} + +// --- GXSetTexCoordScaleManually for coord 3 --- +TEST_F(GXFifoTest, TexCoordScale_Manual_Coord3) { + GXSetTexCoordScaleManually(GX_TEXCOORD3, GX_TRUE, 512, 512); + auto bytes = capture_fifo(); + + ASSERT_EQ(bytes.size(), 10u); + EXPECT_EQ(bytes[1], 0x36); // suTs0[3] = 0x30 + 3*2 + EXPECT_EQ(bytes[6], 0x37); // suTs1[3] = 0x31 + 3*2 + + reset_gx_state(); + decode_fifo(bytes); + + const auto& tcs = g_gxState.texCoordScales[3]; + EXPECT_EQ(tcs.scaleS, 511u); + EXPECT_EQ(tcs.scaleT, 511u); +} + +// --- GXSetTexCoordScaleManually with bias and cyl wrap --- +TEST_F(GXFifoTest, TexCoordScale_BiasAndCylWrap) { + // Enable manual mode first, then set bias and cyl wrap + GXSetTexCoordScaleManually(GX_TEXCOORD0, GX_TRUE, 64, 64); + capture_fifo(); // discard + + GXSetTexCoordBias(GX_TEXCOORD0, GX_TRUE, GX_FALSE); + auto biasBytes = capture_fifo(); + + GXSetTexCoordCylWrap(GX_TEXCOORD0, GX_FALSE, GX_TRUE); + auto cylBytes = capture_fifo(); + + // Each writes 2 BP regs + ASSERT_EQ(biasBytes.size(), 10u); + ASSERT_EQ(cylBytes.size(), 10u); + + reset_gx_state(); + decode_fifo(biasBytes); + decode_fifo(cylBytes); + + const auto& tcs = g_gxState.texCoordScales[0]; + EXPECT_TRUE(tcs.biasS); + EXPECT_FALSE(tcs.biasT); + EXPECT_FALSE(tcs.cylWrapS); + EXPECT_TRUE(tcs.cylWrapT); +} + +// --- GXEnableTexOffsets --- +TEST_F(GXFifoTest, TexCoordScale_TexOffsets) { + GXEnableTexOffsets(GX_TEXCOORD2, GX_TRUE, GX_TRUE); + auto bytes = capture_fifo(); + + // One BP write (suTs0 only): 5 bytes + ASSERT_EQ(bytes.size(), 5u); + EXPECT_EQ(bytes[1], 0x34); // suTs0[2] = 0x30 + 2*2 + + reset_gx_state(); + decode_fifo(bytes); + + const auto& tcs = g_gxState.texCoordScales[2]; + EXPECT_TRUE(tcs.lineOffset); + EXPECT_TRUE(tcs.pointOffset); +} + +TEST_F(GXFifoTest, TexCoordScale_TexOffsets_Disabled) { + GXEnableTexOffsets(GX_TEXCOORD2, GX_FALSE, GX_FALSE); + auto bytes = capture_fifo(); + + ASSERT_EQ(bytes.size(), 5u); + EXPECT_EQ(bytes[1], 0x34); + + reset_gx_state(); + g_gxState.texCoordScales[2].lineOffset = true; + g_gxState.texCoordScales[2].pointOffset = true; + decode_fifo(bytes); + + const auto& tcs = g_gxState.texCoordScales[2]; + EXPECT_FALSE(tcs.lineOffset); + EXPECT_FALSE(tcs.pointOffset); +} + +// --- Coord isolation: writing coord 0 doesn't affect coord 1 --- +TEST_F(GXFifoTest, TexCoordScale_Isolation) { + GXSetTexCoordScaleManually(GX_TEXCOORD0, GX_TRUE, 100, 200); + GXSetTexCoordScaleManually(GX_TEXCOORD1, GX_TRUE, 300, 400); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.texCoordScales[0].scaleS, 99u); + EXPECT_EQ(g_gxState.texCoordScales[0].scaleT, 199u); + EXPECT_EQ(g_gxState.texCoordScales[1].scaleS, 299u); + EXPECT_EQ(g_gxState.texCoordScales[1].scaleT, 399u); +} + +// GXSetCopyClear (BP 0x4F-0x51): clear color and depth + +TEST_F(GXFifoTest, DispCopyState_EncodesBpAndYScale) { + GXSetDispCopySrc(4, 8, 640, 480); + GXSetDispCopyDst(640, 480); + const u32 lines = GXSetDispCopyYScale(1.0f); + auto bytes = capture_fifo(); + + EXPECT_EQ(lines, 480u); + EXPECT_TRUE(has_bp_write(bytes, 0x49)); + EXPECT_TRUE(has_bp_write(bytes, 0x4A)); + EXPECT_TRUE(has_bp_write(bytes, 0x4D)); + EXPECT_TRUE(has_bp_write(bytes, 0x4E)); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.dispCopySrc.x, 4); + EXPECT_EQ(g_gxState.dispCopySrc.y, 8); + EXPECT_EQ(g_gxState.dispCopySrc.width, 640); + EXPECT_EQ(g_gxState.dispCopySrc.height, 480); + EXPECT_NEAR(g_gxState.dispCopyYScale, 1.0f, 0.001f); +} + +TEST_F(GXFifoTest, DispCopyYScaleHelpers_MatchIdentityScale) { + EXPECT_EQ(GXGetNumXfbLines(480, 1.0f), 480u); + EXPECT_NEAR(GXGetYScaleFactor(480, 480), 1.0f, 0.001f); +} + +TEST_F(GXFifoTest, CopyFilter_EncodesAndDecodesAaAndVerticalFilter) { + u8 samples[12][2] = {}; + for (size_t i = 0; i < 12; ++i) { + samples[i][0] = static_cast((i * 2) & 0x0f); + samples[i][1] = static_cast((i * 2 + 1) & 0x0f); + } + u8 vfilter[7] = {1, 2, 3, 4, 5, 6, 7}; + + GXSetCopyFilter(GX_TRUE, samples, GX_TRUE, vfilter); + auto bytes = capture_fifo(); + + ASSERT_EQ(bytes.size(), 30u); + EXPECT_EQ(bytes[1], 0x01); + EXPECT_EQ(bytes[6], 0x02); + EXPECT_EQ(bytes[11], 0x03); + EXPECT_EQ(bytes[16], 0x04); + EXPECT_EQ(bytes[21], 0x53); + EXPECT_EQ(bytes[26], 0x54); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_TRUE(g_gxState.copyFilterAa); + EXPECT_TRUE(g_gxState.copyFilterVf); + for (size_t i = 0; i < 12; ++i) { + EXPECT_EQ(g_gxState.copyFilterSamplePattern[i][0], samples[i][0]); + EXPECT_EQ(g_gxState.copyFilterSamplePattern[i][1], samples[i][1]); + } + for (size_t i = 0; i < 7; ++i) { + EXPECT_EQ(g_gxState.copyFilterVFilter[i], vfilter[i]); + } +} + +TEST_F(GXFifoTest, CopyFilter_DisabledUsesSdkFallbackCoefficients) { + u8 samples[12][2] = {}; + u8 vfilter[7] = {}; + + GXSetCopyFilter(GX_FALSE, samples, GX_FALSE, vfilter); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_FALSE(g_gxState.copyFilterAa); + EXPECT_FALSE(g_gxState.copyFilterVf); + for (const auto& sample : g_gxState.copyFilterSamplePattern) { + EXPECT_EQ(sample[0], 6u); + EXPECT_EQ(sample[1], 6u); + } + const std::array expectedVFilter{0, 0, 21, 22, 21, 0, 0}; + EXPECT_EQ(g_gxState.copyFilterVFilter, expectedVFilter); +} + +TEST_F(GXFifoTest, DispCopyGamma_UpdatesCopyTriggerShadowBits) { + GXSetDispCopyGamma(GX_GM_2_2); + + EXPECT_EQ(g_gxState.dispCopyGamma, GX_GM_2_2); + EXPECT_EQ((g_gxState.bpRegCache[0x52] >> 7) & 3u, static_cast(GX_GM_2_2)); +} + +TEST_F(GXFifoTest, CopyTrigger_DecodesClampGammaFormatHalfScaleAndFrameMode) { + const u32 value = (static_cast(GX_CLAMP_TOP) << 0) | (static_cast(GX_TF_RGBA8) << 3) | + (static_cast(GX_GM_1_7) << 7) | (1u << 9) | (2u << 12); + auto bytes = bp_cmd(0x52, value); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.copyClamp, GX_CLAMP_TOP); + EXPECT_EQ(g_gxState.texCopyFmt, GX_TF_RGBA8); + EXPECT_EQ(g_gxState.dispCopyGamma, GX_GM_1_7); + EXPECT_TRUE(g_gxState.texCopyHalfScale); + EXPECT_EQ(g_gxState.dispCopyFrame2Field, 2u); +} + +TEST_F(GXFifoTest, CopyTexClearTruePassesScratchRectAndUpdateMasksToResolve) { + std::array image{}; + gxState().pixelFmt = GX_PF_RGBA6_Z24; + gxState().colorUpdate = true; + gxState().alphaUpdate = true; + gxState().depthUpdate = true; + gxState().dstAlpha = UINT32_MAX; + gxState().clearColor = {64.f / 255.f, 128.f / 255.f, 192.f / 255.f, 32.f / 255.f}; + gxState().clearDepth = 0x123456; + + GXSetTexCopySrc(336, 300, 152, 114); + GXSetTexCopyDst(152, 114, GX_TF_RGBA8, GX_FALSE); + GXCopyTex(image.data(), GX_TRUE); + + const auto& records = aurora::gfx::testing::resolve_pass_records(); + ASSERT_EQ(records.size(), 1u); + const auto& resolve = records.front(); + EXPECT_EQ(resolve.rect, (aurora::gfx::ClipRect{336, 300, 152, 114})); + ASSERT_TRUE(resolve.sourceRectPixels.has_value()); + EXPECT_EQ(resolve.sourceRectPixels->x(), 336.f); + EXPECT_EQ(resolve.sourceRectPixels->y(), 300.f); + EXPECT_EQ(resolve.sourceRectPixels->z(), 152.f); + EXPECT_EQ(resolve.sourceRectPixels->w(), 114.f); + EXPECT_TRUE(resolve.clearColor); + EXPECT_TRUE(resolve.clearAlpha); + EXPECT_TRUE(resolve.clearDepth); + EXPECT_NEAR(resolve.clearColorValue.x(), 64.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(resolve.clearColorValue.y(), 128.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(resolve.clearColorValue.z(), 192.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(resolve.clearColorValue.w(), 32.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(resolve.clearDepthValue, 0x123456 / 16777216.f, 1.f / 16777216.f); + EXPECT_EQ(resolve.resolveFormat, GX_TF_RGBA8); + EXPECT_FALSE(resolve.halfScale); + EXPECT_FALSE(resolve.forceOpaqueAlpha); + EXPECT_EQ(gxState().copyTextures.at(image.data()).revision, 1u); +} + +TEST_F(GXFifoTest, CopyTexColorFormatMarksResolvePersistent) { + std::array image{}; + gxState().pixelFmt = GX_PF_RGBA6_Z24; + + GXSetTexCopySrc(336, 300, 152, 114); + GXSetTexCopyDst(152, 114, GX_TF_RGBA8, GX_FALSE); + GXCopyTex(image.data(), GX_FALSE); + + const auto& records = aurora::gfx::testing::resolve_pass_records(); + ASSERT_EQ(records.size(), 1u); + EXPECT_TRUE(records.front().persistentCopy); +} + +TEST_F(GXFifoTest, RecurringColorCopyKeepsLaterResolveSkippable) { + std::array image{}; + gxState().pixelFmt = GX_PF_RGBA6_Z24; + + GXSetTexCopySrc(336, 300, 152, 114); + GXSetTexCopyDst(152, 114, GX_TF_RGBA8, GX_FALSE); + aurora::gfx::testing::set_current_frame(10); + GXCopyTex(image.data(), GX_FALSE); + aurora::gfx::testing::set_current_frame(11); + GXCopyTex(image.data(), GX_FALSE); + + const auto& records = aurora::gfx::testing::resolve_pass_records(); + ASSERT_EQ(records.size(), 2u); + EXPECT_TRUE(records[0].persistentCopy); + EXPECT_FALSE(records[1].persistentCopy); +} + +TEST_F(GXFifoTest, ColorCopyAfterFrameGapRegainsPersistentProtection) { + std::array image{}; + gxState().pixelFmt = GX_PF_RGBA6_Z24; + + GXSetTexCopySrc(336, 300, 152, 114); + GXSetTexCopyDst(152, 114, GX_TF_RGBA8, GX_FALSE); + aurora::gfx::testing::set_current_frame(10); + GXCopyTex(image.data(), GX_FALSE); + aurora::gfx::testing::set_current_frame(12); + GXCopyTex(image.data(), GX_FALSE); + + const auto& records = aurora::gfx::testing::resolve_pass_records(); + ASSERT_EQ(records.size(), 2u); + EXPECT_TRUE(records[0].persistentCopy); + EXPECT_TRUE(records[1].persistentCopy); +} + +TEST_F(GXFifoTest, CopyTexDepthFormatKeepsResolveSkippable) { + std::array image{}; + gxState().pixelFmt = GX_PF_RGBA6_Z24; + + GXSetTexCopySrc(0, 0, 4, 4); + GXSetTexCopyDst(4, 4, GX_TF_Z24X8, GX_FALSE); + GXCopyTex(image.data(), GX_FALSE); + + const auto& records = aurora::gfx::testing::resolve_pass_records(); + ASSERT_EQ(records.size(), 1u); + EXPECT_FALSE(records.front().persistentCopy); +} + +TEST_F(GXFifoTest, CopyDispResolveIsNotPersistent) { + GXSetDispCopySrc(0, 0, 32, 32); + GXSetDispCopyDst(32, 32); + GXCopyDisp(nullptr, GX_FALSE); + + const auto& records = aurora::gfx::testing::resolve_pass_records(); + ASSERT_EQ(records.size(), 1u); + EXPECT_FALSE(records.front().persistentCopy); +} + +TEST_F(GXFifoTest, CopyTexMipmapDstRequestsHalfScaleResolve) { + std::array image{}; + gxState().pixelFmt = GX_PF_RGBA6_Z24; + + GXSetTexCopySrc(336, 300, 152, 114); + GXSetTexCopyDst(76, 57, GX_TF_RGBA8, GX_TRUE); + GXCopyTex(image.data(), GX_FALSE); + + const auto& records = aurora::gfx::testing::resolve_pass_records(); + ASSERT_EQ(records.size(), 1u); + const auto& resolve = records.front(); + EXPECT_EQ(resolve.rect, (aurora::gfx::ClipRect{336, 300, 152, 114})); + ASSERT_TRUE(resolve.sourceRectPixels.has_value()); + EXPECT_EQ(resolve.sourceRectPixels->x(), 336.f); + EXPECT_EQ(resolve.sourceRectPixels->y(), 300.f); + EXPECT_EQ(resolve.sourceRectPixels->z(), 152.f); + EXPECT_EQ(resolve.sourceRectPixels->w(), 114.f); + EXPECT_TRUE(resolve.halfScale); + EXPECT_TRUE(gxState().texCopyHalfScale); + EXPECT_EQ(gxState().copyTextures.at(image.data()).width, 76u); + EXPECT_EQ(gxState().copyTextures.at(image.data()).height, 57u); +} + +TEST_F(GXFifoTest, CopyTexRetainsInternalResolutionInGpuCopyAndGuestDimensionsInCache) { + std::array image{}; + aurora::gfx::testing::set_framebuffer_sizes(640, 528, 2560, 2112); + gxState().pixelFmt = GX_PF_RGBA6_Z24; + + GXSetTexCopySrc(336, 300, 152, 114); + GXSetTexCopyDst(152, 114, GX_TF_RGBA8, GX_FALSE); + GXCopyTex(image.data(), GX_FALSE); + + const auto& records = aurora::gfx::testing::resolve_pass_records(); + ASSERT_EQ(records.size(), 1u); + const auto& resolve = records.front(); + EXPECT_EQ(resolve.rect, (aurora::gfx::ClipRect{1344, 1200, 608, 456})); + ASSERT_TRUE(resolve.sourceRectPixels.has_value()); + EXPECT_EQ(resolve.sourceRectPixels->x(), 1344.f); + EXPECT_EQ(resolve.sourceRectPixels->y(), 1200.f); + EXPECT_EQ(resolve.sourceRectPixels->z(), 608.f); + EXPECT_EQ(resolve.sourceRectPixels->w(), 456.f); + ASSERT_TRUE(resolve.texture); + EXPECT_EQ(resolve.texture->size.width, 608u); + EXPECT_EQ(resolve.texture->size.height, 456u); + EXPECT_FLOAT_EQ(resolve.copyFilterRowStride, 4.0f); + + const auto& copy = gxState().copyTextures.at(image.data()); + EXPECT_EQ(copy.width, 152u); + EXPECT_EQ(copy.height, 114u); + EXPECT_EQ(copy.dataSize, GXGetTexBufferSize(152, 114, GX_TF_RGBA8, GX_FALSE, 0)); +} + +TEST_F(GXFifoTest, CopyTexUsesExactRationalScaledEdgesBeforeClearing) { + std::array image{}; + // float32 evaluates 7 * (62 / 14) just below 31. Without stabilizing + // the rational copy edges, floor() expands this clear to begin at x/y 30. + aurora::gfx::testing::set_framebuffer_sizes(14, 14, 62, 62); + gxState().pixelFmt = GX_PF_RGBA6_Z24; + + GXSetTexCopySrc(7, 7, 7, 7); + GXSetTexCopyDst(31, 31, GX_TF_RGBA8, GX_FALSE); + GXCopyTex(image.data(), GX_TRUE); + + const auto& records = aurora::gfx::testing::resolve_pass_records(); + ASSERT_EQ(records.size(), 1u); + const auto& resolve = records.front(); + EXPECT_EQ(resolve.rect, (aurora::gfx::ClipRect{31, 31, 31, 31})); + ASSERT_TRUE(resolve.sourceRectPixels.has_value()); + EXPECT_FLOAT_EQ(resolve.sourceRectPixels->x(), 31.0f); + EXPECT_FLOAT_EQ(resolve.sourceRectPixels->y(), 31.0f); + EXPECT_FLOAT_EQ(resolve.sourceRectPixels->z(), 31.0f); + EXPECT_FLOAT_EQ(resolve.sourceRectPixels->w(), 31.0f); +} + +TEST_F(GXFifoTest, CopyTexClearDoesNotOverlapFractionalLeftEdge) { + std::array image{}; + // MKW split-screen scratch-copy geometry from the regression capture: the scaled source edge is + // x=492.975, so flooring clears column 492 while the matching viewport rasterizes from 493. + aurora::gfx::testing::set_framebuffer_sizes(640, 528, 939, 528); + gxState().pixelFmt = GX_PF_RGBA6_Z24; + + GXSetTexCopySrc(336, 300, 152, 114); + GXSetTexCopyDst(152, 114, GX_TF_RGBA8, GX_FALSE); + GXCopyTex(image.data(), GX_TRUE); + + const auto& records = aurora::gfx::testing::resolve_pass_records(); + ASSERT_EQ(records.size(), 1u); + const auto& resolve = records.front(); + EXPECT_EQ(resolve.rect, (aurora::gfx::ClipRect{493, 300, 223, 114})); + ASSERT_TRUE(resolve.sourceRectPixels.has_value()); + EXPECT_FLOAT_EQ(resolve.sourceRectPixels->x(), 492.975f); + EXPECT_FLOAT_EQ(resolve.sourceRectPixels->y(), 300.0f); + EXPECT_FLOAT_EQ(resolve.sourceRectPixels->z(), 223.0125f); + EXPECT_FLOAT_EQ(resolve.sourceRectPixels->w(), 114.0f); +} + +TEST_F(GXFifoTest, CopyTexClearBoundsRemainExactAtSixTimesInternalResolution) { + std::array image{}; + aurora::gfx::testing::set_framebuffer_sizes(640, 528, 3840, 3168); + gxState().pixelFmt = GX_PF_RGBA6_Z24; + + GXSetTexCopySrc(336, 300, 152, 114); + GXSetTexCopyDst(152, 114, GX_TF_RGBA8, GX_FALSE); + GXCopyTex(image.data(), GX_TRUE); + + const auto& records = aurora::gfx::testing::resolve_pass_records(); + ASSERT_EQ(records.size(), 1u); + const auto& resolve = records.front(); + EXPECT_EQ(resolve.rect, (aurora::gfx::ClipRect{2016, 1800, 912, 684})); + ASSERT_TRUE(resolve.sourceRectPixels.has_value()); + EXPECT_FLOAT_EQ(resolve.sourceRectPixels->x(), 2016.0f); + EXPECT_FLOAT_EQ(resolve.sourceRectPixels->y(), 1800.0f); + EXPECT_FLOAT_EQ(resolve.sourceRectPixels->z(), 912.0f); + EXPECT_FLOAT_EQ(resolve.sourceRectPixels->w(), 684.0f); +} + +TEST_F(GXFifoTest, CopyTexRecreatesGpuCopyWhenInternalResolutionChanges) { + std::array image{}; + gxState().pixelFmt = GX_PF_RGBA6_Z24; + GXSetTexCopySrc(0, 0, 64, 64); + GXSetTexCopyDst(64, 64, GX_TF_RGBA8, GX_FALSE); + + GXCopyTex(image.data(), GX_FALSE); + const auto* nativeHandle = gxState().copyTextures.at(image.data()).handle.get(); + ASSERT_NE(nativeHandle, nullptr); + EXPECT_EQ(nativeHandle->size.width, 64u); + + aurora::gfx::testing::set_framebuffer_sizes(640, 480, 1280, 960); + GXCopyTex(image.data(), GX_FALSE); + + const auto& scaledCopy = gxState().copyTextures.at(image.data()); + ASSERT_TRUE(scaledCopy.handle); + EXPECT_NE(scaledCopy.handle.get(), nativeHandle); + EXPECT_EQ(scaledCopy.handle->size.width, 128u); + EXPECT_EQ(scaledCopy.handle->size.height, 128u); + EXPECT_EQ(scaledCopy.width, 64u); + EXPECT_EQ(scaledCopy.height, 64u); + EXPECT_EQ(scaledCopy.revision, 2u); +} + +TEST_F(GXFifoTest, CopyTexPassesVerticalCopyFilterCoefficientsToResolve) { + std::array image{}; + gxState().pixelFmt = GX_PF_RGBA6_Z24; + u8 vfilter[7] = {3, 5, 7, 11, 13, 17, 19}; + + GXSetCopyFilter(GX_FALSE, nullptr, GX_TRUE, vfilter); + GXSetTexCopySrc(0, 0, 32, 32); + GXSetTexCopyDst(32, 32, GX_TF_RGBA8, GX_FALSE); + GXCopyTex(image.data(), GX_FALSE); + + const auto& records = aurora::gfx::testing::resolve_pass_records(); + ASSERT_EQ(records.size(), 1u); + EXPECT_EQ(records.front().copyFilterCoefficients, (std::array{8, 31, 36})); +} + +TEST_F(GXFifoTest, CopyTexPassesVerticalCopyClampToResolve) { + std::array image{}; + gxState().pixelFmt = GX_PF_RGBA6_Z24; + + GXSetCopyClamp(GX_CLAMP_TOP); + GXSetTexCopySrc(8, 12, 32, 32); + GXSetTexCopyDst(32, 32, GX_TF_RGBA8, GX_FALSE); + GXCopyTex(image.data(), GX_FALSE); + + const auto& records = aurora::gfx::testing::resolve_pass_records(); + ASSERT_EQ(records.size(), 1u); + EXPECT_TRUE(records.front().clampTop); + EXPECT_FALSE(records.front().clampBottom); +} + +TEST_F(GXFifoTest, CopyDispCanDisableSpatialCopyFilterWithoutChangingBrightness) { + u8 vfilter[7] = {3, 5, 7, 11, 13, 17, 19}; + aurora::g_config.disableCopyFilter = true; + + GXSetCopyFilter(GX_FALSE, nullptr, GX_TRUE, vfilter); + GXSetDispCopySrc(0, 0, 32, 32); + GXSetDispCopyDst(32, 32); + GXCopyDisp(nullptr, GX_FALSE); + + const auto& records = aurora::gfx::testing::resolve_pass_records(); + ASSERT_EQ(records.size(), 1u); + EXPECT_EQ(records.front().copyFilterCoefficients, (std::array{0, 75, 0})); +} + +TEST_F(GXFifoTest, CopyTexDepthZ16PreservesVerticalCopyFilterForConversion) { + std::array image{}; + gxState().pixelFmt = GX_PF_RGBA6_Z24; + u8 vfilter[7] = {21, 0, 0, 22, 0, 21, 0}; + + GXSetCopyFilter(GX_FALSE, nullptr, GX_TRUE, vfilter); + EXPECT_TRUE(gxState().copyFilterVf); + EXPECT_EQ(gxState().copyFilterVFilter, (std::array{21, 0, 0, 22, 0, 21, 0})); + GXSetTexCopySrc(0, 0, 32, 32); + GXSetTexCopyDst(32, 32, GX_TF_Z16, GX_FALSE); + EXPECT_EQ(gxState().copyFilterVFilter, (std::array{21, 0, 0, 22, 0, 21, 0})); + GXCopyTex(image.data(), GX_FALSE); + EXPECT_EQ(gxState().copyFilterVFilter, (std::array{21, 0, 0, 22, 0, 21, 0})); + + const auto& records = aurora::gfx::testing::resolve_pass_records(); + ASSERT_EQ(records.size(), 1u); + EXPECT_EQ(records.front().resolveFormat, GX_TF_Z16); + EXPECT_EQ(records.front().copyFilterCoefficients, (std::array{21, 22, 21})); + EXPECT_FALSE(records.front().forceOpaqueAlpha); + EXPECT_EQ(gxState().copyTextures.at(image.data()).format, GX_TF_Z16); +} + +TEST_F(GXFifoTest, CopyTexDepthZ16UsesRa8Ia8Semantics) { + constexpr auto rgba = aurora::gfx::tex_copy_conv::detail::z16_ra8_as_ia8_rgba(0x12); + EXPECT_EQ(rgba, (std::array{0x12, 0x12, 0x12, 0xff})); + + constexpr auto shader = aurora::gfx::tex_copy_conv::detail::Z16FragmentShader; + EXPECT_NE(shader.find("let i = f32(depth_bytes.r)"), std::string_view::npos); + EXPECT_NE(shader.find("return vec4f(i, i, i, 1.0)"), std::string_view::npos); +} + +TEST_F(GXFifoTest, CopyTexClearTrueNoOpsWhenAllUpdateMasksAreDisabled) { + std::array image{}; + gxState().pixelFmt = GX_PF_RGBA6_Z24; + gxState().colorUpdate = false; + gxState().alphaUpdate = false; + gxState().depthUpdate = false; + gxState().dstAlpha = UINT32_MAX; + + GXSetTexCopySrc(8, 16, 64, 64); + GXSetTexCopyDst(64, 64, GX_TF_RGBA8, GX_FALSE); + GXCopyTex(image.data(), GX_TRUE); + + const auto& records = aurora::gfx::testing::resolve_pass_records(); + ASSERT_EQ(records.size(), 1u); + EXPECT_FALSE(records.front().clearColor); + EXPECT_FALSE(records.front().clearAlpha); + EXPECT_FALSE(records.front().clearDepth); +} + +TEST_F(GXFifoTest, CopyTexClearTrueRgbTargetKeepsFormatAndForcesOpaqueCopiedAlpha) { + std::array image{}; + gxState().pixelFmt = GX_PF_RGB8_Z24; + gxState().colorUpdate = true; + gxState().alphaUpdate = false; + gxState().depthUpdate = false; + gxState().dstAlpha = UINT32_MAX; + gxState().clearColor = {0.2f, 0.4f, 0.6f, 0.25f}; + + GXSetTexCopySrc(0, 0, 32, 32); + GXSetTexCopyDst(32, 32, GX_TF_RGBA8, GX_FALSE); + GXCopyTex(image.data(), GX_TRUE); + + const auto& records = aurora::gfx::testing::resolve_pass_records(); + ASSERT_EQ(records.size(), 1u); + EXPECT_TRUE(records.front().clearColor); + EXPECT_FALSE(records.front().clearAlpha); + EXPECT_FALSE(records.front().clearDepth); + EXPECT_NEAR(records.front().clearColorValue.x(), 0.2f, 1.f / 255.f); + EXPECT_NEAR(records.front().clearColorValue.y(), 0.4f, 1.f / 255.f); + EXPECT_NEAR(records.front().clearColorValue.z(), 0.6f, 1.f / 255.f); + EXPECT_EQ(records.front().clearColorValue.w(), 0.25f); + EXPECT_EQ(records.front().resolveFormat, GX_TF_RGBA8); + EXPECT_TRUE(records.front().forceOpaqueAlpha); +} + +TEST_F(GXFifoTest, CopyTexSameTargetReusesHostTextureUntilSampledThisFrame) { + std::array image{}; + gxState().pixelFmt = GX_PF_RGBA6_Z24; + + GXSetTexCopySrc(0, 0, 64, 64); + GXSetTexCopyDst(64, 64, GX_TF_RGBA8, GX_FALSE); + GXCopyTex(image.data(), GX_FALSE); + + const auto& first = gxState().copyTextures.at(image.data()); + ASSERT_TRUE(first.handle); + const auto* firstHandle = first.handle.get(); + EXPECT_EQ(first.revision, 1u); + + GXCopyTex(image.data(), GX_FALSE); + + const auto& second = gxState().copyTextures.at(image.data()); + EXPECT_EQ(second.handle.get(), firstHandle); + EXPECT_EQ(second.revision, 2u); +} + +TEST_F(GXFifoTest, CopyTexSameFrameSampledTargetGetsFreshHostTexture) { + std::array image{}; + gxState().pixelFmt = GX_PF_RGBA6_Z24; + + GXSetTexCopySrc(0, 0, 64, 64); + GXSetTexCopyDst(64, 64, GX_TF_RGBA8, GX_FALSE); + GXCopyTex(image.data(), GX_FALSE); + + const aurora::gx::GXState::CopyTextureKey key{ + .dest = image.data(), + .width = 64, + .height = 64, + .format = GX_TF_RGBA8, + }; + const auto& first = gxState().copyTextures.at(image.data()); + ASSERT_TRUE(first.handle); + const auto* firstHandle = first.handle.get(); + + auto& cached = gxState().copyTextureCache.at(key); + cached.lastSampledFrame = aurora::gfx::current_frame(); + cached.sampledThisFrame = true; + + GXCopyTex(image.data(), GX_FALSE); + + const auto& second = gxState().copyTextures.at(image.data()); + ASSERT_TRUE(second.handle); + EXPECT_NE(second.handle.get(), firstHandle); + EXPECT_EQ(second.revision, 2u); +} + +TEST_F(GXFifoTest, CopyTexSameTargetDifferentShapeRetiresHistoricalCacheEntry) { + std::vector image(608 * 456 * 4); + gxState().pixelFmt = GX_PF_RGBA6_Z24; + + GXSetTexCopySrc(0, 0, 608, 456); + GXSetTexCopyDst(608, 456, GX_TF_RGB565, GX_FALSE); + GXCopyTex(image.data(), GX_FALSE); + + const aurora::gx::GXState::CopyTextureKey fullKey{ + .dest = image.data(), + .width = 608, + .height = 456, + .format = GX_TF_RGB565, + }; + const auto& fullCopy = gxState().copyTextureCache.at(fullKey); + ASSERT_TRUE(fullCopy.handle); + const auto* fullHandle = fullCopy.handle.get(); + + GXSetTexCopySrc(0, 0, 304, 228); + GXSetTexCopyDst(304, 228, GX_TF_RGBA8, GX_TRUE); + GXCopyTex(image.data(), GX_FALSE); + + const aurora::gx::GXState::CopyTextureKey halfKey{ + .dest = image.data(), + .width = 304, + .height = 228, + .format = GX_TF_RGBA8, + }; + const auto& halfCopy = gxState().copyTextureCache.at(halfKey); + ASSERT_TRUE(halfCopy.handle); + + EXPECT_FALSE(gxState().copyTextureCache.contains(fullKey)); + EXPECT_EQ(gxState().copyTextureCache.size(), 1u); + EXPECT_NE(halfCopy.handle.get(), fullHandle); + EXPECT_EQ(gxState().copyTextures.at(image.data()).handle.get(), halfCopy.handle.get()); + EXPECT_EQ(gxState().copyTextures.at(image.data()).width, 304u); + EXPECT_EQ(gxState().copyTextures.at(image.data()).height, 228u); + EXPECT_EQ(gxState().copyTextures.at(image.data()).format, GX_TF_RGBA8); +} + +TEST_F(GXFifoTest, CopyTexCtfFormatsCanBackCompatibleTextureFormats) { + EXPECT_TRUE(aurora::gx::copy_texture_format_compatible(GX_CTF_R4, GX_TF_I4)); + EXPECT_TRUE(aurora::gx::copy_texture_format_compatible(GX_CTF_R8, GX_TF_I8)); + EXPECT_TRUE(aurora::gx::copy_texture_format_compatible(GX_CTF_G8, GX_TF_I8)); + EXPECT_TRUE(aurora::gx::copy_texture_format_compatible(GX_CTF_B8, GX_TF_I8)); + EXPECT_TRUE(aurora::gx::copy_texture_format_compatible(GX_CTF_RA4, GX_TF_IA4)); + EXPECT_TRUE(aurora::gx::copy_texture_format_compatible(GX_CTF_RA8, GX_TF_IA8)); + EXPECT_TRUE(aurora::gx::copy_texture_format_compatible(GX_CTF_RG8, GX_TF_IA8)); + EXPECT_TRUE(aurora::gx::copy_texture_format_compatible(GX_CTF_GB8, GX_TF_IA8)); + EXPECT_TRUE(aurora::gx::copy_texture_format_compatible(GX_TF_Z16, GX_TF_IA8)); + EXPECT_TRUE(aurora::gx::copy_texture_format_compatible(GX_CTF_YUVA8, GX_TF_RGBA8)); + EXPECT_TRUE(aurora::gx::copy_texture_format_compatible(GX_TF_Z24X8, GX_TF_RGBA8)); + EXPECT_FALSE(aurora::gx::copy_texture_format_compatible(GX_CTF_R8, GX_TF_RGBA8)); +} + +TEST_F(GXFifoTest, FieldMaskModeRevBitsAndFogRange_DecodeKnownBpState) { + std::vector bytes; + const auto fieldMask = bp_cmd(0x44, 0x03); + const auto revBits = bp_cmd(0x58, 0x0F); + const auto fieldMode = bp_cmd(0x68, 0x01); + const auto fogRangeBase = bp_cmd(0xE8, 0x0156); + const auto fogRangeK0 = bp_cmd(0xE9, 0x123456); + bytes.insert(bytes.end(), fieldMask.begin(), fieldMask.end()); + bytes.insert(bytes.end(), revBits.begin(), revBits.end()); + bytes.insert(bytes.end(), fieldMode.begin(), fieldMode.end()); + bytes.insert(bytes.end(), fogRangeBase.begin(), fogRangeBase.end()); + bytes.insert(bytes.end(), fogRangeK0.begin(), fogRangeK0.end()); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.fieldMask, 0x03u); + EXPECT_EQ(g_gxState.revBits, 0x0Fu); + EXPECT_EQ(g_gxState.fieldMode, 0x01u); + EXPECT_EQ(g_gxState.fogRange[0], 0x0156u); + EXPECT_EQ(g_gxState.fogRange[1], 0x123456u); +} + +TEST_F(GXFifoTest, XfErrorAndDualTex_DecodeKnownRawState) { + std::vector bytes; + const auto xfError = xf_cmd(0x1000, {0x3F}); + const auto dualTex = xf_cmd(0x1012, {0x01}); + bytes.insert(bytes.end(), xfError.begin(), xfError.end()); + bytes.insert(bytes.end(), dualTex.begin(), dualTex.end()); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.xfError, 0x3Fu); + EXPECT_EQ(g_gxState.dualTex, 0x01u); +} + +TEST_F(GXFifoTest, ClearBoundingBox_EncodesBpAndDecodesFallbackExtents) { + GXClearBoundingBox(); + auto bytes = capture_fifo(); + + ASSERT_EQ(bytes.size(), 10u); + EXPECT_EQ(bytes[0], 0x61); + EXPECT_EQ(bytes[1], 0x55); + EXPECT_EQ(bytes[5], 0x61); + EXPECT_EQ(bytes[6], 0x56); + + reset_gx_state(); + g_gxState.boundingBox = {1, 2, 3, 4}; + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.boundingBox[0], 1023u); + EXPECT_EQ(g_gxState.boundingBox[1], 0u); + EXPECT_EQ(g_gxState.boundingBox[2], 1023u); + EXPECT_EQ(g_gxState.boundingBox[3], 0u); +} + +TEST_F(GXFifoTest, ReadBoundingBox_ReturnsTrackedState) { + g_gxState.boundingBox = {10, 20, 30, 40}; + + u16 left = 0; + u16 right = 0; + u16 top = 0; + u16 bottom = 0; + GXReadBoundingBox(&left, &right, &top, &bottom); + + EXPECT_EQ(left, 10u); + EXPECT_EQ(right, 20u); + EXPECT_EQ(top, 30u); + EXPECT_EQ(bottom, 40u); +} + +// --- Clear color and depth round-trip --- +TEST_F(GXFifoTest, CopyClear_ColorAndDepth) { + GXColor color = {64, 128, 192, 255}; + GXSetCopyClear(color, 0x00ABCDEF); + auto bytes = capture_fifo(); + + // 3 BP writes: 3 * 5 = 15 bytes + ASSERT_EQ(bytes.size(), 15u); + EXPECT_EQ(bytes[0], 0x61); + EXPECT_EQ(bytes[1], 0x4F); // R + A + EXPECT_EQ(bytes[5], 0x61); + EXPECT_EQ(bytes[6], 0x50); // B + G + EXPECT_EQ(bytes[10], 0x61); + EXPECT_EQ(bytes[11], 0x51); // Z + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_NEAR(g_gxState.clearColor[0], 64.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.clearColor[1], 128.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.clearColor[2], 192.f / 255.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.clearColor[3], 255.f / 255.f, 1.f / 255.f); + EXPECT_EQ(g_gxState.clearDepth, 0x00ABCDEFu); +} + +// --- Clear with black and zero depth --- +TEST_F(GXFifoTest, CopyClear_BlackZeroDepth) { + GXColor color = {0, 0, 0, 0}; + GXSetCopyClear(color, 0); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_NEAR(g_gxState.clearColor[0], 0.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.clearColor[1], 0.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.clearColor[2], 0.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.clearColor[3], 0.f, 1.f / 255.f); + EXPECT_EQ(g_gxState.clearDepth, 0u); +} + +// --- Clear with max depth --- +TEST_F(GXFifoTest, CopyClear_MaxDepth) { + GXColor color = {255, 255, 255, 128}; + GXSetCopyClear(color, 0xFFFFFF); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_NEAR(g_gxState.clearColor[0], 1.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.clearColor[1], 1.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.clearColor[2], 1.f, 1.f / 255.f); + EXPECT_NEAR(g_gxState.clearColor[3], 128.f / 255.f, 1.f / 255.f); + EXPECT_EQ(g_gxState.clearDepth, 0xFFFFFFu); +} + +TEST_F(GXFifoTest, PeekZ_ReturnsClearDepthFallbackAndRequestsSnapshot) { + g_gxState.clearDepth = 0x123456; + + u32 z = 0; + GXPeekZ(10, 20, &z); + + EXPECT_EQ(z, 0x123456u); + EXPECT_TRUE(aurora::gfx::depth_peek::testing::snapshot_requested()); +} + +TEST_F(GXFifoTest, PeekZ_ReturnsLatestCompletedSnapshot) { + aurora::gfx::depth_peek::testing::set_latest(2, 2, {0x000001, 0x000002, 0x000003, 0x01000004}); + + u32 z = 0; + GXPeekZ(1, 1, &z); + + EXPECT_EQ(z, 0x000004u); + EXPECT_TRUE(aurora::gfx::depth_peek::testing::snapshot_requested()); +} + +TEST_F(GXFifoTest, PeekZ_OutOfRangeReturnsClearDepthFallback) { + g_gxState.clearDepth = 0xabcdef; + aurora::gfx::depth_peek::testing::set_latest(1, 1, {0x000001}); + + u32 z = 0; + GXPeekZ(1, 0, &z); + + EXPECT_EQ(z, 0xabcdefu); + EXPECT_TRUE(aurora::gfx::depth_peek::testing::snapshot_requested()); +} + +// Composite tests (multiple state changes in a single FIFO stream) + +TEST_F(GXFifoTest, Composite_BlendAndZMode) { + GXSetBlendMode(GX_BM_BLEND, GX_BL_SRCALPHA, GX_BL_INVSRCALPHA, GX_LO_NOOP); + GXSetZMode(true, GX_LEQUAL, true); + GXSetAlphaCompare(GX_GREATER, 128, GX_AOP_AND, GX_ALWAYS, 0); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.blendMode, GX_BM_BLEND); + EXPECT_EQ(g_gxState.blendFacSrc, GX_BL_SRCALPHA); + EXPECT_EQ(g_gxState.blendFacDst, GX_BL_INVSRCALPHA); + + EXPECT_TRUE(g_gxState.depthCompare); + EXPECT_EQ(g_gxState.depthFunc, GX_LEQUAL); + EXPECT_TRUE(g_gxState.depthUpdate); + + EXPECT_EQ(g_gxState.alphaCompare.comp0, GX_GREATER); + EXPECT_EQ(g_gxState.alphaCompare.ref0, 128u); +} + +// --- GXLoadTexMtxImm for PTTexMtx (XF 0x500-0x5EF) --- + +TEST_F(GXFifoTest, LoadPTTexMtx_Identity) { + aurora::Mat3x4 mtx{}; + mtx.m0[0] = 1.0f; + mtx.m1[1] = 1.0f; + mtx.m2[2] = 1.0f; + + GXLoadTexMtxImm(&mtx, GX_PTTEXMTX0, GX_MTX3x4); + auto bytes = capture_fifo(); + + // XF opcode 0x10, addr = (64 - 64) * 4 + 0x500 = 0x500, count = 12 + ASSERT_GE(bytes.size(), 5u); + EXPECT_EQ(bytes[0], 0x10); + + reset_gx_state(); + decode_fifo(bytes); + + auto& decoded = g_gxState.ptTexMtxs[0]; + EXPECT_FLOAT_EQ(decoded.m0[0], 1.0f); + EXPECT_FLOAT_EQ(decoded.m0[1], 0.0f); + EXPECT_FLOAT_EQ(decoded.m0[2], 0.0f); + EXPECT_FLOAT_EQ(decoded.m0[3], 0.0f); + EXPECT_FLOAT_EQ(decoded.m1[0], 0.0f); + EXPECT_FLOAT_EQ(decoded.m1[1], 1.0f); + EXPECT_FLOAT_EQ(decoded.m1[2], 0.0f); + EXPECT_FLOAT_EQ(decoded.m1[3], 0.0f); + EXPECT_FLOAT_EQ(decoded.m2[0], 0.0f); + EXPECT_FLOAT_EQ(decoded.m2[1], 0.0f); + EXPECT_FLOAT_EQ(decoded.m2[2], 1.0f); + EXPECT_FLOAT_EQ(decoded.m2[3], 0.0f); +} + +TEST_F(GXFifoTest, LoadPTTexMtx_ArbitraryValues) { + aurora::Mat3x4 mtx{}; + mtx.m0[0] = 2.0f; mtx.m0[1] = 0.5f; mtx.m0[2] = 0.0f; mtx.m0[3] = 10.0f; + mtx.m1[0] = -0.5f; mtx.m1[1] = 3.0f; mtx.m1[2] = 0.0f; mtx.m1[3] = 20.0f; + mtx.m2[0] = 0.0f; mtx.m2[1] = 0.0f; mtx.m2[2] = 1.5f; mtx.m2[3] = -5.0f; + + GXLoadTexMtxImm(&mtx, GX_PTTEXMTX0, GX_MTX3x4); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& decoded = g_gxState.ptTexMtxs[0]; + EXPECT_FLOAT_EQ(decoded.m0[0], 2.0f); + EXPECT_FLOAT_EQ(decoded.m0[1], 0.5f); + EXPECT_FLOAT_EQ(decoded.m0[2], 0.0f); + EXPECT_FLOAT_EQ(decoded.m0[3], 10.0f); + EXPECT_FLOAT_EQ(decoded.m1[0], -0.5f); + EXPECT_FLOAT_EQ(decoded.m1[1], 3.0f); + EXPECT_FLOAT_EQ(decoded.m1[2], 0.0f); + EXPECT_FLOAT_EQ(decoded.m1[3], 20.0f); + EXPECT_FLOAT_EQ(decoded.m2[0], 0.0f); + EXPECT_FLOAT_EQ(decoded.m2[1], 0.0f); + EXPECT_FLOAT_EQ(decoded.m2[2], 1.5f); + EXPECT_FLOAT_EQ(decoded.m2[3], -5.0f); +} + +TEST_F(GXFifoTest, LoadPTTexMtx_DifferentSlots) { + aurora::Mat3x4 mtx0{}; + mtx0.m0[0] = 1.0f; + mtx0.m1[1] = 1.0f; + mtx0.m2[2] = 1.0f; + + aurora::Mat3x4 mtx5{}; + mtx5.m0[0] = 5.0f; + mtx5.m1[1] = 6.0f; + mtx5.m2[2] = 7.0f; + mtx5.m0[3] = 100.0f; + + GXLoadTexMtxImm(&mtx0, GX_PTTEXMTX0, GX_MTX3x4); + GXLoadTexMtxImm(&mtx5, GX_PTTEXMTX5, GX_MTX3x4); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + // Slot 0 + EXPECT_FLOAT_EQ(g_gxState.ptTexMtxs[0].m0[0], 1.0f); + EXPECT_FLOAT_EQ(g_gxState.ptTexMtxs[0].m1[1], 1.0f); + EXPECT_FLOAT_EQ(g_gxState.ptTexMtxs[0].m2[2], 1.0f); + + // Slot 5: GX_PTTEXMTX5 = 79, index = (79 - 64) / 3 = 5 + EXPECT_FLOAT_EQ(g_gxState.ptTexMtxs[5].m0[0], 5.0f); + EXPECT_FLOAT_EQ(g_gxState.ptTexMtxs[5].m1[1], 6.0f); + EXPECT_FLOAT_EQ(g_gxState.ptTexMtxs[5].m2[2], 7.0f); + EXPECT_FLOAT_EQ(g_gxState.ptTexMtxs[5].m0[3], 100.0f); +} + +TEST_F(GXFifoTest, LoadPTTexMtx_LastSlot) { + aurora::Mat3x4 mtx{}; + mtx.m0[0] = 42.0f; + mtx.m1[1] = 43.0f; + mtx.m2[2] = 44.0f; + + GXLoadTexMtxImm(&mtx, GX_PTTEXMTX19, GX_MTX3x4); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + auto& decoded = g_gxState.ptTexMtxs[19]; + EXPECT_FLOAT_EQ(decoded.m0[0], 42.0f); + EXPECT_FLOAT_EQ(decoded.m1[1], 43.0f); + EXPECT_FLOAT_EQ(decoded.m2[2], 44.0f); + // Other elements should be zero (from reset) + EXPECT_FLOAT_EQ(decoded.m0[1], 0.0f); + EXPECT_FLOAT_EQ(decoded.m2[3], 0.0f); +} + +TEST_F(GXFifoTest, LoadPTTexMtx_Isolation) { + // Loading PTTexMtx0 should not affect PTTexMtx1 + aurora::Mat3x4 mtx{}; + mtx.m0[0] = 99.0f; + mtx.m1[1] = 88.0f; + mtx.m2[2] = 77.0f; + + GXLoadTexMtxImm(&mtx, GX_PTTEXMTX0, GX_MTX3x4); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + // Slot 0 should have our values + EXPECT_FLOAT_EQ(g_gxState.ptTexMtxs[0].m0[0], 99.0f); + // Slot 1 should remain zeroed + EXPECT_FLOAT_EQ(g_gxState.ptTexMtxs[1].m0[0], 0.0f); + EXPECT_FLOAT_EQ(g_gxState.ptTexMtxs[1].m1[1], 0.0f); + EXPECT_FLOAT_EQ(g_gxState.ptTexMtxs[1].m2[2], 0.0f); +} + +// Composite / multi-command tests + +TEST_F(GXFifoTest, Composite_TevSetup) { + // Set up a simple 1-stage TEV that passes through texture color + GXSetNumTevStages(1); + GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR0A0); + GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, GX_CC_TEXC); + GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_TEXA); + + // TEV order writes to dirty state, so flush before capture + auto bytes = flush_and_capture(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.numTevStages, 1u); + EXPECT_EQ(g_gxState.tevStages[0].texMapId, GX_TEXMAP0); + EXPECT_EQ(g_gxState.tevStages[0].texCoordId, GX_TEXCOORD0); + EXPECT_EQ(g_gxState.tevStages[0].channelId, GX_COLOR0A0); + EXPECT_EQ(g_gxState.tevStages[0].colorPass.d, GX_CC_TEXC); + EXPECT_EQ(g_gxState.tevStages[0].alphaPass.d, GX_CA_TEXA); +} diff --git a/aurora-main/tests/gx_test_common.hpp b/aurora-main/tests/gx_test_common.hpp new file mode 100644 index 0000000..7d37c52 --- /dev/null +++ b/aurora-main/tests/gx_test_common.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include + +#include + +#include "gx/gx.hpp" +#include "gx/fifo.hpp" +#include "gx/command_processor.hpp" +#include "gfx/depth_peek.hpp" +#include "internal.hpp" + +#include +#include +#include +#include + +// Forward declare dirty state flush +extern "C" void __GXSetDirtyState(); + +namespace aurora::gfx::testing { +struct ResolvePassRecord { + TextureHandle texture; + ClipRect rect; + bool clearColor = false; + bool clearAlpha = false; + bool clearDepth = false; + Vec4 clearColorValue{0.f, 0.f, 0.f, 0.f}; + float clearDepthValue = 0.f; + GXTexFmt resolveFormat = GX_TF_RGBA8; + std::optional> sourceRectPixels; + bool halfScale = false; + std::array copyFilterCoefficients{0, 64, 0}; + bool forceOpaqueAlpha = false; + float copyFilterRowStride = 1.0f; + bool clampTop = false; + bool clampBottom = false; + bool persistentCopy = false; +}; + +void reset_resolve_pass_records() noexcept; +const std::vector& resolve_pass_records() noexcept; +void set_current_frame(uint32_t frame) noexcept; +void set_framebuffer_sizes(uint32_t logicalWidth, uint32_t logicalHeight, + uint32_t targetWidth, uint32_t targetHeight) noexcept; +void reset_vertex_push_record() noexcept; +const std::vector& last_pushed_vertices() noexcept; +const std::vector& last_pushed_indices() noexcept; +void reset_uniform_allocations() noexcept; +const std::vector& uniform_allocation(size_t index) noexcept; +void use_draw_command_tracking(bool enabled) noexcept; +void use_real_vertex_format_helpers(bool enabled) noexcept; +} // namespace aurora::gfx::testing + +class GXFifoTest : public ::testing::Test { +protected: + void SetUp() override { + aurora::gfx::testing::set_current_frame(0); + aurora::gfx::testing::set_framebuffer_sizes(640, 480, 640, 480); + GXInit(nullptr, 0); + aurora::gx::fifo::clear_buffer(); + aurora::gx::g_gxState = aurora::gx::GXState{}; + aurora::g_config.disableCopyFilter = false; + aurora::gfx::depth_peek::testing::reset(); + aurora::gfx::testing::reset_resolve_pass_records(); + aurora::gfx::testing::reset_vertex_push_record(); + aurora::gfx::testing::use_real_vertex_format_helpers(false); + } + + // Copy the internal FIFO buffer contents and clear it + std::vector capture_fifo() { + auto size = aurora::gx::fifo::get_buffer_size(); + auto* data = aurora::gx::fifo::get_buffer_data(); + std::vector bytes(data, data + size); + aurora::gx::fifo::clear_buffer(); + return bytes; + } + + // Reset g_gxState to default-constructed state + void reset_gx_state() { aurora::gx::g_gxState = aurora::gx::GXState{}; } + + // Decode a captured FIFO byte stream through the command processor + void decode_fifo(const std::vector& bytes) { + aurora::gx::fifo::process(bytes.data(), static_cast(bytes.size()), true); + } + + // Flush dirty state, then capture the FIFO buffer + std::vector flush_and_capture() { + __GXSetDirtyState(); + return capture_fifo(); + } + + // Convenience reference to g_gxState + aurora::gx::GXState& gxState() { return aurora::gx::g_gxState; } +}; diff --git a/aurora-main/tests/gx_test_stubs.cpp b/aurora-main/tests/gx_test_stubs.cpp new file mode 100644 index 0000000..32bf6bd --- /dev/null +++ b/aurora-main/tests/gx_test_stubs.cpp @@ -0,0 +1,553 @@ +// Stubs for renderer symbols the GX/FIFO/command_processor code references but that live in the +// full renderer, so the test binary links without the WebGPU runtime. + +#include "gx/gx.hpp" +#include "gfx/clear.hpp" +#include "gfx/common.hpp" +#include "gfx/depth_peek.hpp" +#include "gfx/efb_ram_copy.hpp" +#include "gfx/tex_copy_conv.hpp" +#include "gfx/tex_palette_conv.hpp" +#include "gfx/texture.hpp" +#include "gx/pipeline.hpp" +#include "gx/shader_info.hpp" +#include "internal.hpp" +#include "webgpu/gpu.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +aurora::Vec2 s_logicalFbSize{640, 480}; +aurora::Vec2 s_renderTargetSize{640, 480}; +uint32_t s_currentFrame = 0; +std::vector s_lastPushedVertices; +std::vector s_lastPushedIndices; +std::optional s_lastGxDraw; +bool s_trackDrawCommands = false; +bool s_useRealVertexFormatHelpers = false; +std::deque> s_uniformAllocations; +} // namespace + +// --- aurora::g_config --- +namespace aurora { +AuroraConfig g_config{}; +void wait_for_frame_worker() noexcept {} +std::chrono::nanoseconds wait_for_frame_worker_sealed() noexcept { return {}; } +bool wait_for_frame_worker_for(std::chrono::microseconds) noexcept { return true; } +std::recursive_mutex& renderer_gpu_mutex() noexcept { + static std::recursive_mutex mutex; + return mutex; +} +} // namespace aurora + +extern "C" bool aurora_wait_for_frame_worker_for(uint32_t) { return true; } + +// --- aurora::log_internal --- +namespace aurora { +void log_internal(AuroraLogLevel level, const char* module, const char* message, unsigned int len) noexcept { + fprintf(stderr, "[%d] %s: %.*s\n", static_cast(level), module, len, message); +} +void Module::show_fatal_dialog(const char*, std::string_view) noexcept {} +} // namespace aurora + +namespace aurora::window { +void set_present_surface_fill(bool) {} +} // namespace aurora::window + +// --- fmt::formatter --- +auto fmt::formatter::format(AuroraLogLevel level, format_context& ctx) const + -> format_context::iterator { + return fmt::format_to(ctx.out(), "{}", static_cast(level)); +} + +// --- GPU buffers (default-constructed, not used in tests) --- +namespace aurora::gfx { +AuroraStats g_stats; +wgpu::Buffer g_vertexBuffer; +wgpu::Buffer g_uniformBuffer; +wgpu::Buffer g_indexBuffer; +wgpu::Buffer g_storageBuffer; +uint32_t g_drawCallCount = 0; +uint32_t g_mergedDrawCallCount = 0; +} // namespace aurora::gfx + +namespace aurora::webgpu { +GraphicsConfig g_graphicsConfig{}; +} // namespace aurora::webgpu + +// --- GXState (the real instance -- tests validate this) --- +namespace aurora::gx { +GXState g_gxState{}; +void notify_copy_texture_created() noexcept {} +} // namespace aurora::gx + +namespace aurora::vi { +Vec2 configured_fb_size() noexcept { return s_logicalFbSize; } +void configure(const GXRenderModeObj*) noexcept {} +} // namespace aurora::vi + +// --- Texture uploads --- +namespace aurora::gfx { +std::vector g_textureUploads; +} // namespace aurora::gfx + +namespace aurora::gfx::efb_ram { +void schedule(void*, uint32_t, uint32_t, GXTexFmt, TextureHandle) noexcept {} +} // namespace aurora::gfx::efb_ram + +// --- get_texture --- +namespace aurora::gx { +const gfx::TextureBind& get_texture(GXTexMapID id) noexcept { return g_gxState.textures[id]; } +void evict_texture_object(u32 texObjId) noexcept { + for (auto& obj : g_gxState.loadedTextures) { + if (obj.texObjId == texObjId) { + obj.set_no_cache(true); + } + } +} +void evict_tlut_object(u32 tlutObjId) noexcept { + for (auto& obj : g_gxState.loadedTluts) { + if (obj.tlutObjId == tlutObjId) { + obj.set_no_cache(true); + } + } +} +void invalidate_static_texture_cache() noexcept { + for (auto& texture : g_gxState.textures) { + texture.reset(); + } + g_gxState.stateDirty = true; +} +void evict_copy_texture(const void* dest) noexcept { + g_gxState.copyTextures.erase(dest); + for (auto it = g_gxState.copyTextureCache.begin(); it != g_gxState.copyTextureCache.end();) { + if (it->first.dest == dest) { + g_gxState.copyTextureCache.erase(it++); + } else { + ++it; + } + } +} +void set_display_copy_present_source() noexcept {} +void shutdown() noexcept {} +Vec2 logical_fb_size() noexcept { return s_logicalFbSize; } +gfx::Viewport map_logical_viewport(const gfx::Viewport& logicalViewport) noexcept { + const float scaleX = static_cast(s_renderTargetSize.x) / static_cast(s_logicalFbSize.x); + const float scaleY = static_cast(s_renderTargetSize.y) / static_cast(s_logicalFbSize.y); + return { + .left = logicalViewport.left * scaleX, + .top = logicalViewport.top * scaleY, + .width = logicalViewport.width * scaleX, + .height = logicalViewport.height * scaleY, + .znear = logicalViewport.znear, + .zfar = logicalViewport.zfar, + }; +} +gfx::ClipRect map_logical_scissor(const gfx::ClipRect& logicalScissor) noexcept { + const auto mapped = map_logical_viewport({ + .left = static_cast(logicalScissor.x), + .top = static_cast(logicalScissor.y), + .width = static_cast(logicalScissor.width), + .height = static_cast(logicalScissor.height), + .znear = 0.0f, + .zfar = 1.0f, + }); + const auto left = static_cast(std::floor(mapped.left)); + const auto top = static_cast(std::floor(mapped.top)); + const auto right = static_cast(std::ceil(mapped.left + mapped.width)); + const auto bottom = static_cast(std::ceil(mapped.top + mapped.height)); + return {left, top, right - left, bottom - top}; +} +MappedRenderState map_logical_render_state() noexcept { + return { + .viewport = map_logical_viewport(g_gxState.logicalViewport), + .scissor = map_logical_scissor(g_gxState.logicalScissor), + }; +} +void set_logical_viewport(const gfx::Viewport& viewport) noexcept { + g_gxState.logicalViewport = viewport; + set_render_viewport(map_logical_viewport(viewport)); +} +void set_render_viewport(const gfx::Viewport& viewport) noexcept { g_gxState.renderViewport = viewport; } +void set_logical_scissor(const gfx::ClipRect& scissor) noexcept { + g_gxState.logicalScissor = scissor; + set_render_scissor(map_logical_scissor(scissor)); +} +void set_render_scissor(const gfx::ClipRect& scissor) noexcept { g_gxState.renderScissor = scissor; } +} // namespace aurora::gx + +// --- Shader/pipeline stubs --- +namespace aurora::gx { +void populate_pipeline_config(PipelineConfig& config, GXPrimitive primitive, GXVtxFmt fmt) noexcept { + // No-op for tests +} +GXBindGroups build_bind_groups(const ShaderInfo& info) noexcept { return {}; } +void resolve_sampled_textures(const ShaderInfo& info) noexcept {} +u8 color_channel(GXChannelID id) noexcept { return 0; } +u8 comp_type_size(GXAttr attr, GXCompType type) noexcept { + if (!s_useRealVertexFormatHelpers) { + return 0; + } + switch (attr) { + case GX_VA_PNMTXIDX: + case GX_VA_TEX0MTXIDX: + case GX_VA_TEX1MTXIDX: + case GX_VA_TEX2MTXIDX: + case GX_VA_TEX3MTXIDX: + case GX_VA_TEX4MTXIDX: + case GX_VA_TEX5MTXIDX: + case GX_VA_TEX6MTXIDX: + case GX_VA_TEX7MTXIDX: + return 1; + case GX_VA_CLR0: + case GX_VA_CLR1: + switch (type) { + case GX_RGB565: + case GX_RGBA4: + return 2; + case GX_RGB8: + case GX_RGBA6: + return 3; + case GX_RGBX8: + case GX_RGBA8: + return 4; + default: + return 0; + } + default: + switch (type) { + case GX_U8: + case GX_S8: + return 1; + case GX_U16: + case GX_S16: + return 2; + case GX_F32: + return 4; + default: + return 0; + } + } +} +u8 comp_cnt_count(GXAttr attr, GXCompCnt cnt) noexcept { + if (!s_useRealVertexFormatHelpers) { + return 0; + } + switch (attr) { + case GX_VA_PNMTXIDX: + case GX_VA_TEX0MTXIDX: + case GX_VA_TEX1MTXIDX: + case GX_VA_TEX2MTXIDX: + case GX_VA_TEX3MTXIDX: + case GX_VA_TEX4MTXIDX: + case GX_VA_TEX5MTXIDX: + case GX_VA_TEX6MTXIDX: + case GX_VA_TEX7MTXIDX: + return 1; + case GX_VA_POS: + return cnt == GX_POS_XY ? 2 : cnt == GX_POS_XYZ ? 3 : 0; + case GX_VA_NRM: + return cnt == GX_NRM_XYZ ? 3 : (cnt == GX_NRM_NBT || cnt == GX_NRM_NBT3) ? 9 : 0; + case GX_VA_CLR0: + case GX_VA_CLR1: + return 1; + case GX_VA_TEX0: + case GX_VA_TEX1: + case GX_VA_TEX2: + case GX_VA_TEX3: + case GX_VA_TEX4: + case GX_VA_TEX5: + case GX_VA_TEX6: + case GX_VA_TEX7: + return cnt == GX_TEX_S ? 1 : cnt == GX_TEX_ST ? 2 : 0; + default: + return 0; + } +} +} // namespace aurora::gx + +// --- Buffer push stubs --- +namespace aurora::gfx { +Range push_verts(const uint8_t* data, size_t length) { + s_lastPushedVertices.assign(data, data + length); + return {}; +} +Range push_indices(const uint8_t* data, size_t length) { + CHECK(length % sizeof(uint16_t) == 0, "unaligned test index upload"); + s_lastPushedIndices.resize(length / sizeof(uint16_t)); + std::memcpy(s_lastPushedIndices.data(), data, length); + return {}; +} +Range push_uniform(const uint8_t* data, size_t length) { return {}; } +Range push_storage(const uint8_t* data, size_t length) { return {}; } +std::pair map_uniform(size_t length) { + s_uniformAllocations.emplace_back(length, 0); + auto& uniformBytes = s_uniformAllocations.back(); + return {ByteBuffer{uniformBytes.data(), uniformBytes.size()}, + Range{static_cast(s_uniformAllocations.size() - 1), + static_cast(length)}}; +} +std::pair copy_uniform(Range source) { + return map_uniform(source.size); +} +uint32_t align_uniform(uint32_t value) { return (value + 255u) & ~255u; } + +Vec2 get_render_target_size() noexcept { return s_renderTargetSize; } +Vec2 get_frame_buffer_size() noexcept { return s_renderTargetSize; } +uint32_t current_frame() noexcept { return s_currentFrame; } +// The command processor keys its resolved-pipeline memo on this. The test build never changes +// render targets, so a fixed single-sample target matches what pipeline_ref would produce. +uint32_t get_sample_count() noexcept { return 1; } +void set_viewport(const Viewport& viewport) noexcept {} +void set_scissor(uint32_t x, uint32_t y, uint32_t w, uint32_t h) noexcept {} +} // namespace aurora::gfx + +namespace aurora::gfx::testing { +void set_current_frame(uint32_t frame) noexcept { s_currentFrame = frame; } + +void reset_vertex_push_record() noexcept { + s_lastPushedVertices.clear(); + s_lastPushedIndices.clear(); + s_lastGxDraw.reset(); + s_trackDrawCommands = false; + g_mergedDrawCallCount = 0; +} +const std::vector& last_pushed_vertices() noexcept { + return s_lastPushedVertices; +} +const std::vector& last_pushed_indices() noexcept { + return s_lastPushedIndices; +} +void reset_uniform_allocations() noexcept { + s_uniformAllocations.clear(); +} +const std::vector& uniform_allocation(size_t index) noexcept { + CHECK(index < s_uniformAllocations.size(), "uniform test allocation {} out of range {}", index, + s_uniformAllocations.size()); + return s_uniformAllocations[index]; +} +void use_draw_command_tracking(bool enabled) noexcept { + s_trackDrawCommands = enabled; + s_lastGxDraw.reset(); +} +void use_real_vertex_format_helpers(bool enabled) noexcept { + s_useRealVertexFormatHelpers = enabled; +} +} // namespace aurora::gfx::testing + +// --- Pipeline/draw command stubs --- +namespace aurora::gfx { +template <> +PipelineRef pipeline_ref(const clear::PipelineConfig& config) { + return 0; +} +template <> +void push_draw_command(clear::DrawData data) { + // No-op +} +template <> +PipelineRef pipeline_ref(const gx::PipelineConfig& config) { + return 0; +} +template <> +void push_draw_command(gx::DrawData data) { + if (s_trackDrawCommands) { + s_lastGxDraw = std::move(data); + } +} +template <> +gx::DrawData* get_last_draw_command() { + return s_trackDrawCommands && s_lastGxDraw.has_value() ? &*s_lastGxDraw : nullptr; +} +} // namespace aurora::gfx + +// --- TextureBind::get_descriptor --- +namespace aurora::gfx { +wgpu::SamplerDescriptor TextureBind::get_descriptor() const noexcept { return wgpu::SamplerDescriptor{}; } +} // namespace aurora::gfx + +// --- Texture creation/write/replacement stubs --- +namespace aurora::gfx { +namespace testing { +struct ResolvePassRecord { + TextureHandle texture; + ClipRect rect; + bool clearColor = false; + bool clearAlpha = false; + bool clearDepth = false; + Vec4 clearColorValue{0.f, 0.f, 0.f, 0.f}; + float clearDepthValue = 0.f; + GXTexFmt resolveFormat = GX_TF_RGBA8; + std::optional> sourceRectPixels; + bool halfScale = false; + std::array copyFilterCoefficients{0, 64, 0}; + bool forceOpaqueAlpha = false; + float copyFilterRowStride = 1.0f; + bool clampTop = false; + bool clampBottom = false; + bool persistentCopy = false; +}; + +namespace { +std::vector s_resolvePassRecords; +} // namespace + +void reset_resolve_pass_records() noexcept { s_resolvePassRecords.clear(); } + +const std::vector& resolve_pass_records() noexcept { return s_resolvePassRecords; } + +void set_framebuffer_sizes(uint32_t logicalWidth, uint32_t logicalHeight, + uint32_t targetWidth, uint32_t targetHeight) noexcept { + s_logicalFbSize = {logicalWidth, logicalHeight}; + s_renderTargetSize = {targetWidth, targetHeight}; +} +} // namespace testing + +TextureHandle new_static_texture_2d(uint32_t width, uint32_t height, uint32_t mips, u32 gxFormat, + ArrayRef data, bool tlut, const char* label) noexcept { + return {}; +} +TextureHandle new_dynamic_texture_2d(uint32_t width, uint32_t height, uint32_t mips, u32 gxFormat, + const char* label) noexcept { + return {}; +} +TextureHandle new_render_texture(uint32_t width, uint32_t height, u32 gxFormat, const char* label) noexcept { + return std::make_shared(wgpu::Texture{}, wgpu::TextureView{}, wgpu::TextureView{}, + wgpu::Extent3D{width, height, 1}, wgpu::TextureFormat::RGBA8Unorm, 1, gxFormat); +} +TextureHandle new_conv_texture(uint32_t width, uint32_t height, u32 gxFormat, const char* label) noexcept { + return std::make_shared(wgpu::Texture{}, wgpu::TextureView{}, wgpu::TextureView{}, + wgpu::Extent3D{width, height, 1}, wgpu::TextureFormat::RGBA8Unorm, 1, gxFormat); +} +void write_texture(const TextureRef& ref, ArrayRef data) noexcept {} +void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool clearAlpha, bool clearDepth, + Vec4 clearColorValue, float clearDepthValue, GXTexFmt resolveFormat, + const Vec4* sourceRectPixels, bool halfScale, + const std::array* copyFilterCoefficients, bool forceOpaqueAlpha, + float copyFilterRowStride, bool clampTop, bool clampBottom, bool persistentCopy) { + testing::ResolvePassRecord record; + record.texture = std::move(texture); + record.rect = rect; + record.clearColor = clearColor; + record.clearAlpha = clearAlpha; + record.clearDepth = clearDepth; + record.clearColorValue = clearColorValue; + record.clearDepthValue = clearDepthValue; + record.resolveFormat = resolveFormat; + record.sourceRectPixels = sourceRectPixels != nullptr ? std::make_optional(*sourceRectPixels) : std::nullopt; + record.halfScale = halfScale; + record.copyFilterCoefficients = + copyFilterCoefficients != nullptr ? *copyFilterCoefficients : std::array{0, 64, 0}; + record.forceOpaqueAlpha = forceOpaqueAlpha; + record.copyFilterRowStride = copyFilterRowStride; + record.clampTop = clampTop; + record.clampBottom = clampBottom; + record.persistentCopy = persistentCopy; + testing::s_resolvePassRecords.push_back(std::move(record)); +} +void queue_palette_conv(tex_palette_conv::ConvRequest req) {} +void begin_offscreen(uint32_t width, uint32_t height) {} +void end_offscreen() {} +bool is_offscreen() noexcept { return false; } +} // namespace aurora::gfx + +namespace aurora::gfx::depth_peek { +namespace { +bool s_snapshotRequested = false; +uint32_t s_width = 0; +uint32_t s_height = 0; +std::vector s_data; +} // namespace + +void initialize() {} +void shutdown() {} +void request_snapshot() noexcept { s_snapshotRequested = true; } +void poll() noexcept {} +void encode_frame_snapshot(const wgpu::CommandEncoder& cmd, const wgpu::TextureView& depthView, + wgpu::Extent3D sourceSize, uint32_t msaaSamples) noexcept {} +void after_submit() noexcept {} + +bool read_latest(uint16_t x, uint16_t y, uint32_t& z) noexcept { + if (x >= s_width || y >= s_height || s_data.empty()) { + return false; + } + z = s_data[static_cast(y) * s_width + x] & 0x00ffffffu; + return true; +} + +namespace testing { +void reset() noexcept { + s_snapshotRequested = false; + s_width = 0; + s_height = 0; + s_data.clear(); +} + +bool snapshot_requested() noexcept { return s_snapshotRequested; } + +void set_latest(uint32_t width, uint32_t height, const std::vector& data) { + s_width = width; + s_height = height; + s_data = data; +} +} // namespace testing +} // namespace aurora::gfx::depth_peek + +namespace aurora::gfx::tex_copy_conv { +bool needs_conversion(GXTexFmt fmt) { return false; } +} // namespace aurora::gfx::tex_copy_conv + +namespace aurora::gfx::tex_palette_conv { +void queue(ConvRequest req) {} +} // namespace aurora::gfx::tex_palette_conv + +namespace aurora::gfx::texture_replacement { +u32 compute_texture_upload_size(const GXTexObj_& obj) noexcept { return 0; } +void register_tlut(const GXTlutObj*, const void*, GXTlutFmt, u16) noexcept {} +void load_tlut(const GXTlutObj*, u32) noexcept {} +std::optional find_replacement(const GXTexObj_&) noexcept { return std::nullopt; } +} // namespace aurora::gfx::texture_replacement + +// --- Window stub --- +#include "../lib/window.hpp" +namespace aurora::window { +AuroraWindowSize get_window_size() { return {640, 480, 640, 480, 640, 480, 1.0f}; } +void set_frame_buffer_aspect_fit(bool) {} +} // namespace aurora::window + +// --- WebGPU C API stubs (prevent linker errors from wgpu:: destructors) --- +extern "C" { +void wgpuDeviceRelease(WGPUDevice) {} +void wgpuQueueRelease(WGPUQueue) {} +void wgpuSurfaceRelease(WGPUSurface) {} +void wgpuBufferRelease(WGPUBuffer) {} +void wgpuTextureRelease(WGPUTexture) {} +void wgpuTextureViewRelease(WGPUTextureView) {} +void wgpuSamplerRelease(WGPUSampler) {} +void wgpuShaderModuleRelease(WGPUShaderModule) {} +void wgpuRenderPipelineRelease(WGPURenderPipeline) {} +void wgpuBindGroupRelease(WGPUBindGroup) {} +void wgpuBindGroupLayoutRelease(WGPUBindGroupLayout) {} +void wgpuPipelineLayoutRelease(WGPUPipelineLayout) {} +void wgpuInstanceRelease(WGPUInstance) {} +void wgpuDeviceAddRef(WGPUDevice) {} +void wgpuQueueAddRef(WGPUQueue) {} +void wgpuSurfaceAddRef(WGPUSurface) {} +void wgpuBufferAddRef(WGPUBuffer) {} +void wgpuTextureAddRef(WGPUTexture) {} +void wgpuTextureViewAddRef(WGPUTextureView) {} +void wgpuInstanceAddRef(WGPUInstance) {} +} + +void aurora::gfx::push_debug_group(std::string) {} +void aurora_push_debug_group(const char*) {} +void aurora_pop_debug_group() {} +void aurora::gfx::insert_debug_marker(std::string) {} diff --git a/aurora-main/tests/os_alloc_test.cpp b/aurora-main/tests/os_alloc_test.cpp new file mode 100644 index 0000000..2811210 --- /dev/null +++ b/aurora-main/tests/os_alloc_test.cpp @@ -0,0 +1,106 @@ +#include + +#include + +#include +#include + +namespace { + +constexpr std::size_t kArenaBytes = 64 * 1024; + +std::array gArena{}; + +void resetAllocator(int maxHeaps = 8) { + std::fill(gArena.begin(), gArena.end(), 0); + void* start = gArena.data(); + void* end = gArena.data() + gArena.size(); + ASSERT_NE(OSInitAlloc(start, end, maxHeaps), nullptr); + EXPECT_EQ(OSSetCurrentHeap(-1), -1); +} + +} // namespace + +TEST(OSAlloc, InitCreateAllocFreeCheck) { + resetAllocator(); + + OSHeapHandle heap = OSCreateHeap(gArena.data() + 512, gArena.data() + 4096); + ASSERT_GE(heap, 0); + + EXPECT_EQ(OSSetCurrentHeap(heap), -1); + + void* p = OSAllocFromHeap(heap, 96); + ASSERT_NE(p, nullptr); + EXPECT_EQ(reinterpret_cast(p) & 31u, 0u); + EXPECT_GE(OSReferentSize(p), 96u); + + s32 freeBefore = OSCheckHeap(heap); + EXPECT_GE(freeBefore, 0); + + OSFreeToHeap(heap, p); + s32 freeAfter = OSCheckHeap(heap); + EXPECT_GE(freeAfter, freeBefore); +} + +TEST(OSAlloc, AddToHeapMakesSpaceAvailable) { + resetAllocator(); + + OSHeapHandle heap = OSCreateHeap(gArena.data() + 0x1000, gArena.data() + 0x2000); + ASSERT_GE(heap, 0); + + void* p1 = OSAllocFromHeap(heap, 0xD00); + ASSERT_NE(p1, nullptr); + + void* p2 = OSAllocFromHeap(heap, 0x400); + EXPECT_EQ(p2, nullptr); + + OSAddToHeap(heap, gArena.data() + 0x3000, gArena.data() + 0x3800); + + p2 = OSAllocFromHeap(heap, 0x400); + EXPECT_NE(p2, nullptr); +} + +TEST(OSAlloc, AllocFixedCarvesRangeFromHeap) { + resetAllocator(); + + OSHeapHandle heap = OSCreateHeap(gArena.data() + 0x2000, gArena.data() + 0x5000); + ASSERT_GE(heap, 0); + + s32 freeBefore = OSCheckHeap(heap); + ASSERT_GT(freeBefore, 0); + + void* fixed = OSAllocFixed(gArena.data() + 0x3000, gArena.data() + 0x3400); + ASSERT_NE(fixed, nullptr); + + s32 freeAfter = OSCheckHeap(heap); + EXPECT_GE(freeBefore, freeAfter); +} + +TEST(OSAlloc, SetCurrentHeapReturnsPrevious) { + resetAllocator(); + + OSHeapHandle heapA = OSCreateHeap(gArena.data() + 0x1000, gArena.data() + 0x2000); + OSHeapHandle heapB = OSCreateHeap(gArena.data() + 0x3000, gArena.data() + 0x4000); + ASSERT_GE(heapA, 0); + ASSERT_GE(heapB, 0); + + EXPECT_EQ(OSSetCurrentHeap(heapA), -1); + EXPECT_EQ(OSSetCurrentHeap(heapB), heapA); + + void* p = OSAlloc(128); + ASSERT_NE(p, nullptr); + OSFree(p); + + EXPECT_GE(OSCheckHeap(heapB), 0); +} + +TEST(OSAlloc, DestroyHeapInvalidatesHandle) { + resetAllocator(); + + OSHeapHandle heap = OSCreateHeap(gArena.data() + 0x1000, gArena.data() + 0x2000); + ASSERT_GE(heap, 0); + + OSDestroyHeap(heap); + EXPECT_LT(OSCheckHeap(heap), 0); + EXPECT_EQ(OSAllocFromHeap(heap, 64), nullptr); +} diff --git a/aurora-main/tests/os_test_globals.cpp b/aurora-main/tests/os_test_globals.cpp new file mode 100644 index 0000000..40e4355 --- /dev/null +++ b/aurora-main/tests/os_test_globals.cpp @@ -0,0 +1,5 @@ +#include + +namespace aurora { +AuroraConfig g_config{}; +} diff --git a/aurora-main/tests/test_dvd.cpp b/aurora-main/tests/test_dvd.cpp new file mode 100644 index 0000000..b48f877 --- /dev/null +++ b/aurora-main/tests/test_dvd.cpp @@ -0,0 +1,288 @@ +#include +#include + +#include + +#include +#include +#include + +// ============================================================================= +// Tests that do NOT require a disc image +// ============================================================================= + +TEST(DVDStubs, Constants) { + EXPECT_EQ(DVD_STATE_END, 0); + EXPECT_EQ(DVD_STATE_BUSY, 1); + EXPECT_EQ(DVD_STATE_CANCELED, 10); + EXPECT_EQ(DVD_RESULT_GOOD, 0); + EXPECT_EQ(DVD_RESULT_FATAL_ERROR, -1); + EXPECT_EQ(DVD_RESULT_CANCELED, -6); +} + +TEST(DVDStubs, StructSizes) { + EXPECT_GT(sizeof(DVDDiskID), 0u); + EXPECT_GT(sizeof(DVDCommandBlock), 0u); + EXPECT_GT(sizeof(DVDFileInfo), 0u); + EXPECT_GT(sizeof(DVDDir), 0u); + EXPECT_GT(sizeof(DVDDirEntry), 0u); + EXPECT_GE(sizeof(DVDFileInfo), sizeof(DVDCommandBlock)); +} + +TEST(DVDStubs, InitWithoutDisc) { DVDInit(); } + +TEST(DVDStubs, GetDriveStatus) { EXPECT_EQ(DVDGetDriveStatus(), DVD_STATE_NO_DISK); } + +TEST(DVDStubs, Reset) { DVDReset(); } + +TEST(DVDStubs, ResetRequired) { EXPECT_EQ(DVDResetRequired(), FALSE); } + +TEST(DVDStubs, PauseResume) { + DVDPause(); + DVDResume(); +} + +TEST(DVDStubs, AutoInvalidation) { + BOOL prev = DVDSetAutoInvalidation(TRUE); + EXPECT_EQ(prev, FALSE); + prev = DVDSetAutoInvalidation(FALSE); + EXPECT_EQ(prev, TRUE); + prev = DVDSetAutoInvalidation(FALSE); + EXPECT_EQ(prev, FALSE); +} + +TEST(DVDStubs, Cancel) { + DVDCommandBlock block{}; + block.state = DVD_STATE_BUSY; + s32 result = DVDCancel(&block); + EXPECT_EQ(result, 0); + EXPECT_EQ(block.state, DVD_STATE_CANCELED); +} + +TEST(DVDStubs, CancelAsync) { + DVDCommandBlock block{}; + block.state = DVD_STATE_BUSY; + DVDCancelAsync(&block, [](s32, DVDCommandBlock*) {}); + EXPECT_EQ(block.state, DVD_STATE_CANCELED); +} + +TEST(DVDStubs, CancelAll) { EXPECT_EQ(DVDCancelAll(), DVD_RESULT_GOOD); } + +TEST(DVDStubs, SeekStubs) { + DVDFileInfo fi{}; + fi.cb.state = DVD_STATE_BUSY; + s32 result = DVDSeekPrio(&fi, 0, 2); + EXPECT_EQ(result, DVD_RESULT_FATAL_ERROR); + EXPECT_EQ(fi.cb.state, DVD_STATE_FATAL_ERROR); + + fi.cb.state = DVD_STATE_BUSY; + BOOL ok = DVDSeekAsyncPrio(&fi, 0, [](s32, DVDFileInfo*) {}, 2); + EXPECT_EQ(ok, TRUE); + EXPECT_EQ(fi.cb.state, DVD_STATE_FATAL_ERROR); +} + +TEST(DVDStubs, GetFSTLocation) { EXPECT_EQ(DVDGetFSTLocation(), nullptr); } + +TEST(DVDStubs, GetCurrentDiskID) { + DVDDiskID* id = DVDGetCurrentDiskID(); + EXPECT_NE(id, nullptr); +} + +TEST(DVDStubs, FileInfoStatus) { + DVDFileInfo fi{}; + fi.cb.state = DVD_STATE_END; + EXPECT_EQ(DVDGetFileInfoStatus(&fi), DVD_STATE_END); + fi.cb.state = DVD_STATE_BUSY; + EXPECT_EQ(DVDGetFileInfoStatus(&fi), DVD_STATE_BUSY); +} + +TEST(DVDStubs, TransferredSize) { + DVDFileInfo fi{}; + fi.cb.transferredSize = 1234; + EXPECT_EQ(DVDGetTransferredSize(&fi), 1234); +} + +TEST(DVDStubs, CommandBlockStatus) { + DVDCommandBlock block{}; + block.state = DVD_STATE_WAITING; + EXPECT_EQ(DVDGetCommandBlockStatus(&block), DVD_STATE_WAITING); +} + +// ============================================================================= +// Without a disc: operations should fail gracefully +// ============================================================================= + +TEST(DVDNoDisc, OpenFails) { + DVDFileInfo fi{}; + EXPECT_EQ(DVDOpen("test.bin", &fi), FALSE); +} + +TEST(DVDNoDisc, FastOpenFails) { + DVDFileInfo fi{}; + EXPECT_EQ(DVDFastOpen(0, &fi), FALSE); +} + +TEST(DVDNoDisc, CloseNullHandle) { + DVDFileInfo fi{}; + fi.cb.userData = nullptr; + fi.cb.state = DVD_STATE_BUSY; + EXPECT_EQ(DVDClose(&fi), TRUE); + EXPECT_EQ(fi.cb.state, DVD_STATE_END); +} + +TEST(DVDNoDisc, OpenDirFails) { + DVDDir dir{}; + EXPECT_EQ(DVDOpenDir("/", &dir), FALSE); +} + +TEST(DVDNoDisc, CloseDir) { + DVDDir dir{}; + EXPECT_EQ(DVDCloseDir(&dir), TRUE); +} + +TEST(DVDNoDisc, ChangeDirFails) { EXPECT_EQ(DVDChangeDir("/"), FALSE); } + +TEST(DVDNoDisc, ConvertPathFails) { EXPECT_EQ(DVDConvertPathToEntrynum("/test"), -1); } + +TEST(DVDNoDisc, GetCurrentDir) { + char buf[256]; + EXPECT_EQ(DVDGetCurrentDir(buf, sizeof(buf)), TRUE); + EXPECT_STREQ(buf, "/"); +} + +// ============================================================================= +// Tests that require a disc image (conditionally compiled) +// ============================================================================= + +#ifdef DVD_TEST_IMAGE + +class DVDDiscTest : public ::testing::Test { +protected: + static void SetUpTestSuite() { + ASSERT_TRUE(aurora_dvd_open(DVD_TEST_IMAGE)); + DVDInit(); + } + + static void TearDownTestSuite() { aurora_dvd_close(); } +}; + +// Helper: find the first file entry in root by iterating the directory. +// Returns the entry number and fills fileName, or returns -1 if none found. +static s32 findFirstRootFile(char* fileName, size_t fileNameSize) { + DVDDir dir{}; + if (!DVDOpenDir("/", &dir)) + return -1; + + DVDDirEntry dirent{}; + s32 fileEntry = -1; + while (DVDReadDir(&dir, &dirent)) { + if (!dirent.isDir) { + fileEntry = static_cast(dirent.entryNum); + std::snprintf(fileName, fileNameSize, "/%s", dirent.name); + break; + } + } + DVDCloseDir(&dir); + return fileEntry; +} + +TEST_F(DVDDiscTest, ConvertPathRoot) { EXPECT_EQ(DVDConvertPathToEntrynum("/"), 0); } + +TEST_F(DVDDiscTest, ConvertPathDotDotDot) { + EXPECT_EQ(DVDConvertPathToEntrynum("."), 0); + EXPECT_EQ(DVDConvertPathToEntrynum(".."), 0); +} + +TEST_F(DVDDiscTest, ConvertPathInvalid) { + EXPECT_EQ(DVDConvertPathToEntrynum("/nonexistent_file_that_should_not_exist"), -1); +} + +TEST_F(DVDDiscTest, OpenDirRoot) { + DVDDir dir{}; + EXPECT_EQ(DVDOpenDir("/", &dir), TRUE); + EXPECT_EQ(dir.entryNum, 0u); + EXPECT_EQ(dir.location, 1u); + + DVDDirEntry dirent{}; + BOOL hasEntry = DVDReadDir(&dir, &dirent); + if (hasEntry) { + EXPECT_NE(dirent.name, nullptr); + EXPECT_GT(std::strlen(dirent.name), 0u); + } + DVDCloseDir(&dir); +} + +TEST_F(DVDDiscTest, ChangeDirRoot) { + EXPECT_EQ(DVDChangeDir("/"), TRUE); + char buf[256]; + DVDGetCurrentDir(buf, sizeof(buf)); + EXPECT_STREQ(buf, "/"); +} + +TEST_F(DVDDiscTest, OpenCloseFile) { + char fileName[256] = {}; + s32 fileEntry = findFirstRootFile(fileName, sizeof(fileName)); + if (fileEntry < 0) { + GTEST_SKIP() << "No files in root directory"; + } + + DVDFileInfo fi{}; + EXPECT_EQ(DVDOpen(fileName, &fi), TRUE); + EXPECT_GT(fi.length, 0u); + EXPECT_EQ(fi.cb.state, DVD_STATE_END); + EXPECT_EQ(DVDClose(&fi), TRUE); + + EXPECT_EQ(DVDFastOpen(fileEntry, &fi), TRUE); + EXPECT_EQ(DVDClose(&fi), TRUE); +} + +TEST_F(DVDDiscTest, ReadFile) { + char fileName[256] = {}; + if (findFirstRootFile(fileName, sizeof(fileName)) < 0) { + GTEST_SKIP() << "No files in root directory"; + } + + DVDFileInfo fi{}; + ASSERT_EQ(DVDOpen(fileName, &fi), TRUE); + + u32 readSize = fi.length < 32 ? fi.length : 32; + std::vector buf(readSize); + s32 bytesRead = DVDReadPrio(&fi, buf.data(), static_cast(readSize), 0, 2); + EXPECT_EQ(bytesRead, static_cast(readSize)); + EXPECT_EQ(DVDGetTransferredSize(&fi), static_cast(readSize)); + + DVDClose(&fi); +} + +TEST_F(DVDDiscTest, ReadAsync) { + char fileName[256] = {}; + if (findFirstRootFile(fileName, sizeof(fileName)) < 0) { + GTEST_SKIP() << "No files in root directory"; + } + + DVDFileInfo fi{}; + ASSERT_EQ(DVDOpen(fileName, &fi), TRUE); + + u32 readSize = fi.length < 32 ? fi.length : 32; + std::vector buf(readSize); + BOOL ok = DVDReadAsyncPrio(&fi, buf.data(), static_cast(readSize), 0, [](s32, DVDFileInfo*) {}, 2); + EXPECT_EQ(ok, TRUE); + EXPECT_EQ(DVDGetTransferredSize(&fi), static_cast(readSize)); + + DVDClose(&fi); +} + +TEST_F(DVDDiscTest, DiskID) { + DVDDiskID* id = DVDGetCurrentDiskID(); + ASSERT_NE(id, nullptr); + bool hasGameName = false; + for (int i = 0; i < 4; i++) { + if (id->gameName[i] != '\0') { + hasGameName = true; + break; + } + } + EXPECT_TRUE(hasGameName); +} + +#endif // DVD_TEST_IMAGE diff --git a/aurora-main/tests/texture_bind_group_cache_key_test.cpp b/aurora-main/tests/texture_bind_group_cache_key_test.cpp new file mode 100644 index 0000000..9a28086 --- /dev/null +++ b/aurora-main/tests/texture_bind_group_cache_key_test.cpp @@ -0,0 +1,44 @@ +#include "gx/texture_bind_group_cache_key.hpp" + +#include + +#include +#include +#include + +namespace aurora::gx { +namespace { + +TEST(TextureBindGroupCacheKeyTest, DistinctViewsDoNotAliasWhenWrapperStorageIsReused) { + struct SimulatedTextureRef { + const void* sampleTextureView; + }; + + alignas(SimulatedTextureRef) std::array wrapperStorage{}; + int firstView = 0; + int secondView = 0; + + TextureBindGroupCacheKey first{ + .sampledTextures = 1, + }; + TextureBindGroupCacheKey second = first; + auto* firstWrapper = new (wrapperStorage.data()) SimulatedTextureRef{&firstView}; + const void* const reusedWrapperAddress = firstWrapper; + set_texture_bind_group_cache_slot(first, 0, firstWrapper->sampleTextureView, 0x1234, 0x5678, 0x9abc); + firstWrapper->~SimulatedTextureRef(); + + auto* secondWrapper = new (wrapperStorage.data()) SimulatedTextureRef{&secondView}; + set_texture_bind_group_cache_slot(second, 0, secondWrapper->sampleTextureView, 0x1234, 0x5678, 0x9abc); + + // A freed TextureRef wrapper can be reconstructed at the same allocator + // address in one frame. The old key therefore saw these bindings as equal. + EXPECT_EQ(reusedWrapperAddress, static_cast(secondWrapper)); + EXPECT_NE(first, second); + + TextureBindGroupCacheKey sameView = first; + EXPECT_EQ(first, sameView); + secondWrapper->~SimulatedTextureRef(); +} + +} // namespace +} // namespace aurora::gx diff --git a/projects/examples/generic-dol.yml b/projects/examples/generic-dol.yml new file mode 100644 index 0000000..bc49597 --- /dev/null +++ b/projects/examples/generic-dol.yml @@ -0,0 +1,40 @@ +schema_version: 1 +workspace_root: ../.. + +project: + id: external-generic-dol + display_name: External Generic DOL + +memory: + base: 0x80000000 + size: 0x01800000 + + # Required by every command that emits RuntimeConfig.h. These are the r13/r2 + # Small Data Area bases your DOL's boot code installs: disassemble the lis/ori + # pairs in __init_registers (the first `bl` from the entry point) and paste the + # two constants here. The translator does not guess them. + # sda_base: 0x00000000 # _SDA_BASE_ (r13) + # sda2_base: 0x00000000 # _SDA2_BASE_ (r2) + +inputs: + # Set RECOMP_GENERIC_DOL to a legally obtained GameCube/Wii main.dol. + dol: + path: "%RECOMP_GENERIC_DOL%" + +translation: + allow_unsupported_instructions: true + # Optional. With no function_map the recursive pass seeds from the entry point + # only and discovers the rest through the call graph; there are no heuristic + # function-start scans to fall back on. + # function_map: + # path: my-game/MAP.txt + +runtime: + native_abi_directories: [] + +output: + root: build/generic-dol + functions: functions + runtime_config: RuntimeConfig.h + data_initializer: data_sections_init.cpp + base_manifest: base/base_manifest.json diff --git a/projects/mkwii/MAP.txt b/projects/mkwii/MAP.txt new file mode 100644 index 0000000..98c8a56 --- /dev/null +++ b/projects/mkwii/MAP.txt @@ -0,0 +1,29792 @@ +800018a8 0x800018a8 +80001d50 0x80001d50 +80001d6c 0x80001d6c +80001d90 0x80001d90 +80001dc0 0x80001dc0 +80001de8 0x80001de8 +80001dec 0x80001dec +80001e14 0x80001e14 +80001e30 0x80001e30 +80001f1c 0x80001f1c +80001f40 0x80001f40 +80001f54 0x80001f54 +80002440 0x80002440 +8000245c 0x8000245c +8000249c __TRKreset +80005f34 memcpy +80005f84 __fill_mem +80006038 memset +80006068 __check_pad3 +80006090 __set_debug_bba +8000609c __get_debug_bba +800060a4 __start +80006210 __init_registers +800062a0 __init_data +80006348 __init_hardware +8000636c __flush_cache +800063a0 0x800063a0 +800072c0 StrapScene::__ct +800073f0 RKScene::__dt +80007448 StrapScene::__dt +800074a4 StrapScene::LinkModule +800074d8 StrapScene::enter +800077c8 StrapScene::CheckInput +800079d0 StrapScene::calc +80007bc0 EGG::ColorFader::getStatus +80007bc8 StrapScene::draw +80007f2c StrapScene::exit +80007f34 EGG::Scene::outgoing_child_create +80007f38 EGG::Scene::incoming_child_destroy +80007f3c EGG::Scene::reinit +80007f40 EGG::Scene::exit +80007f44 EGG::Scene::enter +80007f48 EGG::Scene::draw +80007f4c EGG::Scene::calc +80007f50 getContentArcNameByIdx +80007f64 0x80007f64 +80007f7c DiscError::__ct +80007fac DiscError::__dt +80008004 setDiscReadErrorMessage +800080f4 0x800080f4 +800080f8 DiscInfoMessageData_printError +80008bd0 nw4r::ut::Color::__dt +80008c10 DiscCheckThread::create +80008cb0 DiscCheckThread::__dt +80008d18 DvdThread_main +80008e20 DiscCheckThread::halt +80008e74 DiscInfo_printError +80008e7c 0x80008e7c +80008e80 0x80008e80 +80008e84 RKSystem::GetStaticInstance +80008e90 RKSystem::WPADAlloc +80008eb0 RKSystem::WPADFree +80008ef0 RKSystem::Main +80008fac RKSystem::GetSystemHeap +80008fb4 EGG::TSystem::Initialize +80009190 EGG::TSystem::InitRenderMode +80009194 RKSystem::Initialize +8000951c RKSystem::Run +80009818 EGG::TSystem::GetDisplay +80009820 EGG::TSystem::OnBeginFrame +80009824 EGG::TSystem::OnEndFrame +80009828 EGG::TSystem::GetSceneMgr +80009830 EGG::TSystem::GetPerfView +80009844 RKSceneManager::ChangeSceneByCreatorAfterFadeOut +8000984c RKSceneManager::calcCurrentFader +80009984 RKSceneManager::calc +80009988 RKSceneManager::draw +8000998c RKSceneManager::drawCurrentFaderImpl +8000999c RKSceneManager::calcCurrentFaderImpl +800099ac EGG::TSystem::GetVideo +800099b4 EGG::TSystem::GetSystemHeap +800099bc EGG::TSystem::GetXfbMgr +800099c4 EGG::TSystem::GetAudioMgr +800099cc __sinit_\RKSystem_cpp +80009b40 EGG::Vector3f::__dt +80009b80 EGG::Vector2f::__dt +80009bc0 ExpHeapGroup::SetHeapsGroupId +80009c24 ExpHeapGroup::CalculateGroupSizeRecords +80009c8c RKParameterFile::__ct +80009ce0 RKParameterFile::__dt +80009d6c RKParameterFile::AddParameter +80009ddc RKParameterFile::ReadParameterFile +80009ef8 RamStream::__dt +80009f50 RKParameterFile::vf_0x18 +80009f54 RKParameterFile::vf_0x14 +80009f58 RKParameterFile::vf_0x10 +80009f5c ModuleLinker::Disposer__dt +80009ff0 ModuleLinker::Create +8000a000 0x8000a000 +8000a06c ModuleLinker::0x8000a06c +8000a070 ModuleLinker::0x8000a070 +8000a074 ModuleLinker::__dt +8000a188 ModuleLinker::Link +8000a3f8 ModuleLinker::CallModule +8000a440 ModuleLinker::Report +8000a550 RKSceneCreator::create +8000a5a8 RKSceneCreator::destroy +8000a5d0 SystemManager::create +8000a624 SystemManager::__dt +8000a688 SystemManager::__ct +8000a8f0 SystemManager::PrepareLaunch +8000ab88 SystemManager::LoadMapFile +8000ac50 SystemManager::Init +8000af78 SystemManager::GetCountry +8000afec SystemManager::GetLongitude +8000b0a0 SystemManager::GetLatitude +8000b154 SystemManager::Shutdown +8000b188 SystemManager::ReturnToMenu +8000b1bc SystemManager::RestartGame +8000b1f4 0x8000b1f4 +8000b210 0x8000b210 +8000b22c SystemManager::SetSystemPowerCallbacks +8000b26c SystemManager::HandlePowerState +8000b2d8 SystemManager::GetGameId +8000b2e0 SystemManager::GenerateGameId +8000b308 SystemManager::RipFromDiscAsync +8000b370 SystemManager::RipFromDiscImpl +8000b53c SystemManager::ProcessRipRequest +8000b5c8 SystemManager::RipFromDisc(heap) +8000b5f0 SystemManager::RipFromDisc(no heap) +8000b610 SystemManager::GetDisplayModeType +8000b654 SystemManager::StopAllControllerMotors +8000b6b0 Main +8000b6b4 __DBMtrHandler +8000b6d8 __DBIntrHandler +8000b6fc DBInitComm +8000b758 DBInitInterrupts +8000b7ac DBQueryData +8000b850 DBRead +8000b8d0 DBWrite +8000b9e0 DBOpen +8000b9e4 DBClose +8000b9e8 __EXI2Imm +8000bcd0 __DBEXIInit +8000bd8c __DBEXIReadReg +8000beb0 __DBEXIWriteReg +8000bfb4 __DBEXIReadRam +8000c094 __DBEXIWriteRam +8000c174 _ftell +8000c21c ftell +8000c220 _fseek +8000c3e4 fseek +8000c3e8 rewind +8000c430 atexit +8000c434 Block_link +8000c584 SubBlock_merge_next +8000c62c deallocate_from_fixed_pools +8000c818 free +8000c948 __close_all +8000c9ec __flush_line_buffered_output_files +8000ca70 __flush_all +8000cadc __ull2dec +8000cbb8 __timesdec +8000ce40 __str2dec +8000cf2c __two_exp +8000cf68 __two_exp_switch +8000cf6c __two_exp_caseD_ffffffc0 +8000cf80 __two_exp_caseD_ffffffcb +8000cf98 __two_exp_caseD_ffffffe0 +8000cfb0 __two_exp_caseD_fffffff0 +8000cfc8 __two_exp_caseD_fffffff8 +8000cfe0 __two_exp_caseD_fffffff9 +8000cff8 __two_exp_caseD_fffffffa +8000d010 __two_exp_caseD_fffffffb +8000d028 __two_exp_caseD_fffffffc +8000d040 __two_exp_caseD_fffffffd +8000d058 __two_exp_caseD_fffffffe +8000d070 __two_exp_caseD_ffffffff +8000d088 __two_exp_caseD_0 +8000d0a0 __two_exp_caseD_1 +8000d0b8 __two_exp_caseD_2 +8000d0d0 __two_exp_caseD_3 +8000d0e8 __two_exp_caseD_4 +8000d100 __two_exp_caseD_5 +8000d118 __two_exp_caseD_6 +8000d130 __two_exp_caseD_7 +8000d148 __two_exp_caseD_8 +8000d160 __two_exp_caseD_ffffffc1 +8000d298 __equals_dec +8000d37c __less_dec +8000d47c __minus_dec +8000d998 __num2dec_internal +8000dafc __num2dec +8000dc9c __dec2num +8000e434 __prep_buffer +8000e45c __load_buffer +8000e558 __flush_buffer +8000e610 fread +8000e614 __fread +8000e954 __fwrite +8000ec5c fclose +8000ed18 fflush +8000ee4c __msl_strnicmp +8000ef04 __fpclassifyf +8000ef64 __signbitd +8000ef7c __fpclassifyd +8000eff8 mbtowc +8000f010 __mbtowc_noconv +8000f05c __wctomb_noconv +8000f078 mbstowcs +8000f138 wcstombs +8000f1f0 memmove +8000f2bc memchr +8000f2e8 __memrchr +8000f314 memcmp +8000f360 __copy_longs_aligned +8000f41c __copy_longs_rev_aligned +8000f4c4 __copy_longs_unaligned +8000f584 __copy_longs_rev_unaligned +8000f630 __stdio_atexit +8000f640 parse_format +8000f9d0 parse_format_switch +8000f9d4 parse_format_caseD_58 +8000fa1c parse_format_caseD_46 +8000fa64 parse_format_caseD_41 +8000fab0 parse_format_caseD_47 +8000fac4 parse_format_caseD_45 +8000fb14 parse_format_caseD_70 +8000fb38 parse_format_caseD_63 +8000fb70 parse_format_caseD_73 +8000fb80 0x8000fb80 +8000fb9c 0x8000fb80_caseD_6e +8000fbb4 0x8000fb80_caseD_42 +8000fbfc long2str +8000fe34 longlong2str +80010000 0x80010000 +800100e4 double2hex +80010138 0x80010138 +800104b4 round_decimal +800105dc float2str +80010d74 _pformatter +80010dbc 0x80010dbc +80010e74 _pformatter_switch +80010e78 _pformatter_caseD_64 +80010fd4 _pformatter_caseD_58 +80011130 _pformatter_caseD_45 +800111a0 _pformatter_caseD_41 +80011210 _pformatter_caseD_73 +80011334 _pformatter_caseD_6e +800113ec _pformatter_caseD_63 +8001140c _pformatter_caseD_25 +8001141c _pformatter_caseD_26 +80011620 __FileWrite +80011678 __StringWrite +800116e4 printf +800117b0 vprintf +8001182c vsnprintf +800118b4 vsprintf +80011938 snprintf +80011a2c sprintf +80011b00 qsort +80011c70 rand +80011c90 srand +80011c98 parse_format +80011f6c parse_format_switch +80011f70 parse_format_caseD_58 +80011f88 parse_format_caseD_41 +80011fd0 parse_format_caseD_70 +80011fe4 parse_format_caseD_63 +80012010 parse_format_caseD_73 +800120c8 parse_format_caseD_5b +800122b4 parse_format_caseD_42 +800122bc parse_format_caseD_6e +80012320 __sformatter +8001237c 0x8001237c +800125a4 __sformatter_switch +800125a8 __sformatter_caseD_64 +800125b0 __sformatter_caseD_69 +800126f0 __sformatter_switch +800126f4 __sformatter_caseD_0 +800126fc __sformatter_caseD_1 +80012704 __sformatter_caseD_2 +8001270c __sformatter_caseD_3 +80012714 __sformatter_caseD_4 +80012728 __sformatter_caseD_5 +80012730 __sformatter_caseD_6 +80012738 __sformatter_caseD_7 +8001275c __sformatter_caseD_6f +80012764 __sformatter_caseD_75 +8001276c __sformatter_caseD_58 +80012888 __sformatter_switch +8001288c __sformatter_caseD_0 +80012894 __sformatter_caseD_1 +8001289c __sformatter_caseD_2 +800128a4 __sformatter_caseD_3 +800128ac __sformatter_caseD_4 +800128c0 __sformatter_caseD_5 +800128c8 __sformatter_caseD_6 +800128d0 __sformatter_caseD_7 +800128f4 __sformatter_caseD_41 +8001299c __sformatter_caseD_63 +80012b48 __sformatter_caseD_25 +80012bf0 __sformatter_caseD_73 +80012c94 __sformatter_caseD_5b +80012efc __sformatter_caseD_6e +80012f6c __sformatter_caseD_26 +80012fb8 __StringRead +80013040 sscanf +80013108 raise +80013120 strcpy +800131e0 strncpy +80013224 strcat +80013250 strncat +8001329c strcmp +800133b8 strncmp +800133f8 strchr +80013428 strcspn +800134cc strtok +800135f0 strstr +8001365c __strtold +80014990 atof +800149ec __strtoul +80014e00 __strtoull +800152a8 strtoul +80015350 strtol +8001543c atoi +80015500 __write_console +800155d0 0x800155d0 +800155d8 fwide +80015650 wmemcpy +80015658 wmemchr +80015680 parse_format +800159d0 parse_format_switch +800159d4 parse_format_caseD_58 +80015a18 parse_format_caseD_46 +80015a64 parse_format_caseD_41 +80015ab4 parse_format_caseD_47 +80015ac8 parse_format_caseD_45 +80015b1c parse_format_caseD_70 +80015b40 parse_format_caseD_63 +80015b7c parse_format_caseD_73 +80015bac parse_format_caseD_6e +80015bc4 parse_format_caseD_42 +80015c10 long2str +80015e54 longlong2str +80016110 double2hex +80016568 round_decimal +80016690 float2str +80016e1c __wpformatter +80016e68 0x80016e68 +80016f2c __wpformatter_switch +80016f30 __wpformatter_caseD_64 +8001708c __wpformatter_caseD_58 +800171e8 __wpformatter_caseD_45 +80017264 __wpformatter_caseD_41 +800172e0 __wpformatter_caseD_73 +8001748c __wpformatter_caseD_6e +80017544 __wpformatter_caseD_63 +8001759c __wpformatter_caseD_25 +800175ac __wpformatter_caseD_26 +800177a8 __wStringWrite +80017814 swprintf +8001790c vswprintf +80017998 wcslen +800179b4 wcscpy +800179d0 wcsncpy +80017a14 wcscmp +80017a48 wcschr +80017a74 __ieee754_acos +80017d40 __ieee754_asin +80017fd8 __ieee754_atan2 +80018230 __ieee754_fmod +80018560 __ieee754_log +80018814 __ieee754_log10 +80018928 __ieee754_pow +8001913c __ieee754_rem_pio2 +800194c0 __ieee754_sqrt +80019708 __kernel_cos +80019818 __kernel_rem_pio2 +8001aed0 __kernel_sin +8001af90 __kernel_tan +8001b1e0 nan +8001b1e4 scalbn +8001b1e8 atan +8001b418 ceil +8001b564 copysign +8001b590 cos +8001b658 floor +8001b7a8 frexp +8001b830 ldexp +8001b99c modf +8001ba98 sin +8001bb64 tan +8001bbdc acos +8001bbe0 asin +8001bbe4 atan2 +8001bbe8 fmod +8001bbec log10 +8001bbf0 pow +8001bbf4 sqrt +8001bbf8 stricmp +8001bc98 strncasecmp +8001bc9c strcasecmp +8001bd3c CBGetBytesAvailableForRead +8001bd44 CircleBufferInitialize +8001bd68 CircleBufferWriteBytes +8001be70 CircleBufferReadBytes +8001bf7c MWEnterCriticalSection +8001bfac MWExitCriticalSection +8001bfb4 gdev_cc_initialize +8001bff0 0x8001bff0 +8001bff8 0x8001bff8 +8001c01c 0x8001c01c +8001c024 gdev_cc_read +8001c0d8 gdev_cc_write +8001c14c gdev_cc_pre_continue +8001c170 gdev_cc_post_stop +8001c194 gdev_cc_peek +8001c208 gdev_cc_initinterrupts +8001c22c TRK_DispatchMessage +8001c270 TRK_DispatchMessage_switch +8001c274 TRK_DispatchMessage_caseD_1 +8001c284 TRK_DispatchMessage_caseD_2 +8001c294 TRK_DispatchMessage_caseD_3 +8001c2a4 TRK_DispatchMessage_caseD_7 +8001c2b4 TRK_DispatchMessage_caseD_10 +8001c2c4 TRK_DispatchMessage_caseD_11 +8001c2d4 TRK_DispatchMessage_caseD_12 +8001c2e4 TRK_DispatchMessage_caseD_13 +8001c2f4 TRK_DispatchMessage_caseD_18 +8001c304 TRK_DispatchMessage_caseD_19 +8001c314 TRK_DispatchMessage_caseD_1a +8001c324 TRK_DispatchMessage_caseD_17 +8001c330 TRK_DispatchMessage_caseD_0 +8001c34c InitMetroTRK +8001c3e4 InitMetroTRK_BBA +8001c478 EnableMetroTRKInterrupts +8001c47c TRKTargetTranslate +8001c4e4 __TRK_copy_vectors +8001c618 TRKInitializeTarget +8001c660 __TRKreset +8001c670 TRKLoadContext +8001c6f8 TRKEXICallBack +8001c730 InitMetroTRKCommTable +8001c868 TRKUARTInterruptHandler +8001c86c TRK_InitializeIntDrivenUART +8001c8ec TRKPollUART +8001c900 TRKReadUARTN +8001c93c TRK_WriteUARTN +8001c978 ReserveEXI2Port +8001c98c UnreserveEXI2Port +8001c9a0 0x8001c9a0 +8001c9b8 InitializeProgramEndTrap +8001ca0c TRK_flush_cache +8001ca44 TRK_main +8001ca80 TRK_NubMainLoop +8001cb6c TRK_memcpy +8001cca8 TRK_memset +8001cdd0 TRKSaveExtended1Block +8001cf94 TRKRestoreExtended1Block +8001d0f8 TRK_MessageSend +8001d15c TRK_InitializeMessageBuffers +8001d17c TRK_GetFreeBuffer +8001d218 TRKGetBuffer +8001d23c TRK_ReleaseBuffer +8001d264 TRKResetBuffer +8001d28c TRK_SetBufferPosition +8001d2bc TRK_AppendBuffer +8001d360 TRK_ReadBuffer +8001d3f0 TRKAppendBuffer1_ui32 +8001d4c0 TRKAppendBuffer1_ui64 +8001d5b4 TRKAppendBuffer_ui8 +8001d618 TRKAppendBuffer_ui32 +8001d708 TRKReadBuffer1_ui64 +8001d7e8 TRKReadBuffer_ui8 +8001d880 TRKReadBuffer_ui32 +8001d968 GetTRKConnected +8001d970 TRK_DoConnect +8001d9e0 TRKDoDisconnect +8001da68 TRKDoReset +8001dad4 TRKDoOverride +8001db40 TRKDoReadMemory +8001dccc TRKDoReadMemory_switch +8001dcd0 TRKDoReadMemory_caseD_702 +8001dcd8 TRKDoReadMemory_caseD_700 +8001dce0 TRKDoReadMemory_caseD_704 +8001dce8 TRKDoReadMemory_caseD_705 +8001dcf0 TRKDoReadMemory_caseD_706 +8001dcf8 TRKDoReadMemory_caseD_701 +8001dd74 TRKDoWriteMemory +8001dedc TRKDoWriteMemory_switch +8001dee0 TRKDoWriteMemory_caseD_702 +8001dee8 TRKDoWriteMemory_caseD_700 +8001def0 TRKDoWriteMemory_caseD_704 +8001def8 TRKDoWriteMemory_caseD_705 +8001df00 TRKDoWriteMemory_caseD_706 +8001df08 TRKDoWriteMemory_caseD_701 +8001df84 TRKDoReadRegisters +8001e180 TRKDoWriteRegisters +8001e420 TRKDoContinue +8001e4e4 TRKDoStep +8001e768 TRKDoStop +8001e820 TRKDoSetOption +8001e8ec __read_console +8001e954 __TRK_write_console +8001e9bc __read_file +8001e9c4 __access_file +8001e9cc 0x8001e9cc +8001ea78 TRKDoNotifyStopped +8001eb08 TRKInitializeEventQueue +8001eb2c TRKGetNextEvent +8001ebb8 TRKPostEvent +8001ec84 TRKConstructEvent +8001ec9c TRKDestructEvent +8001eca4 TRK_InitializeNub +8001ed50 TRK_TerminateNub +8001ed74 TRK_board_display +8001ed80 TRK_InitializeEndian +8001edf4 TRKTestForPacket +8001eec0 TRKGetInput +8001eeec TRKProcessInput +8001ef2c TRKInitializeSerialHandler +8001ef34 TRKTerminateSerialHandler +8001ef3c TRK_strlen +8001ef58 TRK_SuppAccessFile +8001f14c TRK_RequestSend +8001f274 HandleOpenFileSupportRequest +8001f38c HandleCloseFileSupportRequest +8001f470 HandlePositionFileSupportRequest +8001f57c TRKTargetContinue +8001f5b0 SetUseSerialIO +8001f5b8 GetUseSerialIO +8001f5c0 __TRK_get_MSR +8001f5c8 __TRK_set_MSR +8001f5d0 TRKValidMemory32 +8001f6c8 TRK_ppc_memcpy +8001f794 TRKTargetAccessMemory +8001f8e4 TRKTargetAccessDefault +8001f9dc TRKTargetAccessFP +8001fb18 TRKTargetAccessExtended1 +8001fc7c TRKTargetAccessExtended2 +80020000 0x80020000 +8002001c 0x8002001c +800200d4 TRKSwapAndGo +800201ec TRKTargetInterrupt +80020248 TRKTargetAddStopInfo +8002037c TRKTargetAddExceptionInfo +80020418 TRKTargetCheckStep +80020534 TRKTargetSingleStep +8002055c 0x8002055c +800205b8 TRKTargetStepOutOfRange +80020628 TRKTargetGetPC +80020638 TRKTargetSupportRequest +80020800 0x80020800 +8002082c TRKTargetStopped +8002083c TRKTargetSetStopped +8002084c TRKTargetStop +80020864 TRKPPCAccessSPR +80020940 TRKPPCAccessPairedSingleRegister +800209e8 ReadFPSCR +80020a0c WriteFPSCR +80020a30 TRKPPCAccessFPRegister +80020bf8 TRKPPCAccessSpecialReg +80020c60 TRKTargetSetInputPendingPtr +80020c70 GetThreadInfo +80020d00 TRKAccessFile +80020d08 0x80020d08 +80020d10 0x80020d10 +80020d18 0x80020d18 +80020d20 __sys_free +80020dd8 __register_fragment +80020e0c __unregister_fragment +80020e34 __construct_new_array +80020f38 __partial_array_destructor::__dt +80020ff4 __construct_array +800210ec __destroy_array +80021164 __destroy_new_array +800211e4 __init_cpp_exceptions +80021220 __fini_cpp_exceptions +80021254 strlen +80021270 __va_arg +80021338 __register__global_object +80021350 __destroy__global_chain +80021360 0x80021360 +8002137c 0x8002137c +80021398 __register_atexit +800213e4 __ptmf_test +80021414 __ptmf_cmpr +80021450 __ptmf_scall +80021478 __cvt_fp2unsigned +800214d4 __save_fpr +800214f8 _save_fpr_23 +8002150c _save_fpr_28 +80021520 __restore_fpr +80021544 _rest_fpr_23 +80021558 _rest_fpr_28 +8002156c _save_gpr_14 +80021570 _save_gpr_15 +80021574 _save_gpr_16 +80021578 _save_gpr_17 +8002157c _save_gpr_18 +80021580 _save_gpr_19 +80021584 _save_gpr_20 +80021588 _save_gpr_21 +8002158c _save_gpr_22 +80021590 _save_gpr_23 +80021594 _save_gpr_24 +80021598 _save_gpr_25 +8002159c _save_gpr_26 +800215a0 _save_gpr_27 +800215b8 _rest_gpr_14 +800215bc _rest_gpr_15 +800215c0 _rest_gpr_16 +800215c4 _rest_gpr_17 +800215c8 _rest_gpr_18 +800215cc _rest_gpr_19 +800215d0 _rest_gpr_20 +800215d4 _rest_gpr_21 +800215d8 _rest_gpr_22 +800215dc _rest_gpr_23 +800215e0 _rest_gpr_24 +800215e4 _rest_gpr_25 +800215e8 _rest_gpr_26 +800215ec _rest_gpr_27 +80021604 __div2u +800216f0 __div2i +80021828 __mod2u +8002190c __mod2i +80021a18 __shl2i +80021a3c __shr2u +80021a60 __cvt_ull_db +80021b00 __cvt_dbl_ull +80021ba8 0x80021ba8 +80021bb0 nw4r::db::DirectPrint_Init +80021c70 nw4r::db::DirectPrint_IsActive +80021ca0 nw4r::db::DirectPrint_EraseXfb +80021e30 nw4r::db::DirectPrint_ChangeXfb +80021e70 nw4r::db::DirectPrint_StoreCache +80021e90 nw4r::db::DirectPrint_DrawString +80021f80 nw4r::db::DrawStringToXfb +800220e0 nw4r::db::DrawStringLineToXfb +800221c0 nw4r::db::DrawCharToXfb +80022480 nw4r::db::DirectPrint_SetupFB +80022670 nw4r::db::RemoveConsoleFromList_ +80022720 nw4r::db::Console_Create +800228d0 nw4r::db::Console_Destroy +80022900 nw4r::db::DoDrawConsole_ +80022ab0 nw4r::db::Console_DrawDirect +80022b30 nw4r::db::PrintToBuffer_ +80022ee0 nw4r::db::Console_VFPrintf +80022fc0 nw4r::db::Console_Printf +800230e0 nw4r::db::Console_GetTotalLines +80023150 nw4r::db::Exception_Init +80023240 nw4r::db::ErrorHandler_ +80023380 nw4r::db::RunThread_ +80023410 nw4r::db::DumpException_ +800235a0 nw4r::db::Exception_Printf_ +80023680 nw4r::db::PrintContext_ +80023980 nw4r::db::ShowMainInfo_ +80023bc0 nw4r::db::ShowGPRMap_ +80023c70 nw4r::db::ShowFloat_ +80024080 nw4r::db::Exception_SetConsole +800240a0 nw4r::db::Exception_SetUserCallback +800240c0 nw4r::db::DbgPrintBase::GetInstance +800240d0 nw4r::db::DbgPrintBase::Initialize +80024190 nw4r::db::DbgPrintBase::Initialize +80024250 nw4r::db::DbgPrintBase::SetFont +80024260 nw4r::db::DbgPrintBase::GetFont +80024270 nw4r::db::DbgPrintBase::SetTextColor +800242a0 nw4r::db::DbgPrintBase::GetTextColor +800242d0 nw4r::db::DbgPrintBase::SetFontSize +800242e0 nw4r::db::DbgPrintBase::GetFontSize +800242f0 nw4r::db::DbgPrintBase::SetVisible +80024300 nw4r::db::DbgPrintBase::IsVisible +80024310 nw4r::db::DbgPrintBase::Registerf +80024420 nw4r::db::DbgPrintBase::Registerf +80024540 nw4r::db::DbgPrintBase::VRegisterf +80024610 nw4r::db::DbgPrintBase::Register +800246c0 nw4r::db::DbgPrintBase::Registf +800247d0 nw4r::db::DbgPrintBase::Registf +80024870 0x80024868 +800248f0 nw4r::db::DbgPrintBase::VRegistf +800249c0 nw4r::db::DbgPrintBase::Reset +800249d0 0x800249d0 +80024a70 nw4r::db::DbgPrintBase::Flush +80024c10 nw4r::db::DbgPrintBase::Flush +80024cd0 nw4r::db::DbgPrintBase::__ct +80024d40 nw4r::db::DbgPrintBase::__dt +80024d90 nw4r::db::DbgPrintBase::Unregister +80024db0 0x80024db0 +80024df0 nw4r::db::DbgPrintBase::SetBuffer +80024e20 nw4r::db::DbgPrintBase::ReleaseBuffer +80024e80 nw4r::db::DbgPrintBase::Operator.new +80024ed0 nw4r::db::DbgPrintBase::GetInstance +80024ef0 nw4r::db::DbgPrintBase::Initialize +80024fb0 nw4r::db::DbgPrintBase::Initialize +80025070 nw4r::db::DbgPrintBase::SetFont +80025080 nw4r::db::DbgPrintBase::GetFont +80025090 nw4r::db::DbgPrintBase::SetTextColor +800250c0 nw4r::db::DbgPrintBase::GetTextColor +800250f0 nw4r::db::DbgPrintBase::SetFontSize +80025100 nw4r::db::DbgPrintBase::GetFontSize +80025110 nw4r::db::DbgPrintBase::SetVisible +80025120 nw4r::db::DbgPrintBase::IsVisible +80025130 nw4r::db::DbgPrintBase::Registerf +80025240 nw4r::db::DbgPrintBase::Registerf +80025360 nw4r::db::DbgPrintBase::VRegisterf +80025430 nw4r::db::DbgPrintBase::Register +800254e0 nw4r::db::DbgPrintBase::Registf +800255f0 nw4r::db::DbgPrintBase::Registf +80025710 nw4r::db::DbgPrintBase::VRegistf +800257e0 nw4r::db::DbgPrintBase::Reset +800257f0 0x800257f0 +80025890 nw4r::db::DbgPrintBase::Flush +800259d0 0x800259d0 +80025a30 nw4r::db::DbgPrintBase::Flush +80025af0 nw4r::db::DbgPrintBase::__ct +80025b60 nw4r::db::DbgPrintBase::__dt +80025bb0 nw4r::db::DbgPrintBase::Unregister +80025bd0 0x80025bd0 +80025c10 nw4r::db::DbgPrintBase::SetBuffer +80025c40 nw4r::db::DbgPrintBase::ReleaseBuffer +80025ca0 nw4r::db::DbgPrintBase::Operator.new +80025cf0 nw4r::db::DbgPrintBase::Operator.delete +80025d00 nw4r::db::Assertion_Printf_ +80025dc0 PrintExceptionTrace +80025e70 nw4r::db::VPanic +80025fb0 nw4r_db_Panic +80026050 nw4r::db::VWarning +80026200 nw4r::db::WarningAlarmFunc_ +80026220 nw4r::ef::DrawOrder::Draw +800262b0 nw4r::ef::DrawOrder::Add +80026330 nw4r::ef::DrawOrder::Remove +80026340 nw4r::ef::Effect::__ct +800263b0 nw4r::ef::Effect::__dt +80026410 nw4r::ef::Effect::Initialize +80026550 nw4r::ef::Effect::Closing +80026560 0x80026560 +80026570 0x80026570 +80026680 nw4r::ef::Effect::CreateEmitter +80026780 nw4r::ef::Effect::RetireEmitter +80026810 nw4r::ef::Effect::RetireEmitterAll +800268e0 nw4r::ef::Effect::RetireParticleAll +80026970 nw4r::ef::Effect::Calc +80026d60 nw4r::ef::Effect::Draw +80026d90 nw4r::ef::Effect::GetRootEmitter +80026da0 nw4r::ef::Effect::GetNumEmitter +80026db0 nw4r::ef::Effect::GetEmitter +80026dc0 nw4r::ef::Effect::ForeachParticleManager +80026e50 nw4r::ef::Effect::ForeachEmitterFrom +80026f30 nw4r::ef::Effect::SetRootMtx +80026fa0 nw4r::ef::Effect::CreateEmitter +800270a0 nw4r::ef::EffectSystem::__dt +80027130 nw4r::ef::EffectSystem::GetInstance +80027140 nw4r::ef::EffectSystem::Initialize +80027250 nw4r::ef::ActivityList::__ct +800272a0 nw4r::ef::EffectSystem::Closing +80027310 nw4r::ef::EffectSystem::CreateEffect(name) +80027380 nw4r::ef::EffectSystem::CreateEffect(res) +800274b0 nw4r::ef::EffectSystem::RetireEffectAll +800275a0 nw4r::ef::EffectSystem::RetireEmitterAll +80027630 nw4r::ef::EffectSystem::RetireParticleAll +800276c0 nw4r::ef::EffectSystem::Draw +80027750 nw4r::ef::EffectSystem::SetProcessCamera +800277e0 nw4r::ef::Swap +80027830 nw4r::ef::EffectSystem::SortEffect +800279b0 nw4r::ef::EffectSystem::SortEffectOperatorZ +80027ab0 __sinit_\ef_effectsystem_cpp +80027be0 nw4r::ef::Emitter::__ct +80027c40 nw4r::ef::Emitter::__dt +80027c80 nw4r::ef::Emitter::RetireParticleAll +80027d10 nw4r::ef::Emitter::DestroyFunc +80027d20 0x80027d20 +80027e10 nw4r::ef::Emitter::Closing +80027ec0 nw4r::ef::Emitter::RetireParticleManagerAll +80027f90 nw4r::ef::Emitter::InitializeDatas +80028260 nw4r::ef::Emitter::Initialize +800283d0 nw4r::ef::Emitter::CreateEmitter +800286b0 nw4r::ef::Emitter::CreateEmitterTmp +80028d90 nw4r::ef::GetLODratio +80028e90 nw4r::ef::Emitter::Emission +800292e0 nw4r::ef::Emitter::CalcEmitter +800294a0 nw4r::ef::Emitter::CalcParticle +80029530 nw4r::ef::Emitter::CalcEmission +80029650 nw4r::ef::Emitter::CalcBillboard +80029830 nw4r::ef::Emitter::RestructMatrix +80029a60 nw4r::ef::Emitter::CalcGlobalMtx +80029bb0 nw4r::ef::Emitter::SetMtxDirty +80029cc0 nw4r::ef::Emitter::GetNumParticleManager +80029cd0 nw4r::ef::Emitter::GetParticleManager +80029ce0 nw4r::ef::ForeachParticleManagerSub +80029d30 nw4r::ef::Emitter::ForeachParticleManager +80029e20 nw4r::ef::AnimCurveExecuteU8 +8002b100 nw4r::ef::AnimCurveExecuteF32 +8002c0b0 nw4r::ef::AnimCurveExecuteRotate +8002d160 nw4r::ef::AnimCurveExecuteTexture +8002d730 nw4r::ef::CreateChild +8002d8a0 nw4r::ef::AnimCurveExecuteChild +8002e660 nw4r::ef::AnimBakedExecuteU8 +8002e9b0 nw4r::ef::AnimBakedExecuteF32 +8002ed00 nw4r::ef::GetTypePositionSphere +8002ee20 nw4r::ef::GetTypePositionCube +8002ef00 nw4r::ef::GetTypePositionCylinder +8002efd0 nw4r::ef::GetTypePositionCircle +8002f0e0 nw4r::ef::GetTypePositionRectangle +8002f1d0 nw4r::ef::GetTypePositionPlane +8002f280 nw4r::ef::CrossCheckPlane +8002f720 nw4r::ef::CrossCheckRectangle +8002fcc0 nw4r::ef::CrossCheckCircle +80030250 nw4r::ef::CrossCheckSphere +80030890 nw4r::ef::CrossCheckCylinder +80031080 nw4r::ef::CrossCheckCube +800317c0 nw4r::ef::CrossCheck +80031be0 nw4r::ef::CalcPostField +80032440 nw4r::ef::Particle::__ct +80032450 nw4r::ef::Particle::__dt +80032490 nw4r::ef::Particle::SendClosing +800324a0 nw4r::ef::Particle::DestroyFunc +800324b0 nw4r::ef::ParticleParameter::Initialize +800327d0 nw4r::ef::Particle::Initialize +800331f0 nw4r::ef::Particle::GetAlphaFlick +80033430 nw4r::ef::ParticleManager::__ct +80033490 nw4r::ef::ParticleManager::SendClosing +800334a0 nw4r::ef::ParticleManager::DestroyFunc +800334b0 nw4r::ef::ParticleManager::Closing +80033540 nw4r::ef::ParticleManager::RetireParticleAll +800335f0 nw4r::ef::ParticleManager::Initialize +80033740 nw4r::ef::ParticleManager::__dt +80033780 nw4r::ef::ParticleManager::CreateParticle +800338b0 nw4r::ef::ParticleManager::Calc +80034f80 nw4r::ef::ParticleManager::Draw +80035260 nw4r::ef::ParticleManager::CalcGlobalMtx +80035340 nw4r::ef::ParticleManager::BeginCalc +800353b0 nw4r::ef::ParticleManager::EndCalc +80035400 nw4r::ef::ParticleManager::BeginDraw +80035500 nw4r::ef::ParticleManager::EndDraw +80035510 nw4r::ef::ParticleManager::Draw_ModifyColor +80035930 __sinit_\ef_particlemanager_cpp +80035940 nw4r::ef::Resource::GetInstance +80035950 nw4r::ef::BreffRelocate +80035ac0 nw4r::ef::BreffSearchName +80035b80 nw4r::ef::BreffIndexOf +80035c20 nw4r::ef::BreftRelocate +80035cb0 nw4r::ef::BreftSearchName +80035d70 nw4r::ef::Resource::Add +80035df0 nw4r::ef::Resource::FindEmitter +80035eb0 nw4r::ef::Resource::FindEmitter +80035f60 nw4r::ef::Resource::AddTexture +80035fe0 nw4r::ef::Resource::RelocateCurveTexture +80036100 nw4r::ef::Resource::RelocateCurveChild +80036200 nw4r::ef::Resource::RelocateCommand +80036440 nw4r::ef::Resource::RemoveEffectProject +800364e0 nw4r::ef::Resource::RemoveTextureProject +80036580 __sinit_\ef_resource_cpp +800365d0 nw4r::ef::UtlistToArray +80036660 nw4r::ef::GetDirMtxY +80036770 nw4r::ef::MtxGetRotationMtx +80036890 nw4r::ef::MtxGetRotation +80036a00 nw4r::ef::MtxGetTranslate +80036a20 nw4r::ef::MtxGetScale +80036f00 nw4r::ef::Normalize +80036f70 nw4r::ef::_PSSinCosRad +80036ff0 nw4r::ef::PSSinCosRad +80037070 nw4r::ef::Rotation2VecY +800371c0 nw4r::ef::PSMTX34RotXYZRad +80037340 nw4r::ef::MTX34ScaleNonZero +800373c0 nw4r::ef::MTX34ScaleNonZero +80037450 nw4r::ef::MTX34ScaleInv +80037500 nw4r::ef::MTXColLen +80037560 nw4r::ef::HandleBase::__ct +80037570 nw4r::ef::HandleBase::Operator=(HandleBase) +80037590 nw4r::ef::HandleBase::Operator=(LinkedObject) +800375c0 nw4r::ef::HandleBase::IsValid +800375f0 nw4r::ef::HandleBase::GetPtr +80037630 nw4r::ef::EmitterForm::CalcVelocity +80037b00 nw4r::ef::EmitterForm::CalcLife +80037be0 nw4r::ef::CreationQueue::__ct +80037bf0 nw4r::ef::CreationQueue::AddParticleCreation +80037d70 nw4r::ef::CreationQueue::AddEmitterCreation +80037ef0 nw4r::ef::CreationQueue::Execute +80038030 nw4r::ef::EmitFormBuilder::Create +800380b0 __sinit_\ef_emform_cpp +80038110 nw4r::ef::EmitterFormPoint::Emission +80038460 nw4r::ef::EmitterFormLine::Emission +800387a0 nw4r::ef::EmitterFormDisc::Emission +80038cd0 nw4r::ef::EmitterFormSphere::Emission +80039530 nw4r::ef::EmitterFormCylinder::EmissionSub +80039990 nw4r::ef::EmitterFormCylinder::Emission +80039ce0 nw4r::ef::EmitterFormTorus::Emission +8003a320 nw4r::ef::EmitterFormCube::EmissionSub +8003a660 nw4r::ef::EmitterFormCube::Emission +8003bf30 nw4r::ef::DrawStrategyBuilder::Create +8003c0b0 nw4r::ef::DrawStrategyImpl::__ct +8003c3c0 nw4r::ef::DrawStrategyImpl::Initialize +8003c4a0 nw4r::ef::DrawStrategyImpl::_InitTev +8003c790 nw4r::ef::DrawStrategyImpl::_InitColor +8003cb60 nw4r::ef::DrawStrategyImpl::SetupGP +8003d000 nw4r::ef::DrawStrategyImpl::_SetupTevReg +8003d830 nw4r::ef::DrawStrategyImpl::_SetupTexture +8003dea0 nw4r::ef::DrawStrategyImpl::GetGetFirstDrawParticle +8003dec0 nw4r::ef::DrawStrategyImpl::GetGetNextDrawParticle +8003dee0 nw4r::ef::DrawStrategyImpl::GetFirstDrawParticle_EldersFirst +8003df10 nw4r::ef::DrawStrategyImpl::GetFirstDrawParticle_YoungersFirst +8003df60 nw4r::ef::DrawStrategyImpl::GetNextDrawParticle_EldersFirst +8003df90 nw4r::ef::DrawStrategyImpl::GetNextDrawParticle_YoungersFirst +8003dfc0 nw4r::ef::DrawStrategyImpl::AheadContext::__ct +8003e1e0 nw4r::ef::DrawStrategyImpl::__dt +8003e220 __sinit_\ef_drawstrategyimpl_cpp +8003e2b0 nw4r::ef::DrawBillboardStrategy::__ct +8003e2f0 nw4r::ef::DrawBillboardStrategy::Draw +8003e330 nw4r::ef::DrawBillboardStrategy::DrawNormalBillboard +8003edf0 nw4r::ef::DrawBillboardStrategy::DrawYBillboard +8003f720 nw4r::ef::DrawBillboardStrategy::DrawDirectionalB +8003ffb0 nw4r::ef::DrawBillboardStrategy::InitGraphics +80040080 nw4r::ef::DrawBillboardStrategy::GetCalcAheadFunc +80040100 nw4r::ef::DrawStrategyImpl::CalcAhead_Speed +800401e0 nw4r::ef::DrawStrategyImpl::CalcAhead_EmitterCent +800402a0 nw4r::ef::DrawStrategyImpl::CalcAhead_EmitterDesi +800402c0 nw4r::ef::DrawStrategyImpl::CalcAhead_Particle +800403e0 nw4r::ef::DrawStrategyImpl::CalcAhead_ParticleBot +80040650 nw4r::ef::DrawBillboardStrategy::__dt +80040690 nw4r::ef::DrawDirectionalStrategy::DrawDirectiona +800406d0 nw4r::ef::DrawQuad +80040810 nw4r::math::VEC3::__ct +80040820 nw4r::math::VEC3Transform +80040860 0x80040860 +80040870 nw4r::ef::DrawQuad +80040a10 nw4r::ef::DrawDirectionalStrategy::Draw +80040b20 nw4r::ef::EmitterResource::GetEmitterDrawSetting +80040b30 nw4r::ef::DrawDirectionalStrategy::DrawDirectiona +800418b0 nw4r::math::MTX34::__ct +800418c0 nw4r::ef::DrawInfo::GetViewMtxCooked +80041a90 nw4r::math::MTX34Mult +80041ad0 nw4r::math::VEC3::Operator= +80041af0 nw4r::math::VEC3::__ct +80041b00 nw4r::ef::DrawStrategyImpl::Normalize +80041b70 nw4r::math::VEC3Dot +80041b90 nw4r::ef::DrawStrategyImpl::GetYoungestParticle +80041ba0 nw4r::ef::DrawStrategyImpl::GetElderParticle +80041bb0 nw4r::ef::Particle::Draw_GetSizeX +80041c30 nw4r::ef::Particle::Draw_GetSizeY +80041d10 nw4r::ef::Particle::GetMoveDir +80041d60 nw4r::math::FSqrt +80041db0 nw4r::ef::CalcRotate +80042120 nw4r::math::MTX34::__ct +80042170 nw4r::ef::CalcLocalTransform +80042370 nw4r::math::VEC3Cross +800423b0 nw4r::ef::DrawDirectionalStrategy::DrawDirectiona +800429f0 nw4r::ef::DrawDirectionalStrategy::InitGraphics +80042ab0 nw4r::ef::DrawDirectionalStrategy::GetCalcAheadFu +80042b40 nw4r::ef::DrawStrategyImpl::CalcAhead_NoDesign +80042b70 nw4r::ef::DrawDirectionalStrategy::~DrawDirection +80042bb0 nw4r::ef::DrawFreeStrategy::__ct +80042bf0 nw4r::ef::DrawQuad +80042d30 nw4r::ef::DrawFreeStrategy::Draw +80043670 nw4r::ef::DrawFreeStrategy::__dt +800436b0 nw4r::ef::DrawLineStrategy::__ct +800436f0 nw4r::ef::DrawLineStrategy::Draw +80043d70 nw4r::ef::DrawLineStrategy::__dt +80043db0 nw4r::ef::DrawPointStrategy::__ct +80043df0 nw4r::ef::DrawPointStrategy::Draw +80044290 nw4r::ef::DrawPointStrategy::__dt +800442d0 nw4r::ef::DrawStripeStrategy::__ct +80044310 nw4r::ef::DrawStripeStrategy::DrawStripe +80044650 nw4r::ef::DrawStrategyImpl::GetNumDrawParticle +80044660 nw4r::ef::DrawStripeStrategy::GetStripeTexmapType +80044670 nw4r::math::FInv +80044690 nw4r::ef::DrawStripeStrategy::DrawParticle +80044b30 GXEnd +80044b40 nw4r::ef::DrawStripeStrategy::CalcVertexTube +80045040 nw4r::ef::DrawStripeStrategy::DrawTubeSegment +80045420 nw4r::ef::DrawStripeStrategy::DrawStripeTube_Conn +800456e0 nw4r::ef::DrawStripeStrategy::DrawStripeTube_Conn +80045a20 nw4r::ef::DrawStripeStrategy::DrawStripeTube_Conn +80045de0 nw4r::ef::DrawStripeStrategy::DrawStripeTube +80045fd0 nw4r::ef::DrawStripeStrategy::Draw +80046840 nw4r::ef::DrawStripeStrategy::GetInitialPrevAxis +80046990 nw4r::ef::DrawStripeStrategy::GetCalcAheadFunc +80046a40 0x80046a3c +80046a50 0x80046a48 +80046a60 nw4r::ef::DrawStripeStrategy::CalcAhead_Particle_ +80046b80 nw4r::ef::DrawStripeStrategy::CalcAhead_ParticleB +80046df0 nw4r::ef::DrawStripeStrategy::CalcAhead_ParticleB +800470b0 nw4r::ef::DrawStripeStrategy::CalcAhead_ParticleB +800472e0 nw4r::ef::DrawStripeStrategy::__dt +80047320 nw4r::ef::DrawSmoothStripeStrategy::DrawSmoothStr +80047360 nw4r::ef::DrawSmoothStripeStrategy::CalcVertex +80047750 nw4r::ef::DrawSmoothStripeStrategy::CalcVertexTub +80047c50 nw4r::ef::DrawSmoothStripeStrategy::DrawSection_B +80047f20 nw4r::math::VEC3::Operator+ +80047f50 nw4r::ef::DrawSmoothStripeStrategy::DrawSectionEn +80048170 nw4r::ef::DrawSmoothStripeStrategy::DrawSectionTu +80048c30 GXEnd +80048c40 nw4r::ef::DrawSmoothStripeStrategy::DrawSmoothStr +80048f90 nw4r::ef::DrawSmoothStripeStrategy::GetDrawOrder +80048fa0 nw4r::ef::DrawSmoothStripeStrategy::GetStripeTexm +80048fb0 nw4r::ef::DrawSmoothStripeStrategy::Vertex::Verte +80048fc0 nw4r::ef::DrawSmoothStripeStrategy::DrawSmoothStr +800493e0 nw4r::ef::DrawSmoothStripeStrategy::DrawSmoothStr +800495d0 0x800495c8 +80049880 nw4r::ef::DrawStrategyImpl::GetYoungestDrawPartic +800498d0 nw4r::math::VEC3::Operator- +80049900 nw4r::math::VEC3::Operator+= +80049930 nw4r::ef::DrawSmoothStripeStrategy::Vertex::Verte +80049970 nw4r::ef::DrawSmoothStripeStrategy::Vertex::Opera +800499b0 nw4r::ef::DrawSmoothStripeStrategy::DrawSmoothStr +80049cb0 nw4r::ef::DrawSmoothStripeStrategy::DrawSmoothStr +8004a060 nw4r::ef::DrawSmoothStripeStrategy::DrawSmoothStr +8004a5e0 nw4r::ef::DrawSmoothStripeStrategy::DrawSmoothStr +8004a7f0 nw4r::ef::DrawSmoothStripeStrategy::Draw +8004b1a0 nw4r::ef::DrawSmoothStripeStrategy::GetInitialPre +8004b2f0 nw4r::ef::DrawSmoothStripeStrategy::GetCalcAheadF +8004b3a0 0x8004b39c +8004b3b0 0x8004b3a8 +8004b3c0 nw4r::ef::DrawSmoothStripeStrategy::CalcAhead_Par +8004b4e0 nw4r::ef::DrawSmoothStripeStrategy::CalcAhead_Par +8004b750 nw4r::ef::DrawSmoothStripeStrategy::CalcAhead_Par +8004ba10 nw4r::ef::DrawSmoothStripeStrategy::CalcAhead_Par +8004bc40 nw4r::ef::DrawSmoothStripeStrategy::~DrawSmoothSt +8004bc80 nw4r::ef::ResEmitter::GetEmitTrack +8004bcb0 nw4r::ef::ResAnimCurve::SetStop +8004bce0 nw4r::g3d::ResName::Operator== +8004bd30 nw4r::g3d::detail::ResWriteBPCmd +8004bd60 nw4r::g3d::detail::ResWriteBPCmd +8004bdc0 nw4r::g3d::detail::ResWriteCPCmd +8004bdf0 nw4r::g3d::detail::ResWriteXFCmd +8004be30 nw4r::g3d::detail::ResWriteSSMask +8004be90 nw4r::g3d::ResDic::Get +8004bf70 nw4r::g3d::ResDic::Get +8004c050 ResDic_operator[] +8004c0d0 ResDic_operator[] +8004c140 nw4r::g3d::ResDic::GetIndex +8004c1d0 nw4r::g3d::ResDic::GetIndex +8004c240 nw4r::g3d::ResFile::GetResMdl(name) +8004c2b0 nw4r::g3d::ResFile::GetResMdl(idx) +8004c350 nw4r::g3d::ResFile::GetResMdl(u32 idx) +8004c3f0 nw4r::g3d::ResFile::GetResMdlNumEntries +8004c460 nw4r::g3d::ResFile::GetResPltt +8004c4e0 nw4r::g3d::ResFile::GetResTex +8004c550 nw4r::g3d::ResFile::GetResTex +8004c5d0 nw4r::g3d::ResFile::GetResTex(idx) +8004c670 nw4r::g3d::ResFile::GetResTex(u32 idx) +8004c710 nw4r::g3d::ResFile::GetResTexNumEntries +8004c780 nw4r::g3d::ResFile::GetResAnmChr(name) +8004c7f0 nw4r::g3d::ResFile::GetResAnmChr(ResName) +8004c890 nw4r::g3d::ResFile::GetResAnmChrNumEntries +8004c900 nw4r::g3d::ResFile::GetResAnmClr(name) +8004c970 nw4r::g3d::ResFile::GetResAnmClr(ResName) +8004ca10 nw4r::g3d::ResFile::GetResAnmClrNumEntries +8004ca80 nw4r::g3d::ResFile::GetResAnmTexPat(name) +8004caf0 nw4r::g3d::ResFile::GetResAnmTexPat(ResName) +8004cb90 nw4r::g3d::ResFile::GetResAnmTexPatNumEntries +8004cc00 nw4r::g3d::ResFile::GetResAnmTexSrt(name) +8004cc70 nw4r::g3d::ResFile::GetResAnmTexSrt(ResName) +8004cd10 nw4r::g3d::ResFile::GetResAnmTexSrtNumEntries +8004cd80 nw4r::g3d::ResFile::GetResAnmShp(name) +8004cdf0 nw4r::g3d::ResFile::GetResAnmShp(ResName) +8004ce90 nw4r::g3d::ResFile::GetResAnmShpNumEntries +8004cf00 nw4r::g3d::ResFile::GetResAnmScn +8004cf70 nw4r::g3d::ResFile::HasResMdl +8004cfc0 nw4r::g3d::ResFile::HasResAnmChr +8004d010 nw4r::g3d::ResFile::HasResAnmVis +8004d060 nw4r::g3d::ResFile::HasResAnmClr +8004d0b0 nw4r::g3d::ResFile::HasResAnmTexPat +8004d100 nw4r::g3d::ResFile::HasResAnmTexSrt +8004d150 nw4r::g3d::ResFile::HasResAnmShp +8004d1a0 nw4r::g3d::ResFile::HasResAnmScn +8004d1f0 nw4r::g3d::ResFile::Bind +8004d430 nw4r::g3d::ResFile::Release +8004d630 nw4r::g3d::ResFile::Init +8004d900 nw4r::g3d::ResFile::CheckRevision +8004e250 nw4r::g3d::ResMdl::GetResByteCode +8004e2a0 nw4r::g3d::ResMdl::GetResNode(name) +8004e2f0 nw4r::g3d::ResMdl::GetResNode(ResName) +8004e340 nw4r::g3d::ResMdl::GetResNode(idx) +8004e3a0 nw4r::g3d::ResMdl::GetResNode(u32 idx) +8004e400 nw4r::g3d::ResMdl::GetResNodeNumEntries +8004e440 nw4r::g3d::ResMdl::GetResVtxPos +8004e490 nw4r::g3d::ResMdl::GetResVtxPos +8004e4f0 nw4r::g3d::ResMdl::GetResVtxPos +8004e550 nw4r::g3d::ResMdl::GetResVtxPosNumEntries +8004e590 nw4r::g3d::ResMdl::GetResVtxNrm +8004e5e0 nw4r::g3d::ResMdl::GetResVtxNrm +8004e640 nw4r::g3d::ResMdl::GetResVtxNrm +8004e6a0 nw4r::g3d::ResMdl::GetResVtxNrmNumEntries +8004e6e0 nw4r::g3d::ResMdl::GetResVtxClr +8004e730 nw4r::g3d::ResMdl::GetResVtxClr +8004e790 nw4r::g3d::ResMdl::GetResVtxClr +8004e7f0 nw4r::g3d::ResMdl::GetResVtxClrNumEntries +8004e830 nw4r::g3d::ResMdl::GetResVtxTexCoord +8004e890 nw4r::g3d::ResMdl::GetResVtxFurPos +8004e8f0 nw4r::g3d::ResMdl::GetResMat(name) +8004e940 nw4r::g3d::ResMdl::GetResMat(resname) +8004e990 nw4r::g3d::ResMdl::GetResMat(idx) +8004e9f0 nw4r::g3d::ResMdl::GetResMat(u32 idx) +8004ea50 nw4r::g3d::ResMdl::GetResMatNumEntries +8004ea90 nw4r::g3d::ResMdl::GetResShp +8004eaf0 nw4r::g3d::ResMdl::GetResShp +8004eb50 nw4r::g3d::ResMdl::GetResShpNumEntries +8004eb90 nw4r::g3d::ResMdl::GetResTexPlttInfoOffsetFromTexName +8004ebf0 nw4r::g3d::ResMdl::GetResTexPlttInfoOffsetFromTexNameNumEntries +8004ec30 nw4r::g3d::ResMdl::Bind +8004ed40 nw4r::g3d::ResMdl::Release +8004ee30 nw4r::g3d::ResMdl::Init +8004f3d0 nw4r::g3d::ResShp::GetResVtxPos +8004f420 nw4r::g3d::ResShp::GetResVtxNrm +8004f480 nw4r::g3d::ResShp::GetResVtxClr +8004f4f0 nw4r::g3d::ResShp::GetResVtxFurPos +8004f550 nw4r::g3d::ResShp::Init +8004f710 0x8004f70c +8004f8e0 nw4r::g3d::ResShp::CallPrePrimitiveDisplayList +8004f9c0 0x8004f9c0 +8004fa30 nw4r::g3d::ResTev::GXSetTevSwapModeTable +8004fae0 nw4r::g3d::ResTev::GXSetTevSwapMode +8004fb40 nw4r::g3d::ResTev::GXSetTevAlphaIn +8004fbd0 nw4r::g3d::ResTev::GXSetTevAlphaOp +8004fc80 nw4r::g3d::ResTev::GXSetTevKColorSel +8004fd10 nw4r::g3d::ResTev::GXSetTevKAlphaSel +8004fda0 nw4r::g3d::ResTev::GXGetTevOrder +8004fe70 nw4r::g3d::ResTev::GXSetTevOrder +8004ff90 nw4r::g3d::ResTev::GXGetTevColorOp +80050070 nw4r::g3d::ResTev::GXSetTevColorIn +80050100 nw4r::g3d::ResTev::GXSetTevColorOp +800501b0 nw4r::g3d::ResTev::GXGetIndTexOrder +80050250 nw4r::g3d::ResTev::SetNumTevStages +80050340 nw4r::g3d::ResTev::CallDisplayList +800503a0 nw4r::g3d::ResTev::CopyTo +80050410 nw4r::g3d::ResTev::DCStore +80050430 nw4r::g3d::ResMat::GetParent +80050450 nw4r::g3d::ResMatPix::DCStore +80050470 nw4r::g3d::ResMatTevColor::DCStore +80050490 nw4r::g3d::ResMatIndMtxAndScale::DCStore +800504b0 nw4r::g3d::ResMatTexCoordGen::DCStore +800504d0 0x800504d0 +80050500 0x80050500 +80050530 nw4r::g3d::ResMatPix::CopyTo +80050570 nw4r::g3d::ResMatTevColor::CopyTo +800505b0 nw4r::g3d::ResMatIndMtxAndScale::CopyTo +800505f0 nw4r::g3d::ResMatTexCoordGen::CopyTo +80050630 nw4r::g3d::ResTexObj::GetTexObj +80050660 nw4r::g3d::ResTexObj::GetTexObj +80050690 nw4r::g3d::ResTexObj::IsValidTexObj +800506d0 SetTextureUsed +80050700 nw4r::g3d::ResTexObj::CopyTo +80050790 nw4r::g3d::ResTlutObj::GetTlut +800507c0 nw4r::g3d::ResTlutObj::GetTlut +800507f0 nw4r::g3d::ResTlutObj::IsValidTlut +80050830 nw4r::g3d::ResTlutObj::CopyTo +800508c0 nw4r::g3d::ResTexSrt::CopyTo +800509a0 0x800509a0 +80050a20 0x80050a20 +80050a70 0x80050a70 +80050af0 nw4r::g3d::ResGenMode::GXSetNumTexGens +80050b10 nw4r::g3d::ResGenMode::GXSetNumTexStages +80050b30 nw4r::g3d::ResGenMode::GXSetCullMode +80050b50 nw4r::g3d::ResGenMode::CopyTo +80050b90 nw4r::g3d::ResMatMisc::GXGetZCompLoc +80050bb0 nw4r::g3d::ResMatMisc::SetLightSetIdx +80050be0 nw4r::g3d::ResMatMisc::GetLightSetIdx +80050c00 nw4r::g3d::ResMatMisc::GetFogIdx +80050c20 nw4r::g3d::ResMatMisc::GetIndirectTexMtxCalcMetho +80050c70 nw4r::g3d::ResMatMisc::CopyTo +80050ce0 nw4r::g3d::ResMatFur::GetLyrRate +80050da0 nw4r::g3d::ResMatPix::GXSetZMode +80050dc0 nw4r::g3d::ResMatPix::GXSetDstAlpha +80050de0 nw4r::g3d::ResMatTevColor::GXGetTevColor +80050ea0 nw4r::g3d::ResMatTevColor::GXSetTevColor +80050f40 nw4r::g3d::ResMatTevColor::GXGetTevKColor +80051000 nw4r::g3d::ResMatTevColor::GXSetTevKColor +80051090 nw4r::g3d::ResMatIndMtxAndScale::CallDisplayList +800510d0 nw4r::g3d::ResMatIndMtxAndScale::GXGetIndTexMtx +80051390 nw4r::g3d::ResMatIndMtxAndScale::GXGetIndTexMtx +80051580 nw4r::g3d::ResMatIndMtxAndScale::GXSetIndTexMtx +800516f0 nw4r::g3d::ResMatChan::GXGetChanMatColor +80051730 nw4r::g3d::ResMatChan::GXGetChanCtrl +800517f0 nw4r::g3d::ResMatChan::CopyTo +80051850 nw4r::g3d::ResMatTexCoordGen::CallDisplayList +80051920 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2 +80051ad0 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2 +80051b34 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2_switch +80051b38 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2_caseD_0 +80051b44 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2_caseD_1 +80051b50 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2_caseD_2 +80051b5c nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2_caseD_3 +80051b68 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2_caseD_13 +80051b70 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2_caseD_14 +80051b78 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2_caseD_4 +80051b80 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2_caseD_5 +80051b88 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2_caseD_6 +80051b90 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2_caseD_7 +80051b98 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2_caseD_8 +80051ba0 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2_caseD_9 +80051ba8 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2_caseD_a +80051bb0 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2_caseD_b +80051bb8 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2_caseD_c +80051bc0 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2_caseD_d +80051bc8 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2_caseD_e +80051bd0 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2_caseD_f +80051bd8 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2_caseD_10 +80051be0 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2_caseD_11 +80051be8 nw4r::g3d::ResMatTexCoordGen::GXGetTexCoordGen2_caseD_12 +80051c40 0x80051c3c +80051cd0 nw4r::g3d::ResMat::Bind +80051da0 nw4r::g3d::ResMat::Release +80051e40 nw4r::g3d::ResMat::ForceBindTex +80052090 nw4r::g3d::ResMat::ReleaseTex +80052190 nw4r::g3d::ResMat::Init +800521c0 nw4r::g3d::ResMat::GetResTev +800521e0 nw4r::g3d::ResMat::GetResMatFur +80052200 nw4r::g3d::ResTexPlttInfo::Bind +800524c0 nw4r::g3d::ResVtxPos::SetArray +80052500 nw4r::g3d::ResVtxPos::GetArray +80052540 nw4r::g3d::ResVtxPos::CopyTo +800525a0 0x800525a0 +800525e0 0x800525e0 +80052620 nw4r::g3d::ResVtxNrm::CopyTo +80052680 0x80052680 +800526c0 nw4r::g3d::ResVtxClr::GetArray +80052700 nw4r::g3d::ResVtxClr::CopyTo +80052760 0x80052760 +800527a0 0x800527a0 +80052820 nw4r::g3d::ResVtxPos::DCStore +80052840 nw4r::g3d::ResVtxNrm::DCStore +80052860 nw4r::g3d::ResVtxClr::DCStore +80052880 nw4r::g3d::ResVtxTexCoord::DCStore +800528a0 nw4r::g3d::ResVtxFurVec::DCStore +800528c0 nw4r::g3d::ResVtxFurPos::DCStore +800528e0 0x800528e0 +80052900 0x80052900 +80052930 nw4r::g3d::ResTex::GetTexObjParam +800529f0 nw4r::g3d::ResTex::GetTexObjCIParam +80052ab0 0x80052ab0 +80052ae0 nw4r::g3d::ResTex::Init +80052af0 nw4r::g3d::ResNode::PatchChrAnmResult +80052c90 nw4r::g3d::ResNode::CalcChrAnmResult +80052e00 nw4r::g3d::ResNode::SetScale +80052eb0 nw4r::g3d::ResNode::SetTranslate +80052f40 nw4r::g3d::ResNode::SetRotate +80052fd0 nw4r::g3d::detail::GetResKeyFrameAnmResult +80053110 nw4r::g3d::detail::GetResColorAnmResult +80053220 nw4r::g3d::ResAnmClr::GetAnmResult +80053390 nw4r::g3d::@unnamed@g3d_resanmtexpat_cpp@::Search +80053450 nw4r::g3d::ResAnmTexPat::GetAnmResult +800535d0 nw4r::g3d::ResAnmTexPat::Bind +80053740 nw4r::g3d::ResAnmTexPat::Release +800538e0 0x800538e0 +80053d00 0x80053d00 +80053fb0 0x80053fb0 +800540f0 0x800540f0 +80054270 0x80054270 +80054460 0x80054460 +80054ed0 0x80054ed0 +80055130 nw4r::g3d::ResAnmChr::GetAnmResult_ +80055150 nw4r::g3d::ResAnmChr::GetAnmResult_T +800551e0 nw4r::g3d::ResAnmChr::GetAnmResult_R +80055250 nw4r::g3d::ResAnmChr::GetAnmResult_S +80055290 nw4r::g3d::ResAnmChr::GetAnmResult_RT +80055340 nw4r::g3d::ResAnmChr::GetAnmResult_SR +800553e0 nw4r::g3d::ResAnmChr::GetAnmResult_ST +80055480 nw4r::g3d::ResAnmChr::GetAnmResult_SRT +80055540 nw4r::g3d::ResAnmChr::GetAnmResult +800555c0 nw4r::g3d::ChrAnmResult::GetScale +80055600 nw4r::g3d::ChrAnmResult::GetRotTrans +800556b0 nw4r::g3d::ChrAnmResult::SetRotTrans +80055840 nw4r::g3d::ResLightSet::Bind +800559b0 nw4r::g3d::ResAnmAmbLight::GetAnmResult +80055a80 0x80055a80 +80055e70 0x80055e70 +80055fd0 nw4r::g3d::ResAnmCamera::GetAnmResult +800562e0 nw4r::g3d::ResAnmScn::HasResAnmAmbLight +80056340 nw4r::g3d::ResAnmScn::HasResAnmLight +800563a0 nw4r::g3d::ResAnmScn::GetResLightSetNumEntries +80056420 nw4r::g3d::ResAnmScn::GetResAnmAmbLight(ResName) +800564b0 nw4r::g3d::ResAnmScn::GetResAnmAmbLight(idx) +80056560 nw4r::g3d::ResAnmScn::GetResAnmAmbLightNumEntries +800565e0 nw4r::g3d::ResAnmScn::GetResAnmLight(ResName) +80056670 nw4r::g3d::ResAnmScn::GetResAnmLight(idx) +80056720 nw4r::g3d::ResAnmScn::GetResAnmLightNumEntries +800567a0 nw4r::g3d::ResAnmScn::GetResAnmFog +80056850 nw4r::g3d::ResAnmScn::GetResAnmFogNumEntries +800568d0 nw4r::g3d::ResAnmScn::GetResAnmCamera +80056980 nw4r::g3d::ResAnmScn::GetResAnmCameraNumEntries +80056a00 nw4r::g3d::ResAnmScn::GetResLightSetByRefNumber(u32) +80056a50 nw4r::g3d::ResAnmScn::GetResLightSetByRefNumber(int) +80056aa0 nw4r::g3d::ResAnmScn::GetResAnmAmbLightByRefNumber +80056af0 nw4r::g3d::ResAnmScn::GetResAnmLightByRefNumber +80056b40 nw4r::g3d::ResAnmScn::GetResAnmFogByRefNumber +80056b90 nw4r::g3d::ResAnmScn:GetResAnmCameraByRefNumber(u32) +80056be0 nw4r::g3d::ResAnmScn:GetResAnmCameraByRefNumber(int) +80056c30 nw4r::g3d::ResAnmScn::Bind +80056da0 0x80056da0 +80056f10 nw4r::g3d::ApplyVisAnmResult +80056fd0 nw4r::g3d::ApplyVisAnmResult +80057060 0x80057060 +80057080 0x80057080 +800570c0 0x800570c0 +800570d0 0x800570c8 +800570e0 0x800570d8 +800570f0 0x800570e4 +80057130 0x80057130 +800571a0 0x80057194 +80057290 0x8005728c +800573b0 0x800573b0 +80057410 0x80057410 +80057490 0x80057488 +80057520 0x80057518 +80057580 0x80057574 +80057610 0x80057608 +80057670 0x80057664 +80057770 0x80057764 +80057830 0x80057828 +800578b0 0x800578b0 +800579f0 0x800579e4 +80057aa0 0x80057aa0 +80057c90 0x80057c90 +80057d50 0x80057d50 +80057d60 0x80057d58 +80057e20 0x80057e14 +80057e30 0x80057e28 +80057f10 0x80057f10 +80058010 0x80058008 +80058090 0x80058084 +800580e0 nw4r::g3d::ApplyClrAnmResult +800585a0 0x80058594 +80058610 0x80058608 +80058660 0x80058660 +80058690 0x80058690 +800586a0 0x8005869c +800586f0 0x800586e4 +80058770 0x8005876c +800587e0 0x800587d8 +80058810 0x80058810 +80058820 0x8005881c +80058890 0x80058888 +800588c0 0x800588c0 +800588d0 0x800588cc +80058900 0x80058900 +80058910 0x80058910 +80058930 nw4r::g3d::AnmObjTexPat::Release +80058970 nw4r::g3d::AnmObjTexPat::Attach +80058980 nw4r::g3d::AnmObjTexPat::Detach +80058990 nw4r::g3d::AnmObjTexPat::DetachAll +800589a0 nw4r::g3d::AnmObjTexPat::__dt +800589e0 nw4r::g3d::AnmObjTexPatNode::__dt +80058a50 nw4r::g3d::AnmObjTexPatNode::Attach +80058b40 nw4r::g3d::AnmObjTexPatNode::Detach +80058c60 nw4r::g3d::AnmObjTexPatNode::DetachAll +80058cc0 nw4r::g3d::AnmObjTexPatNode::UpdateFrame +80058d40 nw4r::g3d::AnmObjTexPatNode::SetFrame +80058dd0 nw4r::g3d::AnmObjTexPatNode::GetFrame +80058e30 nw4r::g3d::AnmObjTexPatNode::SetUpdateRate +80058ec0 nw4r::g3d::AnmObjTexPatNode::GetUpdateRate +80058f20 nw4r::g3d::AnmObjTexPatNode::Bind +80059020 nw4r::g3d::AnmObjTexPatNode::Release +800590e0 nw4r::g3d::AnmObjTexPatNode::G3dProc +80059160 nw4r::g3d::AnmObjTexPatOverride::Construct +800592a0 nw4r::g3d::AnmObjTexPatOverride::GetResult +80059360 nw4r::g3d::AnmObjTexPatRes::Construct +80059550 nw4r::g3d::AnmObjTexPatRes::SetFrame +80059610 nw4r::g3d::AnmObjTexPatRes::GetFrame +80059620 nw4r::g3d::AnmObjTexPatRes::SetUpdateRate +800596e0 nw4r::g3d::AnmObjTexPatRes::GetUpdateRate +800596f0 nw4r::g3d::AnmObjTexPatRes::UpdateFrame +800597d0 nw4r::g3d::AnmObjTexPatRes::Bind +800598d0 nw4r::g3d::AnmObjTexPatRes::GetResult +80059950 nw4r::g3d::AnmObjTexPatRes::G3dProc +800599a0 nw4r::g3d::ApplyTexPatAnmResult +80059ba0 nw4r::g3d::AnmObjTexPatRes::IsDerivedFrom +80059c10 nw4r::g3d::AnmObjTexPat::IsDerivedFrom +80059c60 nw4r::g3d::AnmObjTexPatRes::GetTypeName +80059c90 nw4r::g3d::AnmObjTexPatRes::GetTypeObj +80059ca0 nw4r::g3d::AnmObjTexPatRes::__dt +80059cf0 nw4r::g3d::AnmObjTexPatOverride::IsDerivedFrom +80059d70 nw4r::g3d::AnmObjTexPatNode::IsDerivedFrom +80059de0 nw4r::g3d::AnmObjTexPatOverride::GetTypeName +80059e10 nw4r::g3d::AnmObjTexPatOverride::GetTypeObj +80059e20 nw4r::g3d::AnmObjTexPatOverride::__dt +80059e90 nw4r::g3d::AnmObjTexPatNode::GetTypeName +80059ec0 nw4r::g3d::AnmObjTexPatNode::GetTypeObj +80059ed0 nw4r::g3d::AnmObjTexPat::GetTypeName +80059f00 nw4r::g3d::AnmObjTexPat::GetTypeObj +80059f10 0x80059f10 +80059f30 0x80059f30 +80059f70 0x80059f70 +80059f80 0x80059f78 +80059f90 0x80059f88 +80059fa0 0x80059f94 +80059fe0 0x80059fe0 +8005a050 0x8005a044 +8005a140 0x8005a13c +8005a260 0x8005a260 +8005a2c0 0x8005a2c0 +8005a340 0x8005a338 +8005a3d0 0x8005a3c8 +8005a430 0x8005a424 +8005a4c0 0x8005a4b8 +8005a520 0x8005a514 +8005a620 0x8005a614 +8005a6e0 0x8005a6d8 +8005a760 0x8005a760 +8005a8a0 0x8005a894 +8005a950 0x8005a950 +8005ab40 nw4r::g3d::AnmObjTexSrtRes::SetFrame +8005ac00 0x8005ac00 +8005ac10 0x8005ac08 +8005acd0 0x8005acc4 +8005ace0 0x8005acd8 +8005adc0 0x8005adc0 +8005aec0 0x8005aeb8 +8005af40 0x8005af34 +8005af90 0x8005af90 +8005b030 0x8005b030 +8005b1e0 0x8005b1dc +8005b250 0x8005b248 +8005b2a0 0x8005b2a0 +8005b2d0 0x8005b2d0 +8005b2e0 0x8005b2dc +8005b330 0x8005b324 +8005b3b0 0x8005b3ac +8005b420 0x8005b418 +8005b450 0x8005b450 +8005b460 0x8005b45c +8005b4d0 0x8005b4c8 +8005b500 0x8005b500 +8005b510 0x8005b50c +8005b540 0x8005b540 +8005b550 nw4r::g3d::AnmObjChr::Release +8005b590 nw4r::g3d::AnmObjChr::Attach +8005b5a0 nw4r::g3d::AnmObjChr::Attach +8005b5b0 nw4r::g3d::AnmObjChr::SetWeight +8005b5c0 nw4r::g3d::AnmObjChr::GetWeight +8005b5d0 nw4r::g3d::AnmObjChr::DetachAll +8005b5e0 nw4r::g3d::AnmObjChr::__dt +8005b620 nw4r::g3d::AnmObjChrNode::__dt +8005b690 nw4r::g3d::AnmObjChrNode::Attach +8005b780 nw4r::g3d::AnmObjChrNode::Detach +8005b8a0 nw4r::g3d::AnmObjChrNode::DetachAll +8005b900 nw4r::g3d::AnmObjChrNode::UpdateFrame +8005b980 nw4r::g3d::AnmObjChrNode::SetFrame +8005ba10 nw4r::g3d::AnmObjChrNode::GetFrame +8005ba70 nw4r::g3d::AnmObjChrNode::SetUpdateRate +8005bb00 0x8005baf8 +8005bb60 nw4r::g3d::AnmObjChrNode::Bind +8005bc60 nw4r::g3d::AnmObjChrNode::Bind(BindOption) +8005bd70 nw4r::g3d::AnmObjChrNode::Release +8005be30 nw4r::g3d::AnmObjChrNode::Release(BindOption) +8005bf80 nw4r::g3d::AnmObjChrNode::G3dProc +8005c000 nw4r::g3d::AnmObjChrBlend::Construct +8005c180 nw4r::g3d::AnmObjChrBlend::GetResult +8005c7c0 nw4r::g3d::AnmObjChrBlend::SetWeight +8005c7d0 nw4r::g3d::AnmObjChrBlend::GetWeight +8005c7e0 nw4r::g3d::AnmObjChrRes::Construct +8005c9d0 nw4r::g3d::AnmObjChrRes::SetFrame +8005ca90 nw4r::g3d::AnmObjChrRes::GetFrame +8005caa0 nw4r::g3d::AnmObjChrRes::SetUpdateRate +8005cb60 nw4r::g3d::AnmObjChrRes::GetUpdateRate +8005cb70 nw4r::g3d::AnmObjChrRes::UpdateFrame +8005cc50 nw4r::g3d::AnmObjChrRes::Bind +8005cd60 nw4r::g3d::AnmObjChrRes::Bind(BindOption) +8005cf60 nw4r::g3d::AnmObjChrRes::Release(BindOption) +8005d130 nw4r::g3d::AnmObjChrRes::GetResult +8005d1b0 nw4r::g3d::AnmObjChrRes::G3dProc +8005d200 nw4r::g3d::AnmObjChrRes::IsDerivedFrom +8005d270 nw4r::g3d::AnmObjChr::IsDerivedFrom +8005d2c0 nw4r::g3d::AnmObjChrRes::GetTypeName +8005d2f0 nw4r::g3d::AnmObjChrRes::GetTypeObj +8005d300 nw4r::g3d::AnmObjChrRes::__dt +8005d350 nw4r::g3d::AnmObjChrBlend::IsDerivedFrom +8005d3d0 nw4r::g3d::AnmObjChrNode::IsDerivedFrom +8005d440 nw4r::g3d::AnmObjChrBlend::GetTypeName +8005d470 nw4r::g3d::AnmObjChrBlend::GetTypeObj +8005d480 nw4r::g3d::AnmObjChrBlend::__dt +8005d4f0 nw4r::g3d::AnmObjChrNode::GetTypeName +8005d520 nw4r::g3d::AnmObjChrNode::GetTypeObj +8005d530 nw4r::g3d::AnmObjChr::GetTypeName +8005d560 nw4r::g3d::AnmObjChr::GetTypeObj +8005d570 0x8005d570 +8005d590 0x8005d590 +8005d5d0 0x8005d5d0 +8005d5e0 0x8005d5d8 +8005d5f0 0x8005d5e8 +8005d600 0x8005d5f4 +8005d610 0x8005d608 +8005d620 0x8005d614 +8005d660 0x8005d660 +8005d6d0 0x8005d6c4 +8005d7c0 0x8005d7bc +8005d8e0 0x8005d8e0 +8005d940 0x8005d940 +8005d9c0 0x8005d9b8 +8005da50 0x8005da48 +8005dab0 0x8005daa4 +8005db40 0x8005db38 +8005dba0 0x8005db94 +8005dca0 0x8005dc94 +8005dd60 0x8005dd58 +8005dde0 0x8005dde0 +8005df60 0x8005df58 +8005e270 0x8005e270 +8005e280 0x8005e280 +8005e290 0x8005e290 +8005e4d0 0x8005e4d0 +8005e5a0 0x8005e594 +8005e5b0 0x8005e5a8 +8005e670 0x8005e668 +8005e680 0x8005e678 +8005e770 0x8005e764 +8005e930 0x8005e924 +8005e9b0 0x8005e9a8 +8005ea00 0x8005e9f4 +8005ea70 0x8005ea68 +8005eac0 0x8005eac0 +8005eaf0 0x8005eaf0 +8005eb00 0x8005eafc +8005eb50 0x8005eb44 +8005ebd0 0x8005ebcc +8005ec40 0x8005ec38 +8005ec70 0x8005ec70 +8005ec80 0x8005ec7c +8005ecf0 0x8005ece8 +8005ed20 0x8005ed20 +8005ed30 0x8005ed2c +8005ed60 0x8005ed60 +8005ed70 0x8005ed70 +8005ee40 0x8005ee38 +8005eff0 nw4r::g3d::AnmScn::GetLightSetting +8005f320 0x8005f318 +8005f330 0x8005f328 +8005f340 0x8005f338 +8005f380 0x8005f37c +8005f390 0x8005f390 +8005f3a0 0x8005f3a0 +8005f3b0 0x8005f3b0 +8005f3c0 0x8005f3bc +8005f3d0 0x8005f3d0 +8005f3e0 0x8005f3e0 +8005f3f0 0x8005f3ec +8005f400 0x8005f3fc +8005f410 0x8005f40c +8005f420 0x8005f41c +8005f430 nw4r::g3d::AnmScnRes::Construct +8005f5d0 nw4r::g3d::AnmScnRes::__ct +8005f6e0 0x8005f6d4 +8005f720 nw4r::g3d::AnmScnRes::SetFrame +8005f780 0x8005f778 +8005f790 0x8005f788 +8005f7c0 0x8005f7b4 +8005f7d0 0x8005f7c8 +8005f850 0x8005f848 +8005f8a0 0x8005f894 +8005f9e0 0x8005f9d8 +8005fa50 0x8005fa48 +8005fbe0 nw4r::g3d::AnmScnRes::GetFog +8005fc90 nw4r::g3d::AnmScnRes::GetCamera +8005fdf0 0x8005fde4 +8005fe90 0x8005fe84 +8005fec0 0x8005feb4 +8005ff20 0x8005ff1c +8005ffb0 0x8005ffac +80060040 0x8006003c +80060080 0x80060078 +800600d0 0x800600d0 +80060250 0x80060244 +800602a0 0x800602a0 +800602e0 0x800602d8 +80060310 0x80060310 +80060320 0x8006031c +80060350 0x80060350 +80060360 nw4r::g3d::G3dObj::__dt +800603b0 nw4r::g3d::G3dObj::Destroy +80060430 nw4r::g3d::G3dObj::DetachFromParent +80060490 nw4r::g3d::G3dObj::IsDerivedFrom +800604b0 nw4r::g3d::G3dObj::GetTypeName +800604e0 nw4r::g3d::G3dObj::GetTypeObj +800604f0 0x800604ec +80060500 nw4r::g3d::PlayPolicy_Loop +80060570 nw4r::g3d::AnmObj::SetAnmFlag +800605a0 nw4r::g3d::AnmObj::TestAnmFlag +800605c0 nw4r::g3d::fifo::GDSetGenMode2 +80060640 0x80060640 +80060670 Fifo_GDSetTexCoordScale2 +800606e0 nw4r::g3d::fifo::GDSetIndTexMtx +80060980 0x80060980 +800609e0 0x800609e0 +80060a60 0x80060a60 +80060ae0 nw4r::g3d::tmem::SetTMemLayout +80060be0 TexRegionCallback(_GXTexObj_* +80060cb0 0x80060ca4 +80060ce0 setTexRegion_(TMemCachePlan) +80060e40 nw4r::g3d::detail::Copy32ByteBlocks +80060f20 nw4r::g3d::detail::ZeroMemory32ByteBlocks +80060ff0 nw4r::g3d::detail::ZeroMemory16ByteBlocks +80061080 0x80061074 +80061460 0x80061458 +80061570 0x80061570 +800619b0 nw4r::g3d::detail::ScnDependentMtxFunc::DefaultMa +800619c0 nw4r::g3d::G3DState::IndTexMtxInfo::FifoSend +80061a30 0x80061a30 +80061a70 0x80061a70 +80061a80 0x80061a80 +80061b80 0x80061b80 +80061c90 0x80061c88 +80061fb0 nw4r::g3d::G3DState::CameraMtxState::CameraMtxSta +80062050 nw4r::g3d::G3DState::LoadResMatMisc +800620c0 nw4r::g3d::G3DState::LoadResTexObj +80062330 nw4r::g3d::G3DState::LoadResTlutObj +80062480 nw4r::g3d::G3DState::LoadResGenMode +80062650 nw4r::g3d::G3DState::LoadResTev +80062860 nw4r::g3d::G3DState::LoadResMatPix +80062910 nw4r::g3d::G3DState::LoadResMatTevColor +800629c0 nw4r::g3d::G3DState::LoadResMatIndMtxAndScale +80062a80 nw4r::g3d::G3DState::LoadResMatIndMtxAndScale +80062c50 nw4r::g3d::G3DState::LoadResMatChan +80063380 nw4r::g3d::G3DState::LoadResMatTexCoordGen +80063410 nw4r::g3d::G3DState::LoadResTexSrt +800636c0 0x800636c0 +80063870 0x80063870 +80063bc0 nw4r::g3d::G3DState::SetViewPosNrmMtxArray +80063c50 0x80063c50 +80063d20 nw4r::g3d::G3DState::SetScnDependentTexMtxFunc +80063d90 nw4r::g3d::G3DState::GetScnDependentTexMtxFunc +80063df0 0x80063de4 +80063e30 0x80063e30 +80063e40 0x80063e40 +80063ee0 nw4r::g3d::G3DState::LoadFog +80063fa0 0x80063fa0 +80063fb0 0x80063fb0 +80063fe0 nw4r::g3d::G3DState::LoadLightSet +80064040 nw4r::g3d::G3DState::LoadLightSet +800640d0 nw4r::g3d::G3DState::SetCameraProjMtx +80064180 nw4r::g3d::G3DState::GetCameraMtxPtr +800641a0 0x800641a0 +80064210 nw4r::g3d::G3DState::GetCameraMtxPtr +80064240 nw4r::g3d::G3DState::SetRenderModeObj +80064440 nw4r::g3d::G3DState::GetRenderModeObj +80064450 nw4r::g3d::G3DState::Invalidate +800645a0 nw4r::g3d::G3DState::IndMtxOpStd::SetNrmMapMtx +80064700 nw4r::g3d::G3DState::IndMtxOpStd::Reset +80064750 nw4r::g3d::G3DState::IndMtxOpStd::__cl +800648c0 __sinit_g3d_state_cpp +80064b30 nw4r::g3d::detail::LoadMaterial +80064fd0 0x80064fd0 +80065880 0x80065880 +80065930 0x80065930 +80065a00 0x800659f8 +80065c40 0x80065c34 +80065f20 0x80065f1c +80066200 0x800661f4 +80066580 0x8006657c +80066800 0x800667f8 +80066aa0 0x80066aa0 +80066dd0 0x80066dd0 +80067270 0x80067270 +800677d0 0x800677d0 +80067860 0x80067860 +800678c0 0x800678c0 +800678d0 nw4r::g3d::detail::workmem::GetMtxIDTemporary +800678e0 0x800678e0 +800678f0 0x800678f0 +80067900 0x80067900 +80067910 0x80067910 +80067920 nw4r::g3d::WorldMtxManip::SetScale +800679a0 nw4r::g3d::CalcWorld +80067f70 0x80067f70 +80068250 0x80068250 +80068260 0x80068260 +80068770 0x80068770 +80068940 nw4r::g3d::DrawResMdlLoop(byteCode) +80068a80 nw4r::g3d::DrawResMdlLoop(byteCode, rep) +80068c50 nw4r::g3d::DrawResMdlLoop(MdlZ) +80068d60 nw4r::g3d::DrawResMdlLoop(MdlZ, rep) +80068ec0 nw4r::g3d::SetUpMdlZ +80069000 nw4r::g3d::DrawResMdlDirectly +80069230 0x80069228 +80069270 0x8006926c +800692e0 0x800692e0 +800698a0 0x800698a0 +80069e60 0x80069e60 +8006a0c0 nw4r::g3d::Camera::__ct +8006a0d0 nw4r::g3d::Camera::Init +8006a120 nw4r::g3d::Camera::Init +8006a240 nw4r::g3d::Camera::SetPosition +8006a270 nw4r::g3d::Camera::SetPosition +8006a2b0 nw4r::g3d::Camera::SetPosture +8006a4d0 nw4r::g3d::Camera::SetCameraMtxDirectly +8006a520 nw4r::g3d::Camera::SetPerspective +8006a560 nw4r::g3d::Camera::SetOrtho +8006a5a0 nw4r::g3d::Camera::SetProjectionMtxDirectly +8006a5f0 nw4r::g3d::Camera::SetScissor +8006a610 nw4r::g3d::Camera::SetScissorBoxOffset +8006a630 nw4r::g3d::Camera::SetViewport +8006a6d0 nw4r::g3d::Camera::SetViewportZRange +8006a6f0 nw4r::g3d::Camera::GetViewport +8006a760 nw4r::g3d::Camera::GetCameraMtx +8006a7c0 nw4r::g3d::Camera::GetProjectionMtx +8006a8a0 nw4r::g3d::Camera::GetProjectionTexMtx +8006a970 nw4r::g3d::Camera::GetEnvironmentTexMtx +8006a9f0 nw4r::g3d::Camera::GXSetViewport +8006aa80 nw4r::g3d::Camera::GXSetProjection +8006ab50 nw4r::g3d::Camera::GXSetScissor +8006ab80 nw4r::g3d::Camera::GXSetScissorBoxOffset +8006aba0 0x8006aba0 +8006afc0 0x8006afc0 +8006b120 nw4r::g3d::detail::dcc::CalcWorldMtx_Basic +8006b290 0x8006b288 +8006b2d0 0x8006b2c8 +8006b370 0x8006b364 +8006b3b0 0x8006b3a8 +8006b470 0x8006b46c +8006b520 0x8006b520 +8006b570 0x8006b570 +8006b650 0x8006b64c +8006b6d0 0x8006b6c8 +8006b7a0 0x8006b7a0 +8006b800 0x8006b800 +8006b830 0x8006b828 +8006b9d0 0x8006b9c4 +8006bb20 0x8006bb20 +8006bbb0 0x8006bba8 +8006bd60 0x8006bd60 +8006be00 nw4r::g3d::detail::dcc::CalcWorldMtx_Maya_SSC_App +8006bfa0 0x8006bf94 +8006bfe0 0x8006bfd8 +8006c070 0x8006c068 +8006c0b0 0x8006c0a8 +8006c150 0x8006c150 +8006c220 0x8006c218 +8006c270 0x8006c270 +8006c340 0x8006c338 +8006c3c0 0x8006c3b8 +8006c4d0 0x8006c4d0 +8006c500 0x8006c4f4 +8006c640 0x8006c63c +8006c770 0x8006c76c +8006c800 0x8006c7f8 +8006c960 0x8006c960 +8006ca00 nw4r::g3d::detail::dcc::CalcWorldMtx_Xsi +8006cbc0 0x8006cbb4 +8006cc10 0x8006cc08 +8006ccc0 0x8006ccbc +8006cd00 0x8006ccf8 +8006cdc0 0x8006cdb4 +8006ce90 0x8006ce8c +8006cef0 0x8006cee8 +8006cfc0 0x8006cfbc +8006d040 0x8006d040 +8006d170 0x8006d164 +8006d1a0 0x8006d194 +8006d2f0 0x8006d2e4 +8006d430 0x8006d430 +8006d480 0x8006d474 +8006d4d0 0x8006d4c4 +8006d640 0x8006d640 +8006d6e0 nw4r::g3d::ScnObj::CalcWorldMtx +8006d790 nw4r::g3d::ScnObj::CalcViewMtx +8006d7b0 nw4r::g3d::ScnObj::__ct +8006d8a0 nw4r::g3d::ScnObj::__dt +8006d920 nw4r::g3d::ScnObj::SetScnObjOption +8006d940 nw4r::g3d::ScnObj::SetScnObjOption_caseD_1 +8006d968 nw4r::g3d::ScnObj::SetScnObjOption_caseD_2 +8006d990 nw4r::g3d::ScnObj::SetScnObjOption_caseD_3 +8006d9b8 nw4r::g3d::ScnObj::SetScnObjOption_caseD_4 +8006d9e0 nw4r::g3d::ScnObj::SetScnObjOption_caseD_5 +8006da08 nw4r::g3d::ScnObj::SetScnObjOption_caseD_6 +8006da30 nw4r::g3d::ScnObj::SetScnObjOption_caseD_7 +8006da58 nw4r::g3d::ScnObj::SetScnObjOption_caseD_8 +8006da80 nw4r::g3d::ScnObj::SetScnObjOption_caseD_9 +8006daa8 nw4r::g3d::ScnObj::SetScnObjOption_caseD_0 +8006dac0 nw4r::g3d::ScnObj::GetScnObjOption +8006daf0 nw4r::g3d::ScnObj::GetScnObjOption_caseD_1 +8006db00 nw4r::g3d::ScnObj::GetScnObjOption_caseD_2 +8006db10 nw4r::g3d::ScnObj::GetScnObjOption_caseD_3 +8006db20 nw4r::g3d::ScnObj::GetScnObjOption_caseD_4 +8006db30 nw4r::g3d::ScnObj::GetScnObjOption_caseD_5 +8006db40 nw4r::g3d::ScnObj::GetScnObjOption_caseD_6 +8006db50 nw4r::g3d::ScnObj::GetScnObjOption_caseD_7 +8006db60 nw4r::g3d::ScnObj::GetScnObjOption_caseD_8 +8006db70 nw4r::g3d::ScnObj::GetScnObjOption_caseD_9 +8006db80 nw4r::g3d::ScnObj::GetScnObjOption_caseD_0 +8006db90 nw4r::g3d::ScnObj::SetMtx +8006dc20 nw4r::g3d::ScnObj::SetMtx +8006dc80 nw4r::g3d::ScnObj::GetMtx +8006dcd0 nw4r::g3d::ScnObj::GetValueForSortOpa +8006dce0 nw4r::g3d::ScnObj::GetValueForSortXlu +8006dcf0 nw4r::g3d::ScnObj::SetPriorityDrawOpa +8006dd20 nw4r::g3d::ScnObj::SetPriorityDrawXlu +8006dd50 nw4r::g3d::ScnObj::EnableScnObjCallbackTiming +8006dd90 nw4r::g3d::ScnObj::EnableScnObjCallbackExecOp +8006ddb0 nw4r::g3d::ScnObj::SetBoundingVolume +8006de40 nw4r::g3d::ScnObj::GetBoundingVolume +8006dea0 nw4r::g3d::ScnLeaf::ForEach +8006dee0 nw4r::g3d::ScnLeaf::SetScnObjOption +8006df20 nw4r::g3d::ScnLeaf::GetScnObjOption +8006df70 nw4r::g3d::ScnObj::CalcWorldMtx +8006e070 nw4r::g3d::ScnLeaf::GetScaleProperty +8006e0b0 nw4r::g3d::ScnLeaf::DefG3dProcScnLeaf +8006e390 nw4r::g3d::ScnGroup::Construct +8006e450 nw4r::g3d::ScnGroup::ForEach +8006e590 nw4r::g3d::ScnGroup::ScnGroup_G3DPROC_GATHER_SCNOBJ +8006e6b0 nw4r::g3d::ScnGroup::G3dProc +8006e6f0 nw4r::g3d::ScnGroup::DefG3dProcScnGroup +8006eb60 nw4r::g3d::ScnGroup::Insert +8006ecf0 nw4r::g3d::ScnGroup::Remove(idx) +8006edb0 nw4r::g3d::ScnGroup::Remove(ScnObj) +8006ee40 nw4r::g3d::ScnGroup::__ct +8006eeb0 nw4r::g3d::ScnGroup::__dt +8006ef70 nw4r::g3d::ScnGroup::IsDerivedFrom +8006efc0 nw4r::g3d::G3dObj::IsDerivedFrom +8006f000 nw4r::g3d::ScnGroup::GetTypeNameStatic +8006f030 nw4r::g3d::ScnGroup::GetTypeObjStatic +8006f040 nw4r::g3d::ScnLeaf::IsDerivedFrom +8006f090 nw4r::g3d::ScnLeaf::GetTypeNameStatic +8006f0c0 nw4r::g3d::ScnLeaf::GetTypeObjStatic +8006f0d0 nw4r::g3d::ScnLeaf::destruct +8006f160 nw4r::g3d::ScnObj::GetTypeNameStatic +8006f190 nw4r::g3d::ScnObj::GetTypeObjStatic +8006f1a0 nw4r::g3d::ScnRoot::Construct +8006f2e0 nw4r::g3d::ScnRoot::G3dProc +8006f390 nw4r::g3d::ScnRoot::GetCamera +8006f3f0 nw4r::g3d::ScnRoot::GetCurrentCamera +8006f430 nw4r::g3d::ScnRoot::SetCurrentCamera +8006f440 nw4r::g3d::ScnRoot::GetFog +8006f4a0 nw4r::g3d::ScnRoot::GetLightSet +8006f4f0 nw4r::g3d::ScnRoot::SetAnmScn +8006f590 nw4r::g3d::ScnRoot::UpdateFrame +8006f600 0x8006f600 +8006f830 nw4r::g3d::ScnRoot::CalcAnmScn +8006f9c0 nw4r::g3d::ScnRoot::CalcWorld +8006fa10 nw4r::g3d::ScnRoot::CalcMaterial +8006fa30 nw4r::g3d::ScnRoot::CalcVtx +8006fa50 nw4r::g3d::ScnRoot::CalcView +8006fad0 nw4r::g3d::ScnRoot::GatherDrawScnObj +8006fbc0 nw4r::g3d::ScnRoot::ZSort +8006fc20 nw4r::g3d::ScnRoot::DrawOpa +8006fca0 nw4r::g3d::ScnRoot::DrawXlu +8006fd20 nw4r::g3d::ScnRoot::ForceDrawMode +8006fd50 nw4r::g3d::ScnRoot::__ct +8006fe50 0x8006fe4c +8006fed0 nw4r::g3d::ScnObjGather::Add +8006fff0 nw4r::g3d::ScnObjGather::ZSort +80070050 0x80070050 +80070090 0x80070090 +800700d0 std::sort(void_(nw4r::g3d::ScnObj_** +80070450 std::sort(void_(nw4r::g3d::ScnObj_** +800707d0 std::__sort132(void_(nw4r::g3d::ScnObj_** +800708d0 nw4r::g3d::ScnObjGather::Sort +80070930 nw4r::g3d::@unnamed@g3d_scnroot_cpp@::LessByGetVa +800709c0 nw4r::g3d::@unnamed@g3d_scnroot_cpp@::LessByGetVa +80070a50 nw4r::g3d::ScnObjGather::Sort(CompFuncs) +80070ac0 nw4r::g3d::ScnObjGather::DrawOpa +80070b50 nw4r::g3d::ScnObjGather::DraxXlu +80070be0 nw4r::g3d::ScnObjGather::CheckScnObj +80070bf0 nw4r::g3d::ScnObjGather::__ct +80070c20 nw4r::g3d::ScnObjGather::Clear +80070c30 nw4r::g3d::ScnObjGather::__dt +80070c70 0x80070c70 +80070ce0 0x80070cd8 +80070d10 0x80070d10 +80070d20 nw4r::g3d::ScnMdlSimple::Construct +80070ec0 nw4r::g3d::ScnMdlSimple::ScnMdlSmpl_CalcPosture +80071120 nw4r::g3d::ScnMdlSimple::ScnMdlSmpl_G3DPROC_GATHER_SCNOBJ +80071160 nw4r::g3d::ScnMdlSimple::ScnMdlSmpl_G3DPROC_CALC_ +800714b0 nw4r::g3d::ScnMdlSimple::G3dProc +80071d40 nw4r::g3d::ScnMdlSimple::SetScnObjOption +80071d80 nw4r::g3d::ScnMdlSimple::GetScnObjOption +80071dc0 nw4r::g3d::ScnMdlSimple::GetScnMtxPos +80071f00 nw4r::g3d::ScnMdlSimple::SetAnmObj +80072510 nw4r::g3d::ScnMdlSimple::RemoveAnmObj +80072690 nw4r::g3d::ScnMdlSimple::RemoveAnmObj +80072770 nw4r::g3d::ScnMdlSimple::GetAnmObj +800727d0 nw4r::g3d::ScnMdlSimple::GetAnmObj +80072830 nw4r::g3d::ScnMdlSimple::UpdateFrame +800728f0 nw4r::g3d::ScnMdlSimple::EnableScnMdlCallbackTiming +80072930 nw4r::g3d::ScnMdlSimple::DisableScnMdlCallbackTiming +80072970 nw4r::g3d::ScnMdlSimple::GetViewPosMtxArray +800729a0 nw4r::g3d::ScnMdlSimple::GetViewNrmMtxArray +800729e0 nw4r::g3d::ScnMdlSimple::GetViewTexMtxArray +80072a20 nw4r::g3d::ScnMdlSimple::GetByteCode +80072a70 0x80072a70 +80072c90 nw4r::g3d::ScnMdlSimple::destruct +80072db0 nw4r::g3d::ScnMdlSimple::IsDerivedFrom +80072e20 nw4r::g3d::ScnMdlSimple::GetTypeNameStatic +80072e50 nw4r::g3d::ScnMdlSimple::GetTypeObj +80072e60 nw4r::g3d::ScnMdl::CopiedMatAccess::GetResTexObj +80072eb0 nw4r::g3d::ScnMdl::CopiedMatAccess::GetResMatTevColor +80072f00 nw4r::g3d::ScnMdl::CopiedMatAccess::GetResTexSrtEx +80072f60 nw4r::g3d::ScnMdl::CopiedMatAccess::GetResMatMiscEx +80072fc0 nw4r::g3d::ScnMdl::CopiedMatAccess::GetResMatTevColorEx +80073040 nw4r::g3d::ScnMdl::CopiedMatAccess::GetResTevEx +800730b0 nw4r::g3d::ScnMdl::CopiedMatAccess::__ct +80073330 nw4r::g3d::ScnMdl::CopiedVisAccess::IsVisible +800733b0 nw4r::g3d::ScnMdl::CopiedVisAccess::CopiedVisAcce +80073470 nw4r::g3d::ScnMdl::Construct +80074000 nw4r::g3d::ScnMdl::ScnMdl_G3DPROC_CALC_MAT +80074770 nw4r::g3d::ScnMdl::G3dProc +80074c20 nw4r::g3d::ScnMdl::SetScnObjOption +80074c60 nw4r::g3d::ScnMdl::GetScnObjOption +80074ca0 nw4r::g3d::ScnMdl::InitBuffer +80075040 nw4r::g3d::ScnMdl::CleanMatBuffer +80075380 nw4r::g3d::ScnMdl::SetAnmObj +800754f0 nw4r::g3d::ScnMdl::RemoveAnmObj +800756f0 nw4r::g3d::ScnMdl::RemoveAnmObj +80075740 nw4r::g3d::ScnMdl::GetAnmObj +80075760 nw4r::g3d::ScnMdl::GetAnmObj +80075780 nw4r::g3d::ScnMdl::__dt +800757f0 nw4r::g3d::ScnMdl::IsDerivedFrom +80075870 nw4r::g3d::ScnMdl::GetTypeNameStatic +800758a0 nw4r::g3d::ScnMdl::GetTypeObjStatic +800758b0 0x800758ac +800758c0 0x800758b8 +800759f0 nw4r::g3d::ScnObjGatherExScnMdl::DrawOpa +80075a20 nw4r::g3d::ScnObjGatherExScnMdl::DrawXlu +80075a50 nw4r::g3d::ScnObjGatherExScnMdl::CheckScnObj +80075bd0 nw4r::g3d::ScnObjGatherExScnMdl::GetResMdl +80075be0 nw4r::g3d::ScnObjGatherExScnMdl::DrawLoop +80075fd0 nw4r::g3d::ScnObjGatherExScnMdl::HasEqualTarget +80076060 nw4r::g3d::ScnObjGatherExScnMdl::TypeID +80076070 nw4r::g3d::ScnGroupEx::Construct +800761d0 nw4r::g3d::ScnGroupEx::SetResMdl +80076210 0x8007620c +80076250 nw4r::g3d::ScnGroupEx::G3dProc +80076580 nw4r::g3d::ScnGroupEx::__dt +800765c0 nw4r::g3d::ScnObjGatherExScnMdl::SetResMdl +80076670 nw4r::g3d::ScnObjGatherExScnMdl::__dt +800766b0 0x800766b0 +800766c0 0x800766b4 +800766d0 0x800766c4 +800766e0 0x800766d4 +800766f0 0x800766e4 +80076700 0x800766f4 +80076710 nw4r::g3d::ScnGroupEx::IsDerivedFrom +80076780 nw4r::g3d::ScnGroupEx::GetTypeName +800767b0 nw4r::g3d::ScnGroupEx::GetTypeObj +800767c0 nw4r::g3d::ScnMdl1Mat1Shp::Construct +800768e0 nw4r::g3d::ScnMdl1Mat1Shp::G3dProc +80076bd0 nw4r::g3d::ScnMdl1Mat1Shp::IsDerivedFrom +80076c40 nw4r::g3d::ScnMdl1Mat1Shp::GetTypeName +80076c70 nw4r::g3d::ScnMdl1Mat1Shp::GetTypeObj +80076c80 nw4r::g3d::ScnMdl1Mat1Shp::__dt +80076cc0 nw4r::g3d::CalcMaterialDirectly +800770d0 nw4r::g3d::G3dInit +800771c0 nw4r::g3d::G3dReset +800771d0 nw4r::g3d::ScnProc::Construct +800772e0 nw4r::g3d::ScnProc::G3dProc +800773e0 nw4r::g3d::ScnProc::IsDerivedFrom +80077450 nw4r::g3d::ScnProc::GetTypeName +80077480 nw4r::g3d::ScnProc::GetTypeObj +80077490 nw4r::g3d::ScnProc::__dt +800774d0 nw4r::g3d::Fog::__ct +800774e0 nw4r::g3d::Fog::Init +80077550 nw4r::g3d::Fog::CopyTo +800775b0 nw4r::g3d::Fog::SetFogRangeAdjParam +800775d0 nw4r::g3d::Fog::SetGP +80077660 nw4r::g3d::LightObj::Clear +80077680 nw4r::g3d::LightObj::InitLightColor +800776d0 nw4r::g3d::LightObj::InitLightPos +80077710 nw4r::g3d::LightObj::InitLightDir +80077750 nw4r::g3d::LightObj::InitLightSpot +80077790 nw4r::g3d::LightObj::InitLightAttnA +800777d0 nw4r::g3d::LightObj::InitLightDistAttn +80077810 nw4r::g3d::LightObj::InitLightAttnK +80077850 nw4r::g3d::LightObj::InitSpecularDir +80077890 nw4r::g3d::LightObj::InitLightShininess +800778f0 nw4r::g3d::LightObj::GetLightPos +80077910 nw4r::g3d::LightObj::GetLightDir +80077930 nw4r::g3d::LightSetting::__ct +80077d70 nw4r::g3d::LightSetting::Import +80077f10 nw4r::g3d::LightSetting::ApplyViewMtx +80078030 nw4r::g3d::LightSet::SelectLightObj +800780a0 nw4r::g3d::LightSet::SelectAmbLightObj +80078110 nw4r::g3d::CalcVtx +80078920 nw4r::lyt::LytInit +80078980 nw4r::lyt::Pane::__ct +80078ac0 nw4r::lyt::Pane::__dt +80078c00 nw4r::lyt::Pane::AppendChild +80078c50 nw4r::lyt::Pane::GetPaneRect +80078ce0 nw4r::lyt::Pane::GetVtxColor +80078cf0 nw4r::lyt::Pane::SetVtxColor +80078d00 nw4r::lyt::Pane::GetColorElement +80078d30 nw4r::lyt::Pane::SetColorElement +80078d60 nw4r::lyt::Pane::GetVtxColorElement +80078d70 nw4r::lyt::Pane::SetVtxColorElement +80078d80 nw4r::lyt::Pane::FindPaneByName +80078e30 nw4r::lyt::Pane::FindMaterialByName +80078ef0 nw4r::lyt::Pane::CalculateMtx +800791f0 nw4r::lyt::Pane::Draw +80079280 nw4r::lyt::Pane::DrawSelf +80079290 nw4r::lyt::Pane::Animate +80079320 nw4r::lyt::Pane::AnimateSelf +800793e0 nw4r::lyt::Pane::BindAnimation +80079400 nw4r::lyt::Pane::UnbindAnimation +80079490 nw4r::lyt::Pane::UnbindAllAnimation +800794b0 nw4r::lyt::UnbindAnimationSelf +80079510 nw4r::lyt::Pane::AddAnimationLink +80079550 nw4r::lyt::Pane::FindAnimationLinkSelf +80079560 nw4r::lyt::Pane::FindAnimationLinkSelf +80079570 nw4r::lyt::Pane::SetAnimationEnable +80079670 nw4r::lyt::Pane::SetAnimationEnable +80079770 nw4r::lyt::Pane::LoadMtx +800797d0 nw4r::lyt::Pane::GetVtxPos +800798a0 nw4r::lyt::Pane::GetMaterialNum +800798c0 nw4r::lyt::Pane::GetMaterial +800798d0 nw4r::lyt::Pane::GetMaterial +800798f0 nw4r::lyt::Pane::GetRuntimeTypeInfo +80079900 0x80079900 +80079910 nw4r::lyt::Group::__ct +80079980 nw4r::lyt::Group::__ct +80079aa0 nw4r::lyt::Group::__dt +80079b50 nw4r::lyt::Group::AppendPane +80079be0 nw4r::lyt::GroupContainer::__dt +80079cb0 nw4r::lyt::GroupContainer::AppendGroup +80079cf0 nw4r::lyt::GroupContainer::FindGroupByName +80079d70 nw4r::lyt::SetTagProcessorImpl +80079f30 nw4r::lyt::IsIncludeAnimationGroupRef +8007a000 nw4r::lyt::Layout::__ct +8007a040 nw4r::lyt::Layout::__dt +8007a170 nw4r::lyt::Layout::Build +8007a4c0 nw4r::lyt::Layout::CreateAnimTransform +8007a540 nw4r::lyt::Layout::CreateAnimTransform +8007a5a0 nw4r::lyt::Layout::CreateAnimTransform +8007a630 nw4r::lyt::Layout::BindAnimation +8007a660 nw4r::lyt::Layout::UnbindAnimation +8007a690 nw4r::lyt::Layout::UnbindAllAnimation +8007a6b0 nw4r::lyt::Layout::BindAnimationAuto +8007a940 nw4r::lyt::Layout::SetAnimationEnable +8007a970 nw4r::lyt::Layout::CalculateMtx +8007a990 nw4r::lyt::Layout::Draw +8007a9b0 nw4r::lyt::Layout::Animate +8007a9d0 nw4r::lyt::Layout::SetTagProcessor +8007ab90 nw4r::lyt::Layout::BuildPaneObj +8007ad80 nw4r::ut::Color::__ct +8007ad90 nw4r::lyt::Picture::__ct +8007aee0 nw4r::lyt::Picture::__dt +8007afc0 nw4r::lyt::Picture::Append +8007b180 nw4r::lyt::Picture::GetVtxColor +8007b1b0 nw4r::lyt::Picture::SetVtxColor +8007b1e0 nw4r::lyt::Picture::GetVtxColorElement +8007b200 nw4r::lyt::Picture::SetVtxColorElement +8007b220 nw4r::lyt::Picture::DrawSelf +8007b300 nw4r::lyt::Picture::GetRuntimeTypeInfo +8007b310 __sinit_\lyt_picture_cpp +8007b320 nw4r::lyt::TextBox::__ct +8007b610 nw4r::lyt::TextBox::__dt +8007b7b0 nw4r::lyt::TextBox::GetVtxColor +8007b7e0 nw4r::lyt::TextBox::SetVtxColor +8007b830 nw4r::lyt::TextBox::GetVtxColorElement +8007b850 nw4r::lyt::TextBox::SetVtxColorElement +8007b870 nw4r::lyt::TextBox::DrawSelf +8007bf50 0x8007bf50 +8007c330 0x8007c330 +8007c370 nw4r::lyt::TextBox::GetStringBufferLength +8007c3a0 nw4r::lyt::TextBox::AllocStringBuffer +8007c470 nw4r::lyt::TextBox::FreeStringBuffer +8007c4c0 nw4r::lyt::TextBox::SetString +8007c5a0 nw4r::lyt::TextBox::SetString +8007c670 nw4r::ut::Font__dt +8007c6b0 nw4r::lyt::TextBox::LoadMtx +8007c740 0x8007c740 +8007cab0 0x8007cab0 +8007cc10 nw4r::lyt::TextBox::GetRuntimeTypeInfo +8007cc20 0x8007cc18 +8007cc30 nw4r::lyt::Window::__ct +8007cec0 nw4r::lyt::Window::__dt +8007d010 nw4r::lyt::Window::FindMaterialByName +8007d110 nw4r::lyt::Window::GetVtxColor +8007d140 nw4r::lyt::Window::SetVtxColor +8007d170 nw4r::lyt::Window::GetVtxColorElement +8007d190 nw4r::lyt::Window::SetVtxColorElement +8007d1b0 nw4r::lyt::Window::DrawSelf +8007d2f0 nw4r::lyt::Window::AnimateSelf +8007d380 nw4r::lyt::Window::UnbindAnimationSelf +8007d410 nw4r::lyt::Window::DrawContent +8007d520 nw4r::lyt::Window::DrawFrame +8007db00 nw4r::lyt::Window::DrawFrame4 +8007e250 nw4r::lyt::Window::DrawFrame8 +8007f0a0 nw4r::lyt::Window::GetFrameSize +8007f1a0 nw4r::lyt::Window:GetMaterialNum +8007f1b0 nw4r::lyt::Window::GetMaterial +8007f1e0 nw4r::lyt::Window::GetFrameMaterial +8007f210 nw4r::lyt::Window::GetContent +8007f220 0x8007f220 +8007f230 0x8007f228 +8007f240 Bounding_construct +8007f280 0x8007f27c +8007f2e0 0x8007f2d8 +8007f2f0 nw4r::lyt::Window::GetRuntimeTypeInfo +8007f300 __sinit_\lyt_bounding_cpp +8007f310 0x8007f310 +8007f420 0x8007f420 +8007f620 nw4r::math::VEC2::__as +8007f640 nw4r::ut::LinkList::__ct +8007f660 nw4r::lyt::Material::__ct +8007fcd0 _GXColorS10::Operator= +8007fd00 nw4r::ut::Color::Operator= +8007fd10 nw4r::lyt::MaterialResourceNum::GetTexMapNum +8007fd20 nw4r::lyt::MaterialResourceNum::GetTexSRTNum +8007fd30 nw4r::lyt::MaterialResourceNum::GetCoordGenNum +8007fd40 nw4r::lyt::MaterialResourceNum::GetChanCtrlNum +8007fd50 nw4r::lyt::MaterialResourceNum::GetMapColNum +8007fd60 nw4r::lyt::MaterialResourceNum::HasTevSwapTable +8007fd70 nw4r::lyt::MaterialResourceNum::HasAlphaCompare +8007fd80 nw4r::lyt::MaterialResourceNum::HasBlendMode +8007fd90 nw4r::lyt::MaterialResourceNum::GetIndTexStageNum +8007fda0 nw4r::lyt::MaterialResourceNum::GetIndTexStageNum +8007fdb0 0x8007fdb0 +8007fdc0 0x8007fdc0 +8007fde0 nw4r::lyt::res::TexMap::GetWarpModeS +8007fdf0 nw4r::lyt::res::TexMap::GetWarpModeT +8007fe00 0x8007fe00 +8007fe20 nw4r::lyt::res::TexMap::GetMinFilter +8007fe40 nw4r::lyt::res::TexMap::GetMagFilter +8007fe60 0x8007fe60 +8007fe90 0x8007fe90 +8007fec0 0x8007fec0 +8007fef0 nw4r::lyt::TevSwapMode::__as +8007ff00 nw4r::lyt::TexSRT::operator_= +8007ff30 0x8007ff30 +8007ff60 0x8007ff60 +80080000 0x80080000 +80080010 0x80080010 +80080040 0x80080040 +80080070 0x80080070 +80080080 0x80080080 +80080090 nw4r::snd::DvdSoundArchive::OpenExtStream +800800a0 0x800800a0 +800800b0 0x800800b0 +800800c0 0x800800c0 +800800d0 0x800800d0 +800800e0 nw4r::lyt::detail::ConvertOffsToPtr +800800f0 nw4r::lyt::detail::ConvertOffsToPtr +80080100 0x80080100 +80080110 0x80080110 +80080120 0x80080120 +80080130 nw4r::lyt::detail::ConvertOffsToPtr::CalcCompletion +80510f18 0x80510f18 +80510f58 0x80510f58 +80511034 0x80511034 +80511110 KMP::Manager::CalcNextCPRecursive +80511500 KMP::Manager::CalcNextCP +80511e00 KMP::Manager::ResetCKPTScannedState +80511e7c KMP::Manager::CalcExactFinishTime +80511ec8 KMP::Holder::CalcExactFinishTime +8051201c Vec2::Operator- +80512040 Vec2::Operator+ +80512064 KMP::CKPTSection::CalcDistanceFromCPRecursive +80512370 KMP::CKPTSection::CalcTotalDistance +80512694 KMP::Manager::CreateInstance +8051271c KMP::Manager::DestroyInstance +8051276c KMP::Manager::__ct +805127ac KMP::Manager::__dt +805127ec KMP::Manager::Init +80512c10 KMP::Manager::GetRawKMP +80512c2c KMP::RawHolder::__ct +80512c6c KMP::Holder::GetType +80512c78 KMP::Manager::GetCAMECount +80512c94 KMP::RawHolder::GetVersion +80512c9c KMP::Holder::GetOpeningCamIdx +80512cac KMP::Holder::SetId +80512cb4 KMP::Manager::GetAREACount +80512cd0 KMP::Manager::GetEENPTCount +80512cec KMP::Manager::GetITPTCount +80512d08 KMP::Manager::GetJGPTCount +80512d24 KMP::Manager::GetKTPTCount +80512d40 KMP::Holder::IsNarrowGrid +80512d4c KMP::Holder::GetFilterEffectColor +80512d58 KMP::Holder::HasFilterEffect +80512d64 KMP::Manager::ParseSTGI +80512e84 KMP::Manager::ParseMSPT +80512fa4 KMP::Manager::ParseCNPT +805130c4 KMP::Manager::ParseJGPT +805131e4 KMP::Manager::ParseCAME +80513304 KMP::Manager::ParseAREA +80513398 KMP::Manager::ParsePOTI +805134c8 KMP::Manager::ParseGOBJ +80513600 Vec3::__dt +80513640 KMP::Manager::ParseCKPT +8051377c KMP::Manager::ParseCKPH +80513adc KMP::Manager::ParseITPT +80513bfc KMP::Manager::ParseITPH +80513d18 KMP::Manager::ParseENPT +80513e40 KMP::Manager::ParseENPH +80513f5c KMP::Manager::ParseKTPT +805140dc KMP::AREASection::GetHolder +80514100 KMP::JGPTSection::GetHolder +80514124 KMP::KTPTSection::GetHolder +80514148 KMP::Manager::GetHolder +80514194 KMP::Holder::GetRoute +80514208 KMP::KTPTSection::GetBlock +80514258 KMP::KTPTSection::__ct +80514368 KMP::Holder::CalcCoordinates_0Indexed +80514794 0x80514794 +805147d4 Vec3::Operator-(unary) +805147fc Vec3::RotateQuaternion +80514810 Vec3::MultiplyByScalar +80514838 Vec3::Operator+ +8051486c Vec3::Operator- +805148a0 KMP::Holder::InitLinks +80514b24 KMP::Holder::CalcCoordinates +80514b30 KMP::Manager::GetHolder +80514b7c KMP::Manager::GetHolder +80514bc8 KMP::Manager::GetHolder +80514c30 KMP::Manager::CalcExtremes +80514d3c KMP::Manager::GetHolder +80514d88 KMP::Manager::GetHolder +80514df0 KMP::CKPHSection::Init +80515014 KMP::CKPHSection::GetHolder +80515098 KMP::Holder::Init +805150e0 KMP::Holder::CalcDepthRecursive +80515244 KMP::CKPTSection::Init +805154e4 KMP::Holder__ct +805155e0 KMP::LinkedCKPT::__ct +805155e4 KMP::LinkedCKPT::__dt +80515624 KMP::Holder::InitLinks +80515a6c KMP::Holder::InitType +80515c24 KMP::Manager::GetHolder +80515c70 KMP::Manager::GetHolder +80515cbc KMP::Manager::GetCKPHHolderByCT +80515d3c KMP::Manager::POTISection::__ct +80515e04 KMP::Manager::GetHolder +80515e50 KMP::AREASection::__ct +80515f8c KMP::AREASection::SortByPriority +80516050 KMP::Holder::__ct +805160b0 KMP::Holder::IsPointInAREA +80516138 KMP::Holder::GetRouteId +80516168 KMP::Holder::GetRoute +80516220 KMP::HolderAREABox::__ct +805163b4 KMP::Holder::__dt +805163f4 KMP::HolderAREABox::IsPointInAREAShape +805164fc KMP::HolderAREACylinder::__ct +80516688 KMP::HolderAREACylinder::IsPointInAREAShape +80516768 KMP::Manager::GetHolder +805167b4 KMP::Manager::GetAREAHolderByPriority +80516808 KMP::Manager::FindAREA +80516a60 KMP::Manager::GetMiniMapAREAParams +80516bb0 KMP::Manager::GetHolder +80516bfc KMP::Manager::GetCAMEHolderFromAREAId +80516cd4 KMP::Holder::GetNext +80516d4c KMP::IsNonPrivateBattle +80516d74 KMP::Holder::InitLinks +80517590 KMP::Manager::GetNextENPT +8051760c KMP::Manager::GetNextENPTCount +80517670 KMP::Manager::GetPrevENPT +805176ec KMP::Manager::GetPrevENPTCount +80517750 KMP::Manager::ToggleENPH +80517858 KMP::Holder::ToggleENPHLink +80517ce4 KMP::Holder::AddPrevLink +80517d38 KMP::Holder::AddNextLink +80517d8c KMP::Holder::AddLink +80517e88 KMP::Holder::InitLinks +805181f0 KMP::Manager::GetNextITPT +80518268 KMP::Manager::GetNextITPTCount +805182cc KMP::Manager::GetPrevITPT +80518344 KMP::Manager::GetPrevITPTCount +805183a8 KMP::Holder::__ct +805184fc KMP::Holder::InitLinks +80518920 KMP::Manager::GetHolder +8051896c KMP::Holder::GetPosition +80518ab8 Vec3::MultiplyByScalar2 +80518ae0 KMP::Manager::GetHolder +80518b2c KMP::Manager::GetHolder +80518b78 KMP::Manager::GetHolder +80518bb0 KMP::Holder::GetFilterEffectAlpha +80518bdc KMP::AREASection::__dt +80518c40 KMP::Holder::__dt +80518cc0 ArchiveFile::__ct +80518cf4 ArchiveFile::__dt +80518dcc ArchiveFile::Mount +80518e0c 0x80518e0c +80518e10 ArchiveFile::Load +80518fa4 ArchiveFile::LoadUncompressed +80518fbc ArchiveFile::MountFromRawArchive(void*) +80519040 ArchiveFile::GetFileNewBuffer +805190e8 0x805190e8 +805190f0 ArchiveFile::Dump +805191a4 ArchiveFile::TryDump +80519240 ArchiveFile::ClearAll +805192c8 0x805192c8 +805192cc ArchiveFile::Unmount +80519370 ArchiveFile::ClearArchive +805193c8 ArchiveFile::ClearDump +80519420 ArchiveFile::GetFile +80519508 ArchiveFile::Decompress +805195a4 ArchiveFile::SetDumpToDecompress +805195d8 ArchiveFile::MountFromRawArchive(ArchiveFile*) +80519670 Friend::__dt +805196b0 Friend::SetFriend +805196f0 Friend::StoreToDest +80519704 Friend::AddFriend +80519780 0x80519780 +8051986c 0x8051986c +80519980 Friend::SetData +805199d0 Friend::AddLoss +805199e8 Friend::AddWin +80519a00 0x80519a00 +80519a4c 0x80519a4c +80519b1c 0x80519b1c +80519d34 0x80519d34 +80519dd8 0x80519dd8 +80519fb8 0x80519fb8 +8051a01c 0x8051a01c +8051a04c 0x8051a04c +8051a07c KCLController::CanReuseLastTrianglesList +8051a0f8 0x8051a0f8 +8051a164 0x8051a164 +8051a174 0x8051a174 +8051a1a4 0x8051a1a4 +8051a1ac 0x8051a1ac +8051a1b4 0x8051a1b4 +8051a1e0 GameScene::__ct +8051a368 BaseScene::__dt +8051a3c0 GameScene::__dt +8051a41c GameScene::FadeIn +8051a480 GameScene::FadeOut +8051a4dc GameScene::FinalizeEnter +8051aa4c GameScene::GetScreenCount +8051aa54 GameScene::vf_0x58 +8051aa58 GameScene::AddArchivesHolder +8051aae8 GameScene::UnmountArchives +8051ab58 GameScene::enter +8051af84 GameScene::SetFramerate +8051afe8 GameScene::ChangeScene +8051b08c 0x8051b08c +8051b094 GameScene::FadeOutToReinit +8051b0e0 GameScene::CreateChildScene +8051b0f4 GameScene::Reset +8051b1f8 0x8051b1f8 +8051b250 GameScene::exit +8051b374 BaseScene::ResetRacedataScenarios +8051b380 0x8051b380 +8051b39c 0x8051b39c +8051b3c4 GameScene::vf_0x48 +8051b3c8 GameScene::calc +8051b790 GameScene::isSpectating +8051b798 GameScene::vf_0x2c +8051b7a0 GameScene::vf_0x50 +8051b7a8 0x8051b7a8 +8051b7b0 GameScene::reinit +8051b8d0 GameScene::OnReinit +8051b8d4 GameScene::isPaused +8051b8e4 GameScene::CreateHeap +8051b988 GameScene::DestroyHeap +8051b9d0 GameScene::vf_0x5c +8051b9d4 0x8051b9d4 +8051bc78 GameScene::draw +8051bd34 0x8051bd34 +8051bed0 GameScene::GetCurrent +8051bf00 0x8051bf00 +8051c00c BaseScene::outgoing_child_create +8051c010 BaseScene::incoming_child_destroy +8051c014 0x8051c014 +8051c018 0x8051c018 +8051c01c BaseScene::reinit +8051c020 BaseScene::exit +8051c024 BaseScene::enter +8051c028 BaseScene::draw +8051c02c BaseScene::calc +8051c030 0x8051c030 +8051c088 RKG::ClearBuffer +8051c094 RKG::CheckHeader +8051c120 RKGFile::CheckValidity +8051c23c RKGFile::SetCRC32 +8051c270 GhostData::__ct +8051c334 Timer::__dt +8051c374 Timer::__ct +8051c398 GhostData::__ct(RKG) +8051c4a0 GhostData::Reset +8051c52c 0x8051c52c +8051c530 GhostData::ReadRKG +8051c790 GhostData::Init +8051c7f4 GhostData::FillHeader +8051ca0c GhostData::CreateRKGWithoutInputs +8051ca80 GhostData::CreateRKG +8051cb1c GhostData::Fill +8051ce7c Input::Controller::GetType +8051ce84 0x8051ce84 +8051ce8c Input::ControllerHolder::GetInputsSize +8051ce94 GhostGroup::__ct +8051cf90 GhostData::__dt +8051cff8 GhostGroup::__dt +8051d064 GhostGroup::DisableGhostData +8051d084 GhostGroup::GetGhostData +8051d0bc GhostGroup::ReadRKG +8051d0e0 RKG::CompressTo +8051d1b4 RKG::DecompressTo +8051d388 RKG::GetLength +8051d4d4 HomeMenuMgr::CreateInstance +8051d534 HomeMenuMgr::DestroyInstance +8051d584 HomeMenuMgr::__dt +8051d5e8 HomeMenuMgr::LoadHBMArc +8051d69c 0x8051d69c +8051d6a0 HomeMenuMgr::__ct +8051d894 HomeMenuMgr::ResetControllerData +8051d918 HomeMenuMgr::Open +8051d9f4 HomeMenuMgr::Close +8051dac4 0x8051dac4 +8051dd70 HomeMenuMgr::UpdateControllerData +8051df78 HomeMenuMgr::UpdateLockParams +8051dfc0 0x8051dfc0 +8051e114 HomeMenuMgr::Calc +8051e360 0x8051e360 +8051e420 0x8051e420 +8051e574 0x8051e574 +8051e7e8 HomeMenuMgr::Draw +8051e85c Input::State::Reset +8051e89c Input::State::operator== +8051e904 Input::UIState::Reset +8051e960 Input::State::SetStickXMirrorCheck +8051e9e8 Input::State::SetStickX +8051ea28 Input::State::ConvertQuantised +8051ea60 Input::State::SetStickY +8051eaa0 Input::UIState::SetStickXMirrorCheck +8051eb28 Input::UIState::SetStickY +8051eb68 Input::State::SetControlMotionFlick +8051eba8 Input::Controller::__ct +8051ec94 Input::State::__dt +8051ecd4 Input::UIState::__dt +8051ed14 Input::Controller::Update +8051f00c Input::Controller::ResetSelf +8051f010 Input::Controller::UpdateBatteryField +8051f074 Input::Controller::GetRemainingBattery +8051f07c Input::Controller::GetRuntimeTypeInfo +8051f088 Input::WiiController::__ct +8051f1ec Input::Controller::__dt +8051f22c Input::WiiController::Init +8051f308 Input::Controller::Init +8051f37c Input::Controller::SetDriftType +8051f384 Input::WiiController::WPADInfoCallback +8051f3b4 Input::WiiController::UpdateBattery +8051f3d0 Input::WiiController::GetRemainingBattery +8051f410 Input::WiiController::UpdateStatesClassic +8051fa7c Input::WiiController::UpdateStatesNunchuck +8051fbe0 Input::WiiController::UpdateStatesCore +8051fc84 Input::WiiController::UpdateImpl +8051ffb8 Input::WiiController::TogglePointer +8051ffd0 Input::GCNController::__ct +80520118 Input::GCNController::Init +805201b0 Input::GCNController::UpdateImpl +805206fc Input::GCNController::HasRumble +80520730 Input::GhostController::__ct +80520924 Input::GhostController::__dt +80520998 Input::GhostController::Init +80520a4c Input::GhostController::HasRKGInputs +80520a60 0x80520a60 +80520b18 0x80520b18 +80520b9c Input::GhostController::UpdateImpl +80520d4c Input::GhostDirectionStream::Read +80520eb4 Input::GhostDirectionStream::GetInputSize +80520ebc Input::ControllerHolder::CreateRumbleController +80520f2c Input::RealControllerHolder::SetDriftType +80520f64 Input::ControllerHolder::__ct +80521110 Input::ControllerHolder::Init +80521198 Input::ControllerHolder::Update +80521554 Input::RealControllerHolder::SetController +805215d4 Input::RealControllerHolder::StartGhostReading +80521688 Input::RealControllerHolder::EndGhostWriting +80521768 Input::RealControllerHolder::Update +80521844 Input::RealControllerHolder::SetGhostController +80521930 Input::RealControllerHolder::PrepareForRace +805219ac Input::AIControllerHolder::CopyCPUInputs +80521a34 Input::ControllerHolder::RequestRumble(3args) +80521acc Input::ControllerHolder::RequestRumble(4args) +80521d84 Input::Controller::HasRumble +80521d8c 0x80521d8c +80521e48 0x80521e48 +80521ed0 Input::ControllerHolder::RequestRumbleByDuration +80521f8c Input::ControllerHolder::RequestRumbleByDurationNoFaderCheck +80522014 Input::ControllerHolder::StopAllRumble +80522078 Input::ControllerHolder::ActivateRumble +8052208c Input::Controller::ActivateRumble +80522090 Input::ControllerHolder::StopRumble +805220a4 Input::ControllerHolder::StopCurrentRumble +805220a8 Input::ControllerHolder::ResetSelfController +805220bc Input::RealControllerHolder::__ct +805222b4 Input::ControllerHolder::__dt +805222f4 Input::RealControllerHolder::__dt +80522358 Input::Manager::SetFSStickClamp +80522364 Input::ControllerParams::Fill +80522494 Input::Controller::GetChannel +8052249c 0x8052249c +8052250c 0x8052250c +805225a4 0x805225a4 +80522688 0x80522688 +80522690 Input::Manager::ResetKPAD +805226f4 Input::ControllerHolder::CopyCPUInputs +805226f8 Input::RealControllerHolder::GetInputsSize +80522700 Input::RealControllerHolder::GetRKGInputs +80522708 Input::RealControllerHolder::Init +8052279c Input::AIControllerHolder::__dt +805227dc Input::AIController::__dt +8052281c Input::Controller::IsPointerEnabled +80522824 Input::Controller::TogglePointer +80522828 Input::GhostController::SetDriftType +8052282c Input::GhostController::GetType +80522834 Input::Controller::Func_0x24 +80522840 Input::GCNController:ResetSelf +8052284c Input::GCNController:StopRumble +80522858 Input::GCNController:ActivateRumble +80522864 Input::GCNController:GetChannel +8052286c Input::GCNController:GetType +80522874 Input::GCNController:__dt +805228b4 Input::WiiController::ResetSelf +805228c4 Input::WiiController::StopRumble +805228d0 Input::WiiController::ActivateRumble +805228dc Input::WiiController::HasRumble +805228f4 Input::WiiController::IsPointerEnabled +80522924 Input::WiiController::GetChannel +8052292c Input::WiiController::GetType +80522934 Input::WiiController::__dt +80522974 Input::GhostDirectionStream::Write +80522ae8 Input::GhostTrickStream::Write +80522c5c Input::GhostTrickStream::Read +80522dc4 Input::GhostTrickStream::GetInputSize +80522dcc Input::GhostActionStream::Write +80522f40 Input::GhostActionStream::Read +805230a8 Input::GhostActionStream::GetInputSize +805230b0 Input_cpp_sinit_ +805230e0 Input::Manager::SyncSimpleCallback +8052313c Input::Manager::CreateInstance +8052318c Input::Manager::DestroyInstance +805231dc Input::Manager::__dt +805232b0 Input::DummyController::__dt +805232f0 Input::Manager::__ct +805234a0 Input::AIControllerHolder::__ct +80523520 Input::Manager::Init +80523524 0x80523524 +805235ac Input::Manager::ResetIdleFrameCounter +805235d4 0x805235d4 +80523690 Input::Manager::Prepare +80523724 Input::Manager::ResetSelfWiiAndGCNControllers +805237ac 0x805237ac +805237e8 Input::Manager::UpdateControllers +805238f0 Input::Manager::Update +80523990 0x80523990 +80523a58 Input::Manager::ConvertType +80523a80 0x80523a80 +80523bc8 0x80523bc8 +80523d10 0x80523d10 +80523eac Input::Manager::ResetDummyController +80523ebc Input::Manager::TrySetController +805240ec 0x805240ec +80524264 Input::Manager::ResetRealController +8052427c 0x8052427c +805242d8 Input::Manager::InitControllerHolders +80524438 0x80524438 +80524500 0x80524500 +8052453c Input::Manager::InitGhostController +80524558 Input::Manager::PrepareRealControllerHolderForRace +80524568 Input::Manager::EnableGhostReading +80524580 Input::Manager::StartGhostReading +805245cc Input::Manager::EndGhostWriting +805245dc Input::Manager::EndAllGhostWriting +80524628 Input::Manager::CopyPADStatus +805246c0 Input::Manager::InitRumble +80524718 Input::Manager::ResetRumble +80524734 0x80524734 +80524788 Input::Manager::SyncWiimotes +805247e4 0x805247e4 +80524840 Input::Manager::EnableAllPointers +805248d8 Input::Manager::ToggleAllPointers +80524944 0x80524944 +80524998 0x80524998 +805249b0 Input::Manager::RequestRumbleByDuration +805249c8 0x805249c8 +80524a50 0x80524a50 +80524a58 Input::AIController::UpdateImpl +80524aa8 Input::AIController::GetType +80524ab0 Input::DummyController::UpdateImpl +80524ab4 SITransferCommands +80524ac4 Input::GhostWriter::__ct +80524c2c Input::GhostWriter::__dt +80524ca8 Input::GhostWriter::Init +80524d20 Input::GhostWriter::CopyToRKGInputsBuffer +80524dd0 Input::GhostWriter::IsInputsSumSmallEnough +80524e74 Input::GhostWriter::WriteFrame +80524f78 Input::GhostActionStream::WriteNewValue +80524f88 Input::GhostActionStream::IncrementDuration +80524f98 Input::GhostActionStream::IsNewInput +80524fbc Input::GhostActionStream::ReadValue +80524fc4 Input::GhostActionStream::IsCurReadInputFinished +80524fe0 Input::GhostDirectionStream::WriteNewValue +80524ff0 Input::GhostDirectionStream::IncrementDuration +80525000 Input::GhostDirectionStream::IsNewInput +80525024 Input::GhostDirectionStream::ReadValue +8052502c Input::GhostDirectionStream::IsCurReadInputFinished +80525048 Input::GhostTrickStream::WriteNewValue +8052505c Input::GhostTrickStream::IncrementDuration +80525074 Input::GhostTrickStream::IsNewInput +8052509c Input::GhostTrickStream::ReadValue +805250a8 Input::GhostTrickStream::IsCurReadInputFinished +805250c8 Input::KRMMgr::Init +80525204 Input::KRMMgr::__ct +80525278 Input::Rumble::Init +805252a4 Input::Rumble::Request(KRMEntry) +805252e8 Input::Rumble::Request(float) +80525324 0x80525324 +80525334 Input::Rumble::Update +80525498 Input::RumbleController::__ct +80525508 Input::RumbleController::Init +805255bc Input::RumbleController::Stop +8052564c Input::RumbleController::DeactivateRumble +8052570c Input::RumbleController::Update +80525974 Input::RumbleController::Request(KRMEntry) +80525a74 Input::RumbleController::Request(float) +80525b8c Input::RumbleController::GetFreeRumble +80525c38 Input::RumbleController::__dt +80525c78 Input::Rumble::__dt +80525cb8 0x80525cb8 +80525d88 0x80525d88 +80525db8 0x80525db8 +80525e00 0x80525e00 +80525e60 0x80525e60 +80525f88 Mii::Init +80525ff4 0x80525ff4 +80526020 Mii::Load +805263bc 0x805263bc +80526400 0x80526400 +80526460 Mii::GetRFLArgs +8052663c Mii::CreateScnRfl +805267b4 0x805267b4 +805268f4 0x805268f4 +805269fc 0x805269fc +80526a10 0x80526a10 +80526a80 0x80526a80 +80526b64 MiiManager::Disposer__dt +80526bfc MiiManager::CreateInstance +80526c4c 0x80526c4c +80526c80 MiiManager::__ct +80526d3c MiiManagerSub::__ct +80526d54 MiiManagerSub::__dt +80526d94 MiiManager::Init +80527158 MiiManager::__dt +80527298 MiiManager::CountOfficialMiis +80527498 0x80527498 +80527514 MiiManager::GetMiddleDB3 +8052751c 0x8052751c +8052758c Mii::ComputeRFLStoreData +805275ec Mii::IsMiiId +80527604 Mii::GetMiiId +805276e0 MiiManager::GetStatus +80527700 MiiManager::IsOfficialOrDefaultMii +80527778 MiiManager::CreatePlayerMiiFromOfficial +80527b0c MiiManager::CreateStoreMii(RFLCreateID) +80527d60 MiiManager::CreateOfficialMii(RFLIdx) +80527e84 MiiManager::CreateOfficialMii(RFLCreateID) +80527f84 MiiManager::CreateDefaultMii(RFLIdx) +80528058 0x80528058 +80528068 MiiManager::CreateDefaultMii(RFLCreateID) +8052816c MiiManager::CreateOfficialMii(IdxArray) +80528290 MiiManager::AllowMiiDisplay +805282b0 MiiManager::CreateMiddleDBMii +80528598 MiiManager::CreateRandomMii +805287bc MiiManager::CreateMii(CreationParams) +80528ea8 0x80528ea8 +80529034 Mii::__dt +80529074 MiiManager::CreateMiddleDB2Mii +80529330 MiiManager::CreateMii(RFLStoreData) +80529420 MiiManager::CreateControllerMii +80529530 0x80529530 +80529590 0x80529590 +80529640 MiiManager::CreatePlayerMiiFromMiddleDB +80529730 MiiManager::PrepareRFLWiFiPacket +80529748 MiiManager::RFLWiFiInfo2MiddleDB +80529798 MiiManager::RFLWiFiInfo2StoreData +805297e4 MiiCreationParams::InitAndReset +80529804 MiiCreationParams::Init +80529824 MiiCreationParams::Reset +8052985c 0x8052985c +80529870 0x80529870 +80529898 0x80529898 +805298fc 0x805298fc +8052991c 0x8052991c +8052993c MiiCreationParams2::Init +80529978 MiiCreationParams2::Reset +805299a4 0x805299a4 +805299b8 0x805299b8 +805299e0 0x805299e0 +80529a44 0x80529a44 +80529a64 0x80529a64 +80529a84 0x80529a84 +80529ac4 MovieManager::__ct +80529b68 MovieManager::__dt +80529be4 MovieManager::IsMoviePlaying +80529c0c MovieManager::DrawDone +80529c40 MovieManager::Init +80529c48 MovieManager::Quit +80529c4c MovieManager::CheckForEnd +80529ca0 MovieManager::GetCurrentFrameNum +80529cd0 MovieManager::Prepare +80529d68 MovieManager::PrepareAsync +80529e00 MovieManager::RequestPrepare +80529e5c MovieManager::Play +80529ea4 MovieManager::PlayAsync +80529eec MovieManager::RequestPlay +80529f04 MovieManager::Pause +80529f44 MovieManager::PauseAsync +80529f84 MovieManager::RequestPause +80529f9c MovieManager::Close +80529fe0 MovieManager::CloseAsync +8052a024 MovieManager::RequestClose +8052a03c MovieManager::SetVolume +8052a048 MovieManager::GetVolume +8052a04c MovieManager::FillCurTextureInfo +8052a098 ArchivesHolder::CreateByType +8052a1c8 CourseArchivesHolder::__ct +8052a21c CourseArchivesHolder::Reset +8052a2a8 UIArchivesHolder::__ct +8052a2fc UIArchivesHolder::Reset +8052a36c CommonArchivesHolder::__ct +8052a3c0 CommonArchivesHolder::Reset +8052a430 CourseArchivesHolder::__dt +8052a488 UIArchivesHolder::__dt +8052a4e0 CommonArchivesHolder::__dt +8052a538 ArchivesHolder::ArchiveHolder +8052a648 ArchivesHolder::Reset +8052a6dc ArchivesHolder::__dt +8052a760 ArchivesHolder::GetFile +8052a800 ArchivesHolder::HasArchives +8052a864 ArchivesHolder::FileExists +8052a954 ArchivesHolder::LoadArchives +8052aa88 ArchivesHolder::UnmountArchives +8052aae8 ArchivesHolder::LoadFromOther +8052ab6c ArchivesHolder::DumpArchive +8052ac40 ArchivesHolder::ClearArchives +8052aca0 ArchivesHolder::GetTotalMountedArchivesSize +8052ad08 ArchivesHolder::GetFirstMountedArchive +8052ad80 ArchivesHolder::GetFirstMountedArchiveEnd +8052ae08 ArchivesHolder::GetLoadedArchivesCount +8052ae5c NandUtils::SafeOpen +8052aea4 0x8052aea4 +8052aed8 NandUtils::SafeOpen_NandUtils::SafeOpen_switch +8052aedc NandUtils::SafeOpen_caseD_0 +8052aee4 NandUtils::SafeOpen_caseD_-3 +8052aefc NandUtils::SafeOpen_caseD_-4 +8052af04 NandUtils::SafeOpen_caseD_-15 +8052af0c NandUtils::SafeOpen_caseD_-12 +8052af14 NandUtils::SafeOpen_caseD_-14 +8052af64 NandUtils::Open +8052b058 NandUtils::SafeClose +8052b144 NandUtils::Close +8052b228 NandUtils::Read +8052b3f8 NandUtils::GetLength +8052b4d4 NandUtils::Write +8052b684 NandUtils::Check +8052b754 NandUtils::CreateDir +8052b84c NandUtils::GetType +8052b95c NandUtils::Create +8052ba54 NandUtils::Delete +8052bb40 NandUtils::SetStatus +8052bc3c NandUtils::GetStatus +8052bd38 NandUtils::FileExists +8052bd5c NandMgr::Disposer_dt +8052bdf4 NandMgr::CreateInstance +8052be98 NandMgr::DestroyInstance +8052becc NandMgr::__ct +8052bf44 NandMgr::__dt +8052bff8 NandMgr::Init +8052c0b0 NandMgr::ReadRKSYS +8052c168 NandMgr::ReadFile +8052c20c NandMgr::CheckRKSYSLength +8052c2d0 NandMgr::WriteToRKSYS +8052c428 NandMgr::WriteFile +8052c4ec 0x8052c4ec +8052c5d0 0x8052c5d0 +8052c68c NandMgr::CreateRKSYS +8052c7e4 NandMgr::DeleteRKSYS +8052cb10 NandMgr::GetBannerType +8052cb6c 0x8052cb6c +8052cca4 0x8052cca4 +8052cdc8 0x8052cdc8 +8052ce58 0x8052ce58 +8052cef8 0x8052cef8 +8052d00c 0x8052d00c +8052d068 0x8052d068 +8052d118 0x8052d118 +8052d1c0 0x8052d1c0 +8052d270 RKSYS::Rating::AddPoints +8052d298 ElineController::__ct +8052d310 0x8052d310 +8052d384 0x8052d384 +8052d3d0 0x8052d3d0 +8052d46c ElineMgr::__ct +8052d828 ElineMgr::GetController +8052d888 ElineMgr::Update +8052d96c RacedataPlayer::__ct +8052da10 RKSYS::Rating::__dt +8052da50 RacedataPlayer::RegisterPlayerParams +8052daf0 RacedataPlayer::ComputeGPRank +8052dbc8 RacedataScenario::__ct +8052dc68 RacedataPlayer::__dt +8052dca8 RacedataScenario::BlueTeamHasMorePoints +8052dd18 RacedataPlayer::GetTeam +8052dd20 RacedataScenario::GetPlayer(const) +8052dd30 RacedataPlayer::GetPlayerCount +8052dd40 Racedata::Init +8052e42c RacedataPlayer::SetCharacterId +8052e434 RacedataScenario::GetPlayer +8052e444 RacedataPlayer::SetKartId +8052e44c RacedataPlayer::SetPlayerType +8052e454 Racedata::ResetScenarios +8052e640 RacedataPlayer::ResetScores +8052e658 RacedataPlayer::SetPrevFinishPos +8052e660 0x8052e660 +8052e668 0x8052e668 +8052e764 0x8052e764 +8052e768 0x8052e768 +8052e76c 0x8052e76c +8052e770 0x8052e770 +8052e870 0x8052e870 +8052e950 RacedataScenario::UpdatePoints +8052ed18 RacedataScenario::GetGameType +8052ed20 RacedataPlayer::GetPlayerType +8052ed28 RacedataScenario::InitControllers +8052eef0 RacedataScenario::InitGhostPlayer +8052efd4 0x8052efd4 +8052f064 0x8052f064 +8052f1e0 RacedataScenario::UpdateFromPrevRace +8052f4e8 RacedataScenario::InitScreens +8052f788 RacedataScenario::ComputePlayerCounts +8052f924 RacedataScenario::InitRng +8052fa0c RacedataScenario::InitCompetitionSettings +8052fb90 RacedataScenario::Init +8052fe58 Racedata::CreateInstance +8052ffe8 Racedata::DestroyInstance +80530038 Racedata::__dt +805300f4 RacedataScenario::__dt +8053015c Racedata::__ct +805302c4 Racedata::InitRace +805305ac RacedataScenario::operator= +80530864 Racedata::InitAwards +80530f0c RacedataScenario::AreTeamsEnabled +80530f18 0x80530f18 +80530f20 RacedataPlayer::GetCharacterId +80530f28 RacedataPlayer::GetKartId +80530f30 RacedataPlayer::SetMii +80531068 RacedataPlayer::GetMii +80531070 Racedata::InitCredits +80531ce4 Racedata::UpdatePrevFromCur +80531de4 RacedataScenario::RegisterParameters +80531f18 Racedata::GetHudSlotId +80531f2c Racedata::SetGhost +80531f70 Racedata::GetPlayerIdOfLocalPlayer +80531f80 Racedata::HandleRaceFinish? +80531fc8 Racedata::LoadNextGPTrack +80532030 Racedata::IsTTReplay +80532070 Racedata::vf_0x18 +80532074 Racedata::vf_0x14 +80532078 Racedata::vf_0x10 +8053207c 0x8053207c +80532084 Raceinfo::CreateInstance +805320d4 Raceinfo::DestroyInstance +80532124 GMDataGP::__ct +80532188 Raceinfo::CreateGMData +805321c8 Raceinfo::CreateGMData_Raceinfo::CreateGMData_switch +805321cc Raceinfo::CreateGMData_case_0 +80532220 Raceinfo::CreateGMData_case_1 +80532248 Raceinfo::CreateGMData_case_4 +80532254 Raceinfo::CreateGMData_case_3 +805322c8 Raceinfo::CreateGMData_case_7 +805323e0 Raceinfo::CreateGMData_case_9 +80532444 Raceinfo::CreateGMData_case_2 +80532488 GMDataOnlineVSPlayer::__ct +805324ac GMDataOnlineVSPlayer::__dt +805324ec 0x805324ec +8053252c 0x8053252c +8053256c 0x8053256c +805325ac 0x805325ac +805325ec 0x805325ec +8053262c 0x8053262c +8053266c 0x8053266c +805326ac 0x805326ac +805326ec Raceinfo::GetBattleDuration +805327a0 Raceinfo::__ct +80532d44 RaceTimerMgrBase::Reset +80532de0 RaceTimerMgrBase::__dt +80532e3c Raceinfo::__dt +80532f48 RaceinfoPlayer::__dt +80532f88 Raceinfo::Init +8053308c GMData::Init +80533090 Raceinfo::GetRemainingCountdownTime +805330c0 Raceinfo::ComputeDelfinoPierTideState +805331b4 Raceinfo::Update +805336a0 RaceTimerMgrBase::Update +805336a4 Raceinfo::GetLapCount +805336d8 GMData::HandlePositionTracking +80533afc 0x80533afc +80533be8 GMData::vf_0x20 +80533bec 0x80533bec +80533c34 GMData::vf_0x24 +80533c6c Raceinfo::EndPlayerRace +80533d84 Raceinfo::SetPlayerDisconnected +80533dd4 0x80533dd4 +80533ed8 RaceinfoPlayer::__ct +80534194 0x80534194 +805342e8 RaceinfoPlayer::UpdateRealLocal +80534684 0x80534684 +805347f4 RaceinfoPlayer::EndRace +80534934 0x80534934 +805349b8 RaceinfoPlayer::EndLap +80534c78 RaceinfoPlayer::Vanish +80534cbc RaceinfoPlayer::Disconnect +80534ccc 0x80534ccc +80534d6c 0x80534d6c +80534df8 RaceinfoPlayer::UpdateCheckPoint +8053520c 0x8053520c +80535304 RaceinfoPlayer::Update +80535698 0x80535698 +8053569c 0x8053569c +80535718 RaceinfoPlayer::CopyCPUInputs +8053572c RaceinfoPlayer::FillTimerWithLapSplits +80535864 RaceTimerMgr::Reset +80535904 RaceTimerMgr::Update +80535ca0 Raceinfo::CloneTimer +80535d14 GMDataTT::CanRaceEnd +80535de8 GMData::UpdateLocalPlayer +80535e84 GMData::Update +80535ef4 GMData::GetJGPTHolder +80535f28 GMDataGP::CanRaceEnd +80535fe4 GMDataGP::Update +80536054 GMDataVS::CanRaceEnd +80536208 Raceinfo::CanRaceEnd +8053621c Raceinfo::GetJGPTHolder +80536230 Raceinfo::IsAtLeastStage +80536248 0x80536248 +805362dc Raceinfo::GetInitialPhysicsValues +80536324 Raceinfo::GetInitialPhysicsValues::switch +80536328 Raceinfo::GetInitialPhysicsValues::case_3 +80536450 Raceinfo::GetInitialPhysicsValues::case_b +80536484 Raceinfo::GetInitialPhysicsValues::case_c +80536494 Raceinfo::GetInitialPhysicsValues::case_2 +805364a4 Raceinfo::GetInitialPhysicsValues::case_4 +80536540 Raceinfo::GetInitialPhysicsValues::case_0 +805365c8 Raceinfo::GetKTPTHolder +80536614 Raceinfo::GetKTPTHolder_switch +80536618 Raceinfo::GetKTPTHolder_case_3 +80536738 Raceinfo::GetKTPTHolder_case_4 +805367b8 Raceinfo::GetKTPTHolder_case_b +805367e4 Raceinfo::GetKTPTHolder_case_c +805367ec Raceinfo::GetKTPTHolder_case_2 +805367f4 Raceinfo::GetKTPTHolder_case_0 +80536828 Raceinfo::GetStartENPT +805368c4 Raceinfo::GetRawKRT +805368f8 RaceinfoPlayer::ComputeGPRank +80536c84 0x80536c84 +80537190 0x80537190 +805371a4 GMData::vf_0x28 +805371a8 0x805371a8 +805371e8 0x805371e8 +8053724c 0x8053724c +805372b4 0x805372b4 +805372bc 0x805372bc +80537320 0x80537320 +80537388 0x80537388 +80537390 0x80537390 +805373f4 0x805373f4 +8053745c 0x8053745c +80537464 0x80537464 +805374c8 0x805374c8 +80537530 0x80537530 +80537538 0x80537538 +8053759c 0x8053759c +80537604 0x80537604 +8053760c 0x8053760c +80537670 0x80537670 +805376d8 0x805376d8 +805376e0 RaceTimerMgr::__dt +80537740 0x80537740 +80537830 Vec3::Cross +8053787c 0x8053787c +80537b80 Mtx34::GetColumnByIdx +80537ba4 0x80537ba4 +80537c54 0x80537c54 +80537c94 0x80537c94 +80537cd4 0x80537cd4 +80537f98 0x80537f98 +80538084 0x80538084 +80538170 0x80538170 +805381a4 0x805381a4 +80538220 0x80538220 +8053831c KMGHolder::SetKMG +80538344 KMGHolder::ConvertArenaIdToIdx +80538418 GMDataBattle::HandlePositionTracking +80538770 GMDataBalloonBattle::OnItemCollision +80538994 GMDataBalloonBattle::OnKartCollision +80538bc0 GMDataBalloonBattle::OnOOBCollision +80538ce0 0x80538ce0 +80538e00 0x80538e00 +80538eb4 0x80538eb4 +80538f50 GMDataOnlineBalloonBattle::GetKMGSeconds2 +80538f58 GMDataOnlineBalloonBattle::GetKMGSeconds3 +80538f60 0x80538f60 +80538fec GMDataBattle::Init +805391dc GMDataOnlineBalloonBattle::GetKMGSeconds1 +805391e4 GMDataBattle::GetJGPTHolder +805392e4 0x805392e4 +80539404 0x80539404 +80539574 GMDataBalloonBattle::Update +80539770 GMDataBalloonBattle::CanRaceEnd +80539824 GMDataCoinBattle::Update +80539a84 GMDataCoinBattle::CanRaceEnd +80539a90 GMDataCoinBattle::OnItemCollision +80539b30 GMDataCoinBattle::OnKartCollision +80539be8 GMDataCoinBattle::OnOOBCollision +80539c3c GMDataOnlineBalloonBattle::Init +80539f88 GMDataOnlineBalloonBattle::__ct +8053a180 0x8053a180 +8053a1c0 0x8053a1c0 +8053a200 0x8053a200 +8053a240 0x8053a240 +8053a280 0x8053a280 +8053a2c0 0x8053a2c0 +8053a300 0x8053a300 +8053a340 0x8053a340 +8053a380 0x8053a380 +8053a3c0 0x8053a3c0 +8053a720 0x8053a720 +8053a784 0x8053a784 +8053a78c 0x8053a78c +8053a7f0 0x8053a7f0 +8053a7f8 0x8053a7f8 +8053a868 0x8053a868 +8053a870 0x8053a870 +8053a8d4 0x8053a8d4 +8053a8dc 0x8053a8dc +8053a940 0x8053a940 +8053a948 0x8053a948 +8053a9ac 0x8053a9ac +8053a9b4 0x8053a9b4 +8053aa18 0x8053aa18 +8053aa20 0x8053aa20 +8053abdc 0x8053abdc +8053abfc 0x8053abfc +8053ac1c 0x8053ac1c +8053b0a8 0x8053b0a8 +8053b110 0x8053b110 +8053b178 0x8053b178 +8053b1f8 0x8053b1f8 +8053b260 0x8053b260 +8053b2c8 0x8053b2c8 +8053b330 0x8053b330 +8053b398 0x8053b398 +8053b3cc 0x8053b3cc +8053b4cc GMDataOnlineBalloonBattle::OnItemCollision +8053b584 GMDataOnlineBalloonBattle::OnKartCollision +8053b63c GMDataOnlineBalloonBattle::OnOOBCollision +8053b6e8 GMDataOnlineBalloonBattle::vf_0x3c +8053b6f8 0x8053b6f8 +8053b7a4 GMDataOnlineBalloonBattle::OnObjectCollision +8053b850 0x8053b850 +8053b884 0x8053b884 +8053b910 0x8053b910 +8053b98c 0x8053b98c +8053baa0 0x8053baa0 +8053bbf4 GMDataOnlineBalloonBattle::Update +8053bcc8 0x8053bcc8 +8053bcf4 0x8053bcf4 +8053bd5c GMDataOnlineBalloonBattle::Update_switch +8053bd60 GMDataOnlineBalloonBattle::Update_case_1 +8053bd74 GMDataOnlineBalloonBattle::Update_case_2 +8053bd84 GMDataOnlineBalloonBattle::Update_case_3 +8053bd98 GMDataOnlineBalloonBattle::Update_case_4 +8053be1c GMDataOnlineBalloonBattle::Update_case_5 +8053be2c GMDataOnlineBalloonBattle::Update_case_6 +8053be70 GMDataOnlineBalloonBattle::Update_case_7 +8053be78 GMDataOnlineBalloonBattle::Update_case_0 +8053be7c GMDataOnlineBalloonBattle::Update_case_8 +8053cb2c 0x8053cb2c +8053cbc8 0x8053cbc8 +8053ce10 0x8053ce10 +8053d060 0x8053d060 +8053d1a0 0x8053d1a0 +8053d260 0x8053d260 +8053d3a0 GMDataOnlineBalloonBattle::vf_0x28 +8053d428 GMDataOnlineCoinBattle::Update +8053d848 0x8053d848 +8053d8b8 GMDataBattle::vf_0x3c +8053d8bc GMDataCoinBattle::OnObjectCollision +8053d8c0 GMDataCoinBattle::GetKMGSeconds3 +8053d8dc GMDataCoinBattle::GetKMGSeconds2 +8053d8f8 GMDataCoinBattle::GetKMGSeconds1 +8053d914 GMDataOnlineBalloonBattle::CanRaceEnd +8053d91c GMDataBalloonBattle::GetKMGSeconds3 +8053d938 GMDataBalloonBattle::GetKMGSeconds2 +8053d954 GMDataBalloonBattle::GetKMGSeconds1 +8053d970 0x8053d970 +8053d974 0x8053d974 +8053d978 0x8053d978 +8053d97c Raceinfo::RaceinfoGamemodeDataMissionTournament +8053d9b8 Raceinfo::RaceinfoGamemodeDataMissionTournament_switch +8053d9bc Raceinfo::RaceinfoGamemodeDataMissionTournament_caseD_0 +8053d9e8 Raceinfo::RaceinfoGamemodeDataMissionTournament_caseD_1 +8053da14 Raceinfo::RaceinfoGamemodeDataMissionTournament_caseD_7 +8053da50 0x8053da50 +8053daa8 0x8053daa8 +8053daac 0x8053daac +8053daf8 0x8053daf8 +8053dafc 0x8053dafc +8053db10 0x8053db10 +8053db18 0x8053db18 +8053db34 0x8053db34 +8053dff8 0x8053dff8 +8053e018 0x8053e018 +8053e194 0x8053e194 +8053e27c 0x8053e27c +8053e298 0x8053e298 +8053e2a0 0x8053e2a0 +8053e2b0 0x8053e2b0 +8053e2f0 0x8053e2f0 +8053e330 0x8053e330 +8053e370 GMDataOnlineVS::Init +8053e47c 0x8053e47c +8053e680 0x8053e680 +8053e7ac 0x8053e7ac +8053ebbc 0x8053ebbc +8053ec40 0x8053ec40 +8053ed68 0x8053ed68 +8053ee50 0x8053ee50 +8053efd4 0x8053efd4 +8053f184 0x8053f184 +8053f2a4 GMDataOnlineVS::Update +8053f4a0 GMDataOnlineVS::HandlePositionTracking +8053fa10 GMDataOnlineVS::CanRaceEnd +8053fb38 GMDataOnlineVS::vf_0x24 +8053fb98 GMDataOnlineVS::UpdateLocalPlayers +8053fc4c ArchiveMgr::CreateInstance +8053fc9c ArchiveMgr::DestroyInstance +8053fcec ArchiveMgr::__ct +8053fe68 AllkartArchivesLoader::__ct +8053fe94 AllkartArchivesLoader::__dt +8053fed4 0x8053fed4 +8053ff14 0x8053ff14 +8053ff1c ArchiveMgr::__dt +80540000 0x80540000 +80540038 ArchiveMgr::LoadArchiveAsync +805400a0 ArchiveMgr::WaitForLoad +805401ec 0x805401ec +805401fc 0x805401fc +8054020c ArchiveMgr::LoadArchive(holder) +805402c0 ArchiveMgr::LoadArchive(file) +80540394 ArchiveMgr::LoadArchive(holder, 2heaps) +80540450 ArchiveMgr::LoadArchive(type) +80540558 ArchiveMgr::LoadContentArchive +80540680 ArchiveMgr::LoadUIArchive +80540760 ArchiveMgr::LoadCourseArchive +80540918 ArchiveRoot_loadMissionTrack +80540b14 ArchiveMgr::LoadCompetitionArchives +80540cfc ArchiveMgr::LoadKartArchive(holder) +80540e3c ArchiveMgr::LoadKartArchive(playerId) +80540f90 ArchiveMgr::LoadKartArchiveHolder2(playerId) +805410e4 0x805410e4 +805411d4 ArchiveMgr::Unmount(source) +805411e4 ArchiveMgr::Unmount(holder) +805411ec ArchiveMgr::UnmountContentArchive +805411fc ArchiveMgr::GetFile +80541278 ArchiveMgr::GetMenuKartBRRES +80541320 ArchiveMgr::GetKartBRRES +805413c8 ArchiveMgr::GetKartArchiveFile +80541438 ArchiveMgr::GetKartArchiveFileHolder2 +805414a8 ArchiveMgr::GetBSP +8054155c ArchiveMgr::GetContentFileNewBuffer +805415b4 ArchiveMgr::HasArchives +805415c4 ArchiveMgr::HasKartArchive +805415d4 ArchiveMgr::HasKartArchiveHolder2 +805415e4 ArchiveMgr::HasContentArchive +80541614 ArchiveMgr::GetArchive +8054169c ArchiveMgr::SetLayoutResources +80541738 ArchiveMgr::GetArchiveCount +80541794 ArchiveMgr::GetUIArchiveCount +805417a4 ArchiveMgr::AttachUIAccessor +80541878 ArchiveMgr::AttachLayoutResources +80541998 ArchiveMgr::LoadCourseArchiveAsync +805419ac ArchiveMgr::RequestLoadCourseArchiveAsync +805419c8 ArchiveMgr::GetKartArchivePostfix +805419ec ArchiveMgr::GetKartArchivePrefix +80541a10 AsyncCourseArchiveLoader::__ct +80541a70 AsyncCourseArchiveLoader::Init +80541ac4 AsyncCourseArchiveLoader::__dt +80541b58 AsyncCourseArchiveLoader::Load +80541c18 AsyncCourseArchiveLoader::CopyToHolder +80541c38 0x80541c38 +80541c48 ArchiveMgr::ExecuteTask +80541cbc ArchiveMgr::RequestTask +80541ce0 ArchiveMgr::ProcessRequestsAndShutdown +80541e44 ArchiveMgr::LoadKartArchiveAsync +80542030 0x80542030 +80542210 ArchiveMgr::RequestLoadKartArchives +805422cc ArchiveMgr::InitAllkartArchivesLoaders +805423bc ArchiveMgr::ResetAllkartArchivesLoaders +8054247c ArchiveMgr::GetAllKartLoaderHeap +8054248c ArchiveMgr::LoadEarthBrres(heap) +80542524 ArchiveMgr::CreateEarthHeap +80542584 ArchiveMgr::DestroyEarthHeap +805425d0 ArchiveMgr::LoadEarthBrresAsync +80542694 ArchiveMgr::LoadEarthBrres +80542754 ArchiveMgr::RequestLoadEarthBrres +805427bc ArchiveMgr::LoadStaffGhostAsync +80542868 0x80542868 +80542878 RootScene::__ct +805429a8 RootScene::__dt +80542a10 RootScene::SetHeapsGroupId +80542a18 RootScene::CreateArchiveMgr +80542a80 RootScene::CreateGroup1Instances +80542ae0 RootScene::CreateAudioMgr +80542b5c RootScene::CreateScnMgrCreator +80542ba0 RootScene::CreateRacedata +80542be4 RootScene::CreateEffectsMgr +80542c2c RootScene::CreateHomeMenuMgr +80542c74 RootScene::CreateRKNetInstances +80542cc4 RootScene::CreateUIInstances +80542d0c 0x80542d0c +80542d4c RootScene::CreateInstances +805438b4 RootScene::InitInstances +80543acc 0x80543acc +80543b0c 0x80543b0c +80543b10 RootScene::RequestRootScene +80543b7c 0x80543b7c +80543b84 RootScene::enter +80543c84 RootScene::exit +80543c88 RootScene::calc +80543c8c RootScene::draw +80543c90 0x80543c90 +80543d18 RKSYS::Mgr::Disposer__dt +80543db0 RKSYS::Mgr::CreateInstance +80543edc RKSYS::Mgr::DestroyInstance +80543f10 RKSYS::Mgr::__ct +8054400c RKSYS::LicenseMgr::__ct +8054429c RKSYS::LicenseCup::__ct +805442c0 Timer::__ct(bool) +80544310 Timer::SetActive +80544318 RKSYS::LicenseCup::__dt +80544358 RKSYS::LicenseLdbEntry::__ct +8054437c RKSYS::LicenseLdbEntry::__dt +805443bc RKSYS::LicenseUnkTimer::__ct +805443e0 RKSYS::LicenseUnkTimer::__dt +80544420 RKSYS::LicenseFriends::__dt +80544460 RKSYS::LicenseCompletion::__ct +80544484 RKSYS::LicenseCompletion::__dt +805444c4 RKSYS::LicenseStaffGhostStatus::__ct +805444d0 RKSYS::Rating::__ct +805444e8 RKSYS::LicenseUnkTimer2::__ct +8054450c RKSYS::LicenseUnkTimer2::__dt +8054454c RKSYS::LicenseMgr::SetInitialized +80544558 RKSYS::LicenseMgr::__dt +8054460c RKSYS::Mgr::__dt +805446e8 0x805446e8 +80544824 RKSYS::Mgr::RequestTask +80544840 RKSYS::Mgr::LoadTask +8054484c RKSYS::Mgr::LoadRKSYS +80544a38 RKSYS::Mgr::Init +80544a5c RKSYS::Mgr::SaveCoreTask +80544adc RKSYS::Mgr::SaveCoreImpl +80544b48 RKSYS::Mgr::SaveCore +80544bc4 RKSYS::Mgr::WriteLicenses +80544c2c RKSYS::Mgr::WriteCoreAndRequestSaveCore +80544cd8 RKSYS::LicenseMgr::SetCurLicense +80544d10 RKSYS::Mgr::CheckLicenseMagic +80544d40 RKSYS::Mgr::ReadLicense +805455e0 RKSYS::LicenseMgr::ReadMiiName +80545658 RKSYS::LicenseMgr::GetMiiName +8054565c RKSYS::LicenseMgr::ReadRFLCreateID +805456b0 RKSYS::LicenseMgr::SetRFLCreateID +805456f4 RKSYS::LicenseMgr::ReadDWCAccUserData +80545798 RKSYS::LicenseMgr::SetDWCUserData +80545830 RKSYS::LicenseMgr::ReadFriendData +80545854 RKSYS::LicenseMgr::GetFriends +8054585c RKSYS::LicenseMgr::ReadCup +80545934 RKSYS::LicenseMgr::GetCup +8054594c RKSYS::LicenseMgr::ReadLdbEntry +80545a00 RKSYS::LicenseMgr::GetLdbEntry(SaveCourseId) +80545a18 RKSYS::LicenseMgr::ReadUnkTimer +80545a94 RKSYS::LicenseMgr::GetUnkTimer +80545aa4 RKSYS::LicenseMgr::ReadCompletion +80545ad8 RKSYS::LicenseMgr::SetCompletion +80545b00 RKSYS::LicenseMgr::ReadPBGhostBitfield +80545b1c RKSYS::LicenseMgr::SetPBGhostBitfield +80545b28 RKSYS::LicenseMgr::ReadDLGhostBitfield +80545b44 RKSYS::LicenseMgr::SetDLGhostBitfield +80545b50 RKSYS::LicenseMgr::SetRKPDEA_2 +80545b5c RKSYS::LicenseMgr::ReadRKPDEA_2 +80545b84 RKSYS::LicenseMgr::ReadStaffGhostStatus +80545ba8 RKSYS::LicenseMgr::SetStaffGhostStatus +80545bc0 RKSYS::LicenseMgr::SetRKPD1BC_31 +80545bcc RKSYS::LicenseMgr::ReadRKPD1BC_31 +80545be8 RKSYS::LicenseMgr::GetRKPDVR +80545c04 RKSYS::LicenseMgr::SetVR +80545c14 RKSYS::LicenseMgr::GetRKPDBR +80545c30 RKSYS::LicenseMgr::SetBR +80545c40 RKSYS::LicenseMgr::SetOfflineVSWins +80545c60 RKSYS::LicenseMgr::ReadOfflineVSWins +80545c78 RKSYS::LicenseMgr::SetOfflineVSLosses +80545c98 RKSYS::LicenseMgr::ReadOfflineVSLosses +80545cb0 RKSYS::LicenseMgr::SetOfflineBattleWins +80545cd0 RKSYS::LicenseMgr::ReadOfflineBattleWins +80545ce8 RKSYS::LicenseMgr::SetOfflineBattleLosses +80545d08 RKSYS::LicenseMgr::ReadOfflineBattleLosses +80545d20 RKSYS::LicenseMgr::SetWFCVSWins +80545d40 RKSYS::LicenseMgr::ReadWFCVSWins +80545d58 RKSYS::LicenseMgr::SetWFCVSLosses +80545d78 RKSYS::LicenseMgr::ReadWFCVSLosses +80545d90 RKSYS::LicenseMgr::SetWFCBattleWins +80545db0 RKSYS::LicenseMgr::ReadWFCBattleWins +80545dc8 RKSYS::LicenseMgr::SetWFCBattleLosses +80545de8 RKSYS::LicenseMgr::ReadWFCBattleLosses +80545e00 RKSYS::LicenseMgr::SetGhostRacesWins +80545e20 RKSYS::LicenseMgr::ReadGhostRacesWins +80545e38 RKSYS::LicenseMgr::SetGhostRacesLosses +80545e58 RKSYS::LicenseMgr::ReadGhostRacesLosses +80545e70 RKSYS::LicenseMgr::SetTotalRacePlayed +80545e90 RKSYS::LicenseMgr::ReadTotalRacePlayed +80545ea8 RKSYS::LicenseMgr::SetTotalBattlePlayed +80545ec8 RKSYS::LicenseMgr::ReadTotalBattlePlayed +80545ee0 RKSYS::LicenseMgr::SetRacesOnWheel +80545f00 RKSYS::LicenseMgr::ReadRacesOnWheel +80545f18 RKSYS::LicenseMgr::SetBattlesOnWheel +80545f38 RKSYS::LicenseMgr::ReadBattlesOnWheel +80545f50 RKSYS::LicenseMgr::ReadDistanceTravelled +80545f68 RKSYS::LicenseMgr::SetDistanceTravelled +80545fa4 RKSYS::LicenseMgr::SetGhostDataChallengesSent +80545fc4 RKSYS::LicenseMgr::ReadGhostDataChallengesSent +80545fdc RKSYS::LicenseMgr::SetGhostDataChallengesReceived +80545ffc RKSYS::LicenseMgr::ReadGhostDataChallengesReceived +80546014 RKSYS::LicenseMgr::SetItemHitsDelivered +80546034 RKSYS::LicenseMgr::ReadItemHitsDelivered +8054604c RKSYS::LicenseMgr::SetItemHitsReceived +8054606c RKSYS::LicenseMgr::ReadItemHitsReceived +80546084 RKSYS::LicenseMgr::SetTricksPerformed +805460a4 RKSYS::LicenseMgr::ReadTricksPerformed +805460bc RKSYS::LicenseMgr::SetTimes1stPlaceAchieved +805460dc RKSYS::LicenseMgr::ReadTimes1stPlaceAchieved +805460f4 RKSYS::LicenseMgr::ReadDistancetravelledwhilein1stplace +8054610c RKSYS::LicenseMgr::SetDistancetravelledwhilein1stplace +80546148 RKSYS::LicenseMgr::ReadDistancetravelledonVSRaces +80546160 RKSYS::LicenseMgr::SetDistancetravelledonVSRaces +8054619c RKSYS::LicenseMgr::SetCompetitionsEntered +805461bc RKSYS::LicenseMgr::ReadCompetitionsEntered +805461d4 RKSYS::LicenseMgr::SetRacesOnCharacter +805461fc RKSYS::LicenseMgr::ReadRacesOnCharacter +8054621c RKSYS::LicenseMgr::SetRacesOnKart +80546244 RKSYS::LicenseMgr::ReadRacesOnKart +80546264 RKSYS::LicenseMgr::SetRacesOnTrack +8054628c RKSYS::LicenseMgr::ReadRacesOnTrack +805462ac RKSYS::LicenseMgr::SetRacesOnBattleArena +805462d4 RKSYS::LicenseMgr::ReadRacesOnBattleArena +805462f4 RKSYS::LicenseMgr::SetUnk55f0 +80546310 RKSYS::LicenseMgr::ReadUnk55f0 +8054634c RKSYS::LicenseMgr::SetDefaultDrift +80546358 RKSYS::LicenseMgr::ReadDefaultDrift +80546374 RKSYS::LicenseMgr::Set CC +80546388 RKSYS::LicenseMgr::ReadCC +805463ac RKSYS::LicenseMgr::SetCPUDifficulty +805463c0 RKSYS::LicenseMgr::ReadCPUDifficulty +805463e4 RKSYS::LicenseMgr::SetVehicleType +805463f8 RKSYS::LicenseMgr::ReadVehicleType +8054641c RKSYS::LicenseMgr::SetCourseChoice +80546430 RKSYS::LicenseMgr::ReadCourseChoice +80546454 RKSYS::LicenseMgr::SetItemsType +80546468 RKSYS::LicenseMgr::ReadItemsType +8054648c RKSYS::LicenseMgr::SetTotalRaceCount +805464a0 RKSYS::LicenseMgr::ReadTotalRaceCount +805464c4 RKSYS::LicenseMgr::ReadUnkTimer2 +8054652c RKSYS::LicenseMgr::SetUnkTimer2 +8054656c RKSYS::LicenseMgr::ReadRKPD_1BC +805465cc RKSYS::LicenseMgr::SetRKPD_1BC +805465f8 RKSYS::LicenseMgr::ReadRKPD_5680 +80546660 RKSYS::LicenseUnk5680::Set +805466b4 RKSYS::LicenseMgr::SetRKPD_5680 +805466dc RKSYS::LicenseMgr::ReadRKPD_5688 +80546734 RKSYS::LicenseMgr::SetRKPD_5688 +80546744 RKSYS::LicenseMgr::ReadRKPD_568c +8054677c RKSYS::LicenseMgr::SetRKPD_568c +805467a8 RKSYS::LicenseMgr::SetRKPD_568c_31 +805467b4 RKSYS::LicenseMgr::ReadRKPD_5688_31 +805467d0 RKSYS::LicenseMgr::WriteToRKPD +80546fb4 RKSYS::LicenseMgr::IsInitialized +80546fc0 RKSYS::LicenseMgr::WriteMiiName +80546fdc RKSYS::LicenseMgr::GetMiiName +80546fe0 RKSYS::LicenseMgr::WriteRFLCreateID +80547034 RKSYS::LicenseMgr::GetRFLCreateID +8054703c RKSYS::LicenseMgr::WriteDWCAccUserData +805470d0 RKSYS::LicenseMgr::GetDWCAccUserData +805470dc RKSYS::LicenseMgr::WriteFriendData +805470fc RKSYS::LicenseMgr::GetFriends +80547104 RKSYS::LicenseMgr::WriteCup +80547128 RKSYS::LicenseMgr::GetCup +80547140 RKSYS::LicenseMgr::WriteLdbEntry +8054726c RKSYS::LicenseMgr::GetLdbEntry +80547284 RKSYS::LicenseMgr::WriteUnkTimer +80547374 RKSYS::LicenseMgr::GetUnkTimer +80547384 RKSYS::LicenseMgr::WriteCompletion +805473b8 RKSYS::LicenseMgr::GetCompletion +805473c4 RKSYS::LicenseMgr::WritePBGhostBitfield +805473dc RKSYS::LicenseMgr::GetPBGhostBitfield +805473e8 RKSYS::LicenseMgr::WriteDLGhostBitfield +80547400 RKSYS::LicenseMgr::GetDLGhostBitfield +8054740c RKSYS::LicenseMgr::WriteRKPD_EA_2 +8054742c RKSYS::LicenseMgr::GetRKPD_EA_2 +80547438 RKSYS::LicenseMgr::WriteRKPD1BC_31 +80547458 RKSYS::LicenseMgr::GetRKPD1BC_31 +80547464 RKSYS::LicenseMgr::WriteVR +80547480 RKSYS::LicenseMgr::GetVR +8054748c RKSYS::LicenseMgr::WriteBR +805474a8 RKSYS::LicenseMgr::GetBR +805474b4 RKSYS::LicenseMgr::WriteOfflineVSWins +805474cc RKSYS::LicenseMgr::GetOfflineVSWins +805474d8 RKSYS::LicenseMgr::WriteOfflineVSLosses +805474f0 RKSYS::LicenseMgr::GetOfflineVSLosses +805474fc RKSYS::LicenseMgr::WriteOfflineBattleWins +80547514 RKSYS::LicenseMgr::GetOfflineBattleWins +80547520 RKSYS::LicenseMgr::WriteOfflineBattleLosses +80547538 RKSYS::LicenseMgr::GetOfflineBattleLosses +80547544 RKSYS::LicenseMgr::WriteWFCVSWins +8054755c RKSYS::LicenseMgr::GetWFCVSWins +80547568 RKSYS::LicenseMgr::WriteWFCVSLosses +80547580 RKSYS::LicenseMgr::GetWFCVSLosses +8054758c RKSYS::LicenseMgr::WriteWFCBattleWins +805475a4 RKSYS::LicenseMgr::GetWFCBattleWins +805475b0 RKSYS::LicenseMgr::WriteWFCBattleLosses +805475c8 RKSYS::LicenseMgr::GetWFCBattleLosses +805475d4 RKSYS::LicenseMgr::WriteGhostRacesWins +805475ec RKSYS::LicenseMgr::GetGhostRacesWins +805475f8 RKSYS::LicenseMgr::WriteGhostRacesLosses +80547610 RKSYS::LicenseMgr::GetGhostRacesLosses +8054761c RKSYS::LicenseMgr::WriteTotalRacePlayed +80547634 RKSYS::LicenseMgr::GetTotalRacePlayed +80547654 RKSYS::LicenseMgr::WriteTotalBattlePlayed +8054766c RKSYS::LicenseMgr::WriteRacesOnWheel +8054768c RKSYS::LicenseMgr::GetTotalBattlePlayed +805476a4 RKSYS::LicenseMgr::GetRacesOnWheel +805476c4 RKSYS::LicenseMgr::WriteBattlesOnWheel +805476dc RKSYS::LicenseMgr::GetBattlesOnWheel +805476fc RKSYS::LicenseMgr::WriteDistanceTravelled +80547714 RKSYS::LicenseMgr::GetDistanceTravelled +80547734 RKSYS::LicenseMgr::WriteGhostDataChallengesSent +8054774c RKSYS::LicenseMgr::GetGhostDataChallengesSent +8054776c RKSYS::LicenseMgr::WriteGhostDataChallengesReceived +80547784 RKSYS::LicenseMgr::GetGhostDataChallengesReceived +805477a4 RKSYS::LicenseMgr::WriteItemHitsDelivered +805477bc RKSYS::LicenseMgr::GetItemHitsDelivered +805477dc RKSYS::LicenseMgr::WriteItemHitsReceived +805477f4 RKSYS::LicenseMgr::GetItemHitsReceived +80547814 RKSYS::LicenseMgr::WriteTricksPerformed +8054782c RKSYS::LicenseMgr::GetTricksPerformed +8054784c RKSYS::LicenseMgr::WriteTimes1stPlaceAchieved +80547864 RKSYS::LicenseMgr::GetTimes1stPlaceAchieved +80547884 RKSYS::LicenseMgr::WriteDistancetravelledwhilein1stplace +8054789c RKSYS::LicenseMgr::GetDistancetravelledwhilein1stplace +805478bc RKSYS::LicenseMgr::WriteDistancetravelledonVSRaces +805478d4 RKSYS::LicenseMgr::GetDistancetravelledonVSRaces +805478f4 RKSYS::LicenseMgr::WriteCompetitionsEntered +8054790c RKSYS::LicenseMgr::GetCompetitionsEntered +80547930 RKSYS::LicenseMgr::WriteRacesOnCharacter +80547950 RKSYS::LicenseMgr::GetRacesOnCharacter +8054797c RKSYS::LicenseMgr::WriteRacesOnKart +8054799c RKSYS::LicenseMgr::GetRacesOnKart +805479c8 RKSYS::LicenseMgr::WriteRacesOnTrack +805479e8 RKSYS::LicenseMgr::GetRacesOnTrack +80547a14 RKSYS::LicenseMgr::WriteRacesOnBattleArena +80547a34 RKSYS::LicenseMgr::WriteUnk55f0 +80547a64 RKSYS::LicenseMgr::GetUnk55f0 +80547a80 RKSYS::LicenseMgr::WriteStaffGhostStatus +80547aa4 RKSYS::LicenseMgr::GetStaffGhostStatus +80547ab8 RKSYS::LicenseMgr::WriteDefaultDrift +80547ad8 RKSYS::LicenseMgr::GetDefaultDrift +80547ae4 RKSYS::LicenseMgr::WriteCC +80547b0c RKSYS::LicenseMgr::GetCC +80547b20 RKSYS::LicenseMgr::WriteCPUDifficulty +80547b48 RKSYS::LicenseMgr::GetCPUDifficulty +80547b5c RKSYS::LicenseMgr::WriteVehicleType +80547b84 RKSYS::LicenseMgr::GetVehicleType +80547b98 RKSYS::LicenseMgr::WriteCourseChoice +80547bc0 RKSYS::LicenseMgr::GetCourseChoice +80547bd4 RKSYS::LicenseMgr::WriteItemsType +80547bfc RKSYS::LicenseMgr::GetItemsType +80547c10 RKSYS::LicenseMgr::WriteTotalRaceCount +80547c38 RKSYS::LicenseMgr::GetTotalRaceCount +80547c4c RKSYS::LicenseMgr::WriteUnkTimer2 +80547d1c RKSYS::LicenseMgr::GetUnkTimer2 +80547d30 RKSYS::LicenseMgr::GetRKPD_1BC +80547d74 RKSYS::LicenseMgr::WriteRKPD_1BC +80547e1c RKSYS::LicenseMgr::GetRKPD_5680 +80547e28 RKSYS::LicenseMgr::WriteRKPD_5680 +80547e84 RKSYS::LicenseUnk5680::Get_0 +80547e8c RKSYS::LicenseUnk5680::Get_7 +80547e94 RKSYS::LicenseUnk5680::Get_11 +80547e9c RKSYS::LicenseMgr::GetRKPD_5688 +80547eac RKSYS::LicenseMgr::WriteRKPD_5688 +80547f08 RKSYS::LicenseMgr::WriteRKPD_568c +80547f40 RKSYS::LicenseMgr::GetRKPD_568c +80547f4c RKSYS::LicenseMgr::WriteRKPD_5688_31 +80547f6c RKSYS::LicenseMgr::GetRKPD_5688_31 +80547f78 RKSYS::LicenseMgr::WriteMagic +80547f98 RKSYS::LicenseUnk5680::__ct(copy) +80547fbc RKSYS::Mgr::EraseLicense +80548030 RKSYS::Mgr::CreateLicense +80548100 0x80548100 +80548104 RKSYS::Reset +80548224 RKSYS::SetCRC +80548260 RKSYS::IsCRCCorrect +805482a8 RKSYS::CheckMagicAndVersion +805482d8 RKSYS::CheckRegion +80548300 RKSYS::ResetRKPD +80548454 RKSYS::CreateRKPD +805484f4 RKSYS::ReadRKPDCup +805485b8 RKSYS::ReadRKPDLdbEntry +80548658 RKSYS::ReadRKPDUnkTimer +805486c0 RKSYS::ReadRKPDUnkTimer2 +8054884c RKSYS::WriteRKPDLdbEntry +80548964 RKSYS::WriteRKPDUnkTimer +80548a40 RKSYS::WriteRKPDUnkTimer2 +80548afc RKSYS::ResetRKGD +80548b8c RKSYS::GetRKGDUnk8Idx +80548c58 RKSYS::SetRKGDUnk8 +80548e30 RKSYS::LicenseMgr::GetLdbEntry(CourseId) +80548e90 RKSYS::LicenseMgr::OverwriteAndStoreLdbEntry +80548f84 RKSYS::LicenseMgr::StoreLdbEntry +805490f4 RKSYS::LicenseMgr::GetGhostPosition +805491e4 RKSYS::LicenseMgr::GetBestRank +805493b0 RKSYS::LicenseMgr::GetFavouriteCharacter +80549470 RKSYS::LicenseMgr::GetFavouriteKart +80549550 RKSYS::LicenseMgr::GetFavouriteTrack +80549670 RKSYS::LicenseMgr::GetFavouriteBattleArena +80549720 RKSYS::LicenseMgr::GetWheelRacesPercentage +80549810 RKSYS::LicenseMgr::GetInFirstPercentage +80549878 RKSYS::LicenseMgr::Copy5680To5688 +80549898 RKSYS::LicenseMgr::Generate5688Bitfield +805498d0 RKSYS::LicenseCompletion::Reset +805498e8 RKSYS::LicenseCompletion::GetOffsetAndRelativeBit +805498fc RKSYS::LicenseCompletion::SetComplete +80549924 RKSYS::LicenseCompletion::ResetComplete +8054994c RKSYS::LicenseCompletion::IsCompleted +8054997c RKSYS::Mgr::GetGhostOffset +805499bc RKSYS::Mgr::ReadGhostTask +80549a14 RKSYS::Mgr::ReadGhost +80549a68 RKSYS::Mgr::ReadGhostAsync +80549c44 RKSYS::Mgr::LoadGhostGroupTask +80549c50 RKSYS::Mgr::LoadGhostGroup +80549dac RKSYS::Mgr::LoadGhostGroupAsync +80549ee4 RKSYS::Mgr::WriteGhostTask +80549f98 RKSYS::Mgr::WriteGhost +8054a048 RKSYS::Mgr::SaveGhostAsync +8054a288 RKSYS::Mgr::EraseGhostTask +8054a328 RKSYS::Mgr::EraseGhost +8054a3c4 RKSYS::Mgr::EraseGhostAsync +8054a5c8 RKSYS::Mgr::GetValidLicensesCount +8054a658 RKSYS::Mgr::GetCombinedLicenseCompletions +8054a73c RKSYS::Mgr::ReplaceBinaryTask +8054a748 RKSYS::Mgr::ReplaceBinary +8054a868 RKSYS::Mgr::EraseBinary +8054a9b8 RKSYS::Mgr::GetCountry +8054a9e0 RKSYS::Mgr::GetLongitude +8054aa08 RKSYS::Mgr::GetLatitude +8054aa30 GameSceneCreator::CreateInstance +8054aa64 GameSceneCreator::create +8054ab28 GameSceneCreator::destroy +8054ab2c LensFlare::QuadDrawer::__ct +8054adb0 0x8054adb0 +8054adb4 GameScreenEffectsMgr::GetRootScreenWidth +8054add0 GameScreenEffectsMgr::GetRootScreenHeight +8054adf0 LensFlare::QuadDrawer::Draw +8054afc4 0x8054afc4 +8054b094 LensFlare::Texture::__ct +8054b164 LensFlare::Texture::SetPosition +8054b1a8 LensFlare::Texture::CopyFromEFB +8054b1d4 0x8054b1d4 +8054b310 LensFlare::__ct +8054b514 0x8054b514 +8054b578 LensFlare::__dt +8054b62c LensFlare::OnFadeOutPrepareStart +8054b66c LensFlare::FadeOutPrepareUpdate +8054b7c4 LensFlare::OnFadeOutStart +8054b7c8 LensFlare::FadeOutUpdate +8054b870 LensFlare::OnNoTargetStart +8054b8c0 LensFlare::NoTargetUpdate +8054b91c LensFlare::OnNormalStateStart +8054b9cc LensFlare::OnNormalStateUpdate +8054bb08 LensFlare::Update +8054be10 Vec2::MultiplyByScalar +8054be2c LensFlare::DrawQuads +8054be94 LensFlare::CopyEFBToTexture +8054bec0 0x8054bec0 +8054bec8 0x8054bec8 +8054bf50 LensFlareMgr::__ct +8054c350 LensFlareMgr::__dt +8054c414 LensFlareMgr::GetBRRESTexCount +8054c41c LensFlareMgr::GetRawTexture +8054c498 LensFlareMgr::GetTextureWidth +8054c500 LensFlareMgr::GetTextureHeight +8054c568 LensFlareMgr::GetTextureName +8054c5e4 LensFlareMgr::GetTextureFormat +8054c5f4 LensFlareMgr::Update +8054c660 LensFlareMgr::DrawTextures +8054c7b0 LensFlareMgr::Draw +8054c834 LensFlareMgr::CopyEFBToTextures +8054c8f0 LensFlareMgr::AddLensFX +8054c958 LensFlareMgr::EnableExternalLens +8054c97c LensFlareMgr::GetTargetPosition +8054cc44 LensFlareMgr::GetScreenPosition +8054ce2c 0x8054ce2c +8054ce58 0x8054ce58 +8054ce90 FilterEffect::__ct +8054d0b0 0x8054d0b0 +8054d114 FilterEffectMgr::SetColor +8054d13c FilterEffect::Update +8054d210 0x8054d210 +8054d220 0x8054d220 +8054d22c 0x8054d22c +8054d2c8 0x8054d2c8 +8054d2cc 0x8054d2cc +8054d320 0x8054d320 +8054d324 0x8054d324 +8054d33c 0x8054d33c +8054d34c 0x8054d34c +8054d36c 0x8054d36c +8054d370 0x8054d370 +8054d388 FilterEffect::Draw +8054d488 FilterEffectMgr::__ct +8054d6f4 FilterEffectMgr::__dt +8054d7ac FilterEffect:__dt +8054d814 FilterEffectMgr::Update +8054d908 FilterEffectMgr::Draw +8054da50 0x8054da50 +8054da74 0x8054da74 +8054daa4 0x8054daa4 +8054dc44 CourseFilterEffect::__ct +8054de24 0x8054de24 +8054de88 CourseFilterEffect::Update +8054df5c CourseFilterEffect::OnNoFilterStart +8054df6c CourseFilterEffect::OnWhiteFilterPrepareStart +8054df90 CourseFilterEffect::WhiteFilterPrepareUpdate +8054dfe4 CourseFilterEffect::OnWhiteFilterStart +8054dfe8 CourseFilterEffect::WhiteFilterUpdate +8054e000 CourseFilterEffect::OnBlackFilterStart +8054e020 CourseFilterEffect::BlackFilterUpdate +8054e038 CourseFilterEffect::OnAlphaFadeoutStart +8054e03c CourseFilterEffect::AlphaFadeoutUpdate +8054e098 CourseFilterEffect::Draw +8054e198 CourseFilterEffectMgr::__ct +8054e3c8 CourseFilterEffectMgr::__dt +8054e480 CourseFilterEffect::__dt +8054e4e8 CourseFilterEffectMgr::Update +8054e5dc CourseFilterEffectMgr:Draw +8054e714 0x8054e714 +8054e72c 0x8054e72c +8054e8cc CreditsBars::__ct +8054e9a8 0x8054e9a8 +8054e9ac CreditsBars::Init +8054e9d4 CreditsBars::Draw +8054eb54 GameScreenEffectsMgr::__ct +8054f120 GameScreenEffectsMgr::__dt +8054f1b0 GameScreenEffectsMgr::Update +8054f3c8 0x8054f3c8 +8054f41c GameScreenEffectsMgr::Draw +8054f7a4 GameScreenEffectsMgr::DrawCourseFilterEffects +8054f8e0 GameScreenEffectsMgr::CopyEFBToLensFlareTextures +8054f9a8 GameScreenEffectsMgr::AddLensFX +8054fa10 GameScreenEffectsMgr::EnableExternalLensFlare +8054fa38 0x8054fa38 +8054fa60 0x8054fa60 +8054fa94 GameScreenEffectsMgr::RequestWhiteCourseFilter +8054fab0 CreditsBars::__dt +8054faf0 GameScreenEffects_cpp_sinit_ +8054fb2c IsVehicleTypeUnlocked +8054fbd4 0x8054fbd4 +8054fbe4 GetCupType +8054fc44 IsCupUnlocked +8054fed8 IsCharacterUnlocked +8054ff0c IsCharacterUnlocked_switch +8054ff10 IsCharacterUnlocked_caseD_11 +8054ffa8 IsCharacterUnlocked_caseD_18 +80550000 0x80550000 +80550030 IsCharacterUnlocked_caseD_1a +805500b4 IsCharacterUnlocked_caseD_10 +805500cc IsKartUnlocked +805501a4 0x805501a4 +805501c4 0x805501c4 +80550468 isFastStaffGhostUnlocked +80550548 0x80550548 +805506e8 0x805506e8 +805506ec BitRotator::__ct +8055070c BitRotator::Update +80550730 GetTick +80550734 GetHour +805507b0 THP::CreateAudioDecodeThread +80550880 THP::AudioDecodeThreadStart +805508a0 THP::AudioDecodeThreadCancel +805508e8 THP::AudioDecoder +80550910 THP::AudioDecoderForOnMemory +805509bc THP::AudioDecode +80550aa8 THP::PopFreeAudioBuffer +80550adc THP::PushFreeAudioBuffer +80550af0 THP::PopDecodedAudioBuffer +80550b34 THP::PushDecodedAudioBuffer +80550b48 THP::PlayerInit +80550c68 THP::PlayerQuit +80550cc0 THP::PlayerOpen +80550f48 THP::PlayerClose +80550f9c THP::PlayerCalcNeedMemory +80551054 THP::PlayerSetBuffer +80551294 THP::InitAllMessageQueue +80551364 THP::PrepareReady +80551378 THP::PlayerPrepare +805515f8 THP::PlayerPlay +80551658 THP::PlayerStop +80551734 THP::PlayerPause +80551770 THP::PlayerSkip +80551940 THP::PlayControl +80551c04 THP::ProperTimingForGettingNextFrame +80551d30 THP::PlayerDrawCurrentFrame +80551d38 THP::PlayerGetVideoInfo +80551d80 THP::PlayerGetAudioInfo +80551dc8 THP::PlayerGetFrameRate +80551df0 THP::PlayerGetTotalFrame +80551e14 THP::PlayerGetState +80551e24 THP::PlayerDrawDone +80551e94 0x80551e94 +80551f00 THP::AudioMixCallback +8055206c THP::MixAudio +80552438 THP::PlayerSetVolume +80552564 THP::PlayerGetVolume +8055259c THP::CreateReadThread +8055263c THP::ReadThreadStart +8055265c THP::ReadThreadCancel +805526a4 THP::Reader +80552794 THP::PopReadedBuffer +805527c8 THP::PopReadedBuffer2 +805527dc THP::PopFreeReadBuffer +80552810 THP::PushFreeReadBuffer +80552824 THP::PopReadedBuffer2 +80552858 THP::PushReadedBuffer2 +8055286c THP::CreateVideoDecodeThread +80552940 THP::VideoDecodeThreadStart +80552960 THP::VideoDecodeThreadCancel +805529a8 THP::VideoDecoder +80552a74 THP::VideoDecoderOnMemory +80552ba4 THP::VideoDecode +80552cf0 THP::PopFreeTextureSet +80552d24 THP::PushFreeTextureSet +80552d38 THP::PopDecodedTextureSet +80552d7c THP::PushDecodedTextureSet +80552d90 courseIdToGhostIndex +80552e74 GetCourseIdBySlot +80552e98 0x80552e98 +80553038 0x80553038 +80553180 0x80553180 +80553314 0x80553314 +80553564 0x80553564 +8055368c 0x8055368c +805536d8 0x805536d8 +805536fc 0x8055368c_switch +80553700 0x8055368c_switch_805536fc::CaseD_0 +80553708 0x8055368c_switch_805536fc::CaseD_fffffffe +80553720 0x8055368c_switch_805536fc::CaseD_fffffffc +80553728 0x8055368c_switch_805536fc::CaseD_ffffffff +80553730 0x8055368c_caseD_fffffff2 +80553788 0x80553788 +805537cc 0x805537cc +80553824 0x80553824 +80553828 0x80553828 +8055382c 0x8055382c +80553838 0x80553838 +80553844 0x80553844 +80553850 GlobeScene::__ct +80553894 GlobeScene::__dt +805538ec GlobeScene::OnEnter +805539b8 GlobeScene::CreateAndInitInstances +80553a88 GlobeScene::OnEnterEnd +80553ac8 GlobeScene::OnExit +80553b10 GlobeScene::OnCalc +80553b88 RaceScene::__ct +80553bd4 RaceScene::__dt +80553c50 RaceScene::OnEnter +80554208 RaceScene::CreateAndInitInstances +8055472c RaceScene::OnEnterEnd +805549ac RaceScene::vf_0x58 +805549b0 RaceScene::OnExit +80554a94 RaceScene::OnReinit +80554ad4 RaceScene::UpdateRaceInstances +80554d10 0x80554d10 +80554d90 RaceScene::UpdateSomeInstances +80554e14 RaceScene::isPaused +80554e6c RaceScene::OnCalc +80554f68 RaceScene::GetScreenCount +80554f78 RaceScene::isSpectating +80554f88 RaceScene::vf_0x48 +80554f94 RaceScene::vf_0x50 +80554f9c RaceScene::vf_0x58 +80554fa4 MenuScene::__ct +80554fe8 MenuScene::__dt +80555040 MenuScene::OnEnter +80555154 MenuScene::CreateAndInitInstances +80555264 MenuScene::OnEnterEnd +80555298 MenuScene::OnExit +805552c8 MenuScene::OnCalc +8055531c RelProlog +80555368 RelEpilog +805553b0 RelUnresolvedSection +80555464 Random::__ct +805554d0 Random::Reset +80555514 Random::__ct(seed) +80555538 Random::__dt +80555578 Random::Next +805555cc Random::NextLimited +80555628 Random::NextFloat +805556a8 Random::NextFloatLimited +8055572c 0x8055572c +80555750 ModelTransformator::__ct +8055585c ModelTransformator::AppendAnmHolder +80555864 ModelTransformator::AppendAnmBlend +8055586c ModelTransformator::SetBlendWeightDecrement +80555890 ModelTransformator::GetBlendWeightDecrement +805558bc ModelTransformator::DecrementBlendWeight +805558ec ModelTransformator::DetachAllAnmFromBlend(type) +8055591c ModelTransformator::DetachAllAnmFromBlend(listIdx) +8055597c ModelTransformator::LinkAnimationByName +80556718 ModelTransformator::IsG3dObj +80556734 AnmChrBlendHolder::__dt +80556774 0x80556774 +805567b4 ModelTransformator::LinkAnimationByBRRESIdx +80556f10 ModelTransformator::LinkChrAnmToBone +805570ec 0x805570ec +805571a4 ModelTransformator::LinkEmptyAnm +80557234 ModelTransformator::GetAnmHolderByIdx +8055723c ModelTransformator::GetAnmObjByIdx +80557264 ModelTransformator::AddAnmToScnMdlAndSetupBRASD +805572f0 ModelTransformator::AddAnmToScnMdl +80557308 ModelTransformator::OverwriteAnm +80557328 ModelTransformator::RestoreAnm +80557340 ModelTransformator::GetAnmHolderByType +80557350 ModelTransformator::GetPrevAnmHolderByType +80557360 ModelTransformator::IsTypeActive +8055737c ModelTransformator::IsTypeInPrev +80557398 ModelTransformator::GetAnmObjByType +805573ac ModelTransformator::DetachHolderFromParent +805573cc ModelTransformator::PlayAnmNoBlend +80557548 0x80557548 +80557650 ModelTransformator::AddAnmBlendToScnMdl +80557684 ModelTransformator::PlayAnmWithBlend +80557830 ModelTransformator::StopAnmType +805578d8 ModelTransformator::UpdateBRASDAndBlend +80557954 AnmHolder::GetFrame +80557960 ModelTransformator::UpdateBlend +80557b20 AnmHolder::IsLooped +80557b28 AnmHolder::GetFrameCount +80557b34 0x80557b34 +80557bb8 ModelTransformator::Bind +80557c10 ModelTransformator::UpdateActiveAnms +80557cf8 0x80557cf8 +80557d90 0x80557d90 +80557dd0 0x80557dd0 +80557ef4 0x80557ef4 +80557fb0 ModelTransformator::vf_0x8 +80557fb4 AnmHolder::SetUpdateRate +80557fb8 AnmHolder::GetUpdateRate +80557fc4 AnmHolder::UpdateRateAndSetFrame +80557fc8 AnmHolder::SetFrame +80557fcc 0x80557fcc +8055800c 0x8055800c +80558054 0x80558054 +805580a4 0x805580a4 +805582e8 0x805582e8 +805583e8 0x805583e8 +80558538 0x80558538 +80558704 0x80558704 +80558794 AnmTexPatOverrideHolder::__dt +805587d4 AnmTexPatOverrideHolder::SetWeight +8055881c AnmTexPatOverrideHolder::GetWeight +8055886c AnmTexPatOverrideHolder::AttachNew +80558ab0 AnmTexPatOverrideHolder::Attach +80558bb0 AnmTexPatOverrideHolder::DecrementWeight +80558d00 AnmTexPatOverrideHolder::ShouldDetachFromParent +80558ecc AnmTexPatOverrideHolder::DetachAll +80558f5c 0x80558f5c +80558f9c 0x80558f9c +80558fe4 0x80558fe4 +80559034 0x80559034 +80559278 0x80559278 +80559378 0x80559378 +805594c8 0x805594c8 +80559694 0x80559694 +80559724 0x80559724 +80559764 0x80559764 +805597ac 0x805597ac +805597fc 0x805597fc +80559a40 0x80559a40 +80559b40 0x80559b40 +80559c90 0x80559c90 +80559e5c 0x80559e5c +80559eec AnmChrBlendHolder::__dt +80559f2c AnmChrBlendHolder::SetWeight +80559fdc AnmChrBlendHolder::GetWeight +8055a07c AnmChrBlendHolder::AttachNew +8055a2c0 AnmChrBlendHolder::Attach +8055a3c0 AnmChrBlendHolder::DecrementWeight +8055a510 AnmChrBlendHolder::ShouldDetachFromParent +8055a6dc AnmChrBlendHolder::DetachAll +8055a76c 0x8055a76c +8055a780 0x8055a780 +8055a794 0x8055a794 +8055a800 0x8055a800 +8055a884 0x8055a884 +8055a8e8 0x8055a8e8 +8055a8fc 0x8055a8fc +8055a910 AnmTexPatHolder::GetFrame +8055a924 AnmTexPatHolder::SetFrame +8055a938 AnmTexPatHolder::UpdateRateAndSetFrame +8055a9a4 AnmTexPatHolder::GetFrameCount +8055aa28 AnmTexPatHolder::IsLooped +8055aa8c AnmTexPatHolder::GetUpdateRate +8055aaa0 AnmTexPatHolder::SetUpdateRate +8055aab4 0x8055aab4 +8055aac8 0x8055aac8 +8055aadc 0x8055aadc +8055ab48 0x8055ab48 +8055abcc 0x8055abcc +8055ac30 0x8055ac30 +8055ac44 0x8055ac44 +8055ac58 0x8055ac58 +8055ac6c 0x8055ac6c +8055ac80 0x8055ac80 +8055acec 0x8055acec +8055ad70 0x8055ad70 +8055add4 0x8055add4 +8055ade8 0x8055ade8 +8055adfc AnmChrHolder::GetFrame +8055ae10 AnmChrHolder::SetFrame +8055ae24 AnmChrHolder::UpdateRateAndSetFrame +8055ae90 AnmChrHolder::GetFrameCount +8055af14 AnmChrHolder::IsLooped +8055af78 AnmChrHolder::GetUpdateRate +8055af8c AnmChrHolder::SetUpdateRate +8055afa0 0x8055afa0 +8055afd0 0x8055afd0 +8055aff0 0x8055aff0 +8055b05c ModelDirector::SetScale +8055b0b0 0x8055b0b0 +8055b128 0x8055b128 +8055b194 0x8055b194 +8055b1e8 0x8055b1e8 +8055b1f8 0x8055b1f8 +8055b258 0x8055b258 +8055b298 ModelDirector::__ct(ClipInfo) +8055b50c ModelDirector::__ct +8055b558 ModelDirector::__ct(ScnMgrIdx) +8055b5bc ModelDirector::__dt +8055b5fc ModelDirector::GetBRRES +8055b618 ModelDirector::BindBRRES +8055b6a4 ModelDirector::BindBRRESFallback +8055b754 ModelDirector::RipAndBindBRRES +8055b7f8 ModelDirector::BindBRRESImpl +8055ba00 ModelDirector::BRRESExists +8055ba44 ModelDirector::GetResMdl(string) +8055ba54 ModelDirector::GetResMdl(idx) +8055ba64 ModelDirector::MdlExists +8055ba9c ModelDirector::HasResMdl +8055baa0 ModelDirector::Init +8055bd00 ModelDirector::InitG3D +8055bed4 ModelDirector::AppendToScnMgr +8055bf10 ModelDirector::CreateScn +8055c0e8 ModelDirector::LoadWithAnm(name) +8055c1d0 ModelDirector::LoadWithAnm(idx) +8055c2b8 ModelDirector::LoadNoAnm(name) +8055c318 ModelDirector::LoadNoAnm(idx) +8055c378 ModelDirector::LoadWithOption(name) +8055c44c ModelDirector::LoadWithOption(idx) +8055c520 ModelDirector::LoadScnMdl1Mat1Shp +8055c580 MiiHeadsModel::InitModel +8055c7f4 0x8055c7f4 +8055c8c4 0x8055c8c4 +8055c93c 0x8055c93c +8055c9f0 ModelDirector::ToggleVisible +8055cb08 ModelDirector::EnableDraw +8055cbd0 0x8055cbd0 +8055cc0c ModelDirector::UpdateVisibility +8055cce0 ModelDirector::EnableScreen +8055ccf8 ModelDirector::DisableScreen +8055cd10 ModelDirector::IsVisibleOnScreen +8055cd30 ModelDirector::EnableScreen2 +8055cd48 ModelDirector::IsVisibleOnScreen2 +8055cd68 ModelDirector::SetScnMdlOptions +8055ce84 0x8055ce84 +8055cf08 0x8055cf08 +8055cf90 0x8055cf90 +8055d03c 0x8055d03c +8055d0f4 0x8055d0f4 +8055d23c ModelDirector::Update +8055d2f4 0x8055d2f4 +8055d448 ModelDirector::SetAllScnOptionsFromClipInfo +8055d554 ModelDirector::SetDisableDrawScnOptionsFromClipInfo +8055d6bc 0x8055d6bc +8055d82c 0x8055d82c +8055d920 0x8055d920 +8055d9f0 ModelDirector::CreateTransformator +8055daf0 0x8055daf0 +8055dc00 0x8055dc00 +8055dce4 ModelDirector::SetAudioActor +8055ddec ModelDirector::LinkAnimation +8055df80 0x8055df80 +8055e0f4 0x8055e0f4 +8055e274 ModelDirector::LinkEmptyAnm +8055e388 0x8055e388 +8055eae0 ModelDirector::LinkAllAnimations +8055f1e4 ModelDirector::PrepareAllAnms +8055f27c 0x8055f27c +8055f28c 0x8055f28c +8055f2ac 0x8055f2ac +8055f2b4 ModelDirector::SetLight +8055f2c4 ModelDirector::GetScnMgr +8055f2fc ModelDirector::RemoveFromGroup +8055f34c ModelDirector::ToggleTransparent +8055f3e8 ModelDirector::ReplaceMatGXTexObj +8055f41c 0x8055f41c +8055f4c4 ModelDirector::UpdateDrawPriority +8055f508 0x8055f508 +8055f5e4 0x8055f5e4 +8055f6a8 0x8055f6a8 +8055f754 0x8055f754 +8055f7d8 ModelDirector::ChangeTransformator +8055f848 ModelDirector::GetScale +8055f894 ModelDirector::SetScale(float) +8055f8d0 ModelCalcBase::__ct +8055f8dc 0x8055f8dc +8055f9bc 0x8055f9bc +8055fa90 ModelCalcCallback::GetBoneWorldMtx +8055fb64 0x8055fb64 +8055fc44 0x8055fc44 +8055fd18 0x8055fd18 +8055fdec 0x8055fdec +8055fe34 ModelCalcCallback::__ct +8055fee8 0x8055fee8 +8055ff28 ModelCalcCallback::ExecCallback_CALC_WORLD +8055ff4c ModelCalcCBBoneLinked::__ct +80560010 ModelCalcCallback::__dt +80560050 ModelCalcCBBoneLinked::ExecWorldCBImpl +8056022c 0x8056022c +805602f4 ModelCalcCBBoneLinked::__dt +80560334 ModelCalcCallback::ExecCallback_CALC_XLU +80560338 ModelCalcCallback::ExecCallback_CALC_OPA +8056033c ModelCalcCallback::ExecCallback_CALC_VIEW +80560340 ModelCalcCallback::ExecCallback_CALC_MAT +80560344 0x80560344 +80560348 0x80560348 +80560388 ModelCalcCallback::__dt(thunk) +80560390 ModelCalcCBBoneLinked::__dt(thunk) +80560398 ModelCalcCallback::ExecCallback_CALC_WORLD(thunk) +805603a0 0x805603a0 +805603a8 Light::__ct +8056058c 0x8056058c +80560648 Light::Init +805607fc 0x805607fc +805609a4 0x805609a4 +80560aa0 0x80560aa0 +80560afc Light::InitLightObject +80560c34 0x80560c34 +80560c90 0x80560c90 +80560d20 Light::Update +80561000 0x80561000 +80561024 LightMgrHolder::__ct +805610e8 LightMgrHolder::805610e8 +8056120c LightMgrHolder::8056120c +805612e8 LightMgrHolder::LoadBLIGHT +805612ec LightMgrHolder::805612ec +8056138c 0x8056138c +80561398 LightMgrHolder::AcquireLight +80561404 LightMgrHolder::moveFromAtoB +80561450 LightMgrHolder::vf_0x10 +8056156c LightMgrHolder::Update +805615f0 LightMgrHolder::LoadBLMAP +80561688 LightMgrHolderRace::__ct +80561948 LightMgrHolder::__dt +80561988 LightMgrHolderRace::LoadBLIGHT +80561a88 LightMgrHolder::LoadBLMAP +80561b18 LightMgrHolderMenu_construct +80561be8 LightMgrHolderMenu_loadBlight +80561c40 LightMgrHolderMenu_loadLightMap +80561c9c LightMgrHolderMiiBody_construct +80561d6c LightMgrHolderMiiBody_loadBlight +80561dd4 LightMgrHolderMiiBody_loadLightMap +80561e40 LightMgrHolderMiiBody_destruct +80561e80 LightMgrHolderMenu_destruct +80561ec0 LightMgrHolderRace::destruct +80561f00 Light::__dt +80561f40 ScnMgr::__ct +80562064 0x80562064 +805620bc ScnMgr::__dt +80562144 ScnMgr::ResetRenderModeObj +80562180 ScnMgr::InitImpl +80562350 ScnMgr::LoadPostEffects +80562354 ScnMgr::vf_0x28 +80562358 ScnMgr::CreateAllGroups +80562444 ScnMgr::CreateScnGroup +80562470 ScnMgr::LoadLights +805624cc ScnMgr::DrawLights +8056251c 0x8056251c +80562520 ScnMgr::DoneDraw +805625a8 ScnMgr::DrawModelsImpl +80562888 ScnMgr::UpdateScnRoot +80562b34 ScnMgr::UpdateCameras +80562bf0 GameCamera::GetViewMatrix +80562bf8 ScnMgr::RemoveShadowTextureMgr +80562c0c 0x80562c0c +80562c8c 0x80562c8c +80562cbc ScnMgr::InsertModelDirectorInScn +80562e90 ScnMgr::SetModelDrawPriority +80562f60 ScnMgr::SetModelXluDrawPriority +80562f80 0x80562f80 +80563068 0x80563068 +805630ec 0x805630ec +805631ac 0x805631ac +8056326c ScnMgr::SetAnmScn +80563410 ScnMgr::SetRootAnmScn +805635b4 ScnMgr::AppendScnGroupExHolder +805635c4 ScnMgr::RemoveModelDirector +805635cc ScnMgr::AppendScreenSpecificModelDirector +805635d4 ScnMgr::AppendMatModelDirector +805635dc ScnMgr::RemoveMatModelDirector +805635e4 ScnMgr::SwitchScnRoot +80563674 ScnMgr::UpdateVisibility +805636dc 0x805636dc +805637f4 0x805637f4 +805638fc ScnMgr::CreateScnGroupEx +80563a78 ScnMgr::RearrangeDirectorGroups +80563b9c ScnMgr::80563b9c +80563be8 ScnMgr::80563be8 +80563c38 ScnMgr::80563c38 +80563c84 ScnGroupExHolder::__ct +80563ce4 TextureHolder::__ct +80563db0 TextureHolder::CopyFromEFB +80563ee8 TextureHolder::80563ee8 +80564074 ScnMgr::UpdateModelDrawPriority +8056410c ScnMgr::RemoveGroup +80564140 ScnMgr::InsertGroup +8056417c ScnMgr::GetScnGroup +805641d8 TextureHolder::__dt +80564218 ScnMgr::vf_0x2c +8056421c ScnMgr::DrawModels +80564224 ScnMgr::CalcSelf +80564228 ScnMgr::vf_0xC +8056422c ScnMgr::Init +80564238 ScnGroupHolder::__dt +80564278 ScnGroupExHolder::__dt +805642b8 Renderer::__ct +8056430c Renderer::__dt +80564378 RendererWithModels::__ct +805643cc RendererWithModels::__dt +80564444 RendererWithModels::Calc +80564458 RendererWithModels::Draw +805644a8 RendererWithModels::DrawWithProjection +805644ec 0x805644ec +80564618 RendererMixed::__ct +8056466c RendererMixed::Draw +80564688 RendererMii::__ct +805646e4 RendererMixed::__dt +80564760 RendererMii::DrawModels +805647bc RendererMii::__dt +8056483c ScnMgr::InitScn +805649a0 0x805649a0 +805649e8 0x805649e8 +80564a14 0x80564a14 +80564adc ScnMgr::CalcAll +80564b7c ScnMgr::Calc(idx) +80564c44 0x80564c44 +80564c64 ScnMgr::Draw(idx) +80564d60 ScnMgr::DrawMain +80564e38 ScnMgr::DrawSecondary +80564fa4 0x80564fa4 +80564fbc Renderer::Remove(internal idx) +80564ff4 ScnMgr::Unregister +80565030 ScnMgr::Reset +805650b0 ScnMgr::Finalize +80565114 Renderer::Register(listIdx) +80565164 Renderer::RegisterAndAppendScreen +80565268 Renderer::Register2ndList(NoAppend) +805652ac Renderer::Remove(listIdx) +805652c4 Renderer::RemoveFrom1stList +805652d4 Renderer::RemoveFrom2ndList +805652e8 Renderer::Register(bool Append) +8056539c ScnMgr::Register +805653d0 0x805653d0 +8056543c 0x8056543c +80565494 0x80565494 +805654ec 0x805654ec +80565630 ScnMgr::AcquireLight +80565640 0x80565640 +80565654 0x80565654 +80565664 0x80565664 +80565674 0x80565674 +805656c4 0x805656c4 +805656c8 0x805656c8 +8056572c 0x8056572c +805657a4 0x805657a4 +8056581c GameScreen::__ct +805658f0 0x805658f0 +80565ac8 0x80565ac8 +80565b08 0x80565b08 +80565b48 0x80565b48 +80565b50 0x80565b50 +80565b88 0x80565b88 +80565bc8 0x80565bc8 +80565cb8 0x80565cb8 +80565d14 0x80565d14 +80565da0 GameScreen::SetAndLoadOrthoProj +80565e88 0x80565e88 +80565f70 0x80565f70 +80566020 GameScreen::SetAndLoadProjection +805660d0 0x805660d0 +80566180 0x80566180 +805661e8 GameScreen::SetProjection +80566278 0x80566278 +80566330 0x80566330 +805663b8 0x805663b8 +8056645c 0x8056645c +805664e4 0x805664e4 +80566584 0x80566584 +80566670 0x80566670 +805666b0 0x805666b0 +805666f0 0x805666f0 +80566824 0x80566824 +80566958 0x80566958 +80566bb8 GameScreen::Set16_9AspectRatio +80566c20 GameScreen::Set4_3AspectRatio +80566ca0 GameScreen::Init +80566dbc 0x80566dbc +80566e38 GameScreen::CalcDimensions +80566f6c GameScreen::GetAbsoluteToView +80566fc0 GameScreen::GetViewToScreen +80566fc4 GameScreen::GetAbsoluteToScreen +80567044 0x80567044 +805670b4 0x805670b4 +80567130 GameScreen::__dt +80567170 RendererGlobe::__ct +805671ac RendererMenu::__dt +8056720c 0x8056720c +80567268 RendererGlobe::__dt +805672cc Kart::Damage::__ct +8056735c 0x8056735c +8056739c Kart::Damage::Reset +805673b0 Kart::Damage::Update +805674f4 Kart::Damage::SetAffectedByPOW +805674fc Kart::Damage::SetPOWParams +80567524 Kart::Damage::SetPOWParams(static) +8056754c Kart::Damage::ShouldReceivePOWDamage +80567578 Kart::Damage::ResetPOWParams +80567594 Kart::Damage::UpdatePOWParams +805675dc Kart::Damage::SetDamage +80567a54 Kart::Damage::UpdateCurDamage +80567a88 Kart::Damage::EndCurDamage +80567b98 0x80567b98 +80567c68 0x80567c68 +80567ce4 Kart::Damage::UpdateCurDamageSpeed +80567d3c Kart::Damage::ApplySpinDamage +80567f68 Kart::Damage::ApplyBananaDamage +80567fb4 Kart::Damage::ApplyGoombaDamage +80568000 Kart::Damage::ApplyFireDamage +8056804c Kart::Damage::ApplyShockDamage +80568058 Kart::Damage::ApplyZapperDamage +80568064 Kart::Damage::ApplyPOWDamage +80568198 0x80568198 +80568204 Kart::Damage::UpdateSpinOutdamage +8056837c Kart::Damage::OnSpinOutDamageEnd +805683b8 Kart::Damage::UpdatePOWDamage +80568558 Kart::Damage::vf_0x10 +80568620 Kart::Damage::OnPOWDamageEnd +8056865c Kart::Damage::ApplyShellFIBDamage +805686dc Kart::Damage::OnShellFIBDamageEnd +80568718 Kart::Damage::ApplyStarDamage +80568794 Kart::Damage::ApplyKnockback +80568aa8 Kart::Damage::UpdateKnockbackDamage +80568c40 0x80568c40 +80568c7c Kart::Damage::OnStarCarDamageEnd +80568cb8 Kart::Damage::ApplyChompDamage +80568d34 Kart::Damage::UpdateBigKnockbackDamage +80568f68 Kart::Damage::OnBigKnockbackDamageEnd +80568fa4 Kart::Damage::ApplyCarDamage +80569024 Kart::Damage::ApplyBulletDamage +805690a0 Kart::Damage::ApplyLaunchDamage +805692b4 Kart::Damage::UpdateLaunchDamage +805696c8 Kart::Damage::OnLaunchDamageEnd +805696cc 0x805696cc +80569774 Kart::Damage::ApplyThwompDamage +80569818 Kart::Damage::ApplyMegaDamage +805698bc Kart::Damage::ApplySquishRespawnDamage +80569978 Kart::Damage::ApplyCarSquishDamage +80569a1c Kart::Damage::UpdateSquishDamage +80569ad0 Kart::Damage::OnSquishDamageEnd +80569ae8 Kart::Damage::ApplyDrivingDirKnockback +80569b94 Kart::Damage::ApplySidewaysKnockback +80569d4c 0x80569d4c +80569da4 0x80569da4 +80569dfc 0x80569dfc +80569e9c 0x80569e9c +80569f2c 0x80569f2c +80569fb0 0x80569fb0 +8056a04c 0x8056a04c +8056a164 Kart::Damage::vf_0x14 +8056a168 0x8056a168 +8056a1a8 Kart::Damage::__dt +8056a1e8 0x8056a1e8 +8056a228 KartDamageCpp_sinit_ +8056a264 Kart::ModelsVisibility::__ct +8056a2b0 0x8056a2b0 +8056a2f0 Kart::ModelsVisibility::SetInvisible +8056a300 Kart::ModelsVisibility::SetModelsVisibility +8056a470 Kart::ModelsVisibility::UpdateModelsVisibility +8056a580 Kart::ModelsVisibility::SetModelDraw +8056a59c Kart::ModelsVisibility::__dt +8056a5dc Kart::MatTEVHandler::SetPartTEVSwapMode +8056a5ec Kart::MatTEVHandler::__ct +8056a630 Kart::MatTEVHandler::SetTexSwapModeAndSetTEVSwapModes +8056a6d8 Kart::MatTEVHandler::SetTEVSwapModes +8056a780 Kart::MatTEVHandler::SetModelTEVSwapMode(bool) +8056a89c Kart::MatTEVHandler::SetModelTevColorOp +8056a998 Kart::MatTEVHandler::SetTEVSwapModesNoTexModeUpdate +8056aa28 Kart::MatTEVHandler::SetModelTEVSwapMode +8056ab2c Kart::MatTEVHandler::__dt +8056ab6c Kart::ScaleAnmController::LoadAnimations +8056ac9c Kart::ScaleAnmController::GetAnmScale +8056acf4 Kart::ScaleAnmController::GetPressUpAnmScale +8056ad44 Kart::ScaleAnmController::__ct +8056ae90 0x8056ae90 +8056aed0 0x8056aed0 +8056af10 Kart::ScaleAnmController::Reset +8056afb4 Kart::ScaleAnmController::RequestScaleChange +8056b060 0x8056b060 +8056b094 0x8056b094 +8056b0cc 0x8056b0cc +8056b168 Kart::ScaleAnmController::RequestEndScale +8056b218 Kart::ScaleAnmController::Update +8056b45c 0x8056b45c +8056b5a8 Kart::ScaleAnmController::__dt +8056b5e8 KartScaleAnmController_cpp_sinit_ +8056b630 Kart::ScaleAnmController::__ct +8056b63c Kart::StarAnmMgr::LoadAnm +8056b8a8 Kart::StarAnmMgr::__ct +8056b8ec Kart::StarAnmMgr::Activate +8056b910 Kart::StarAnmMgr::Update +8056bbac Kart::StarAnmMgr::StartEnding +8056bbc0 Kart::StarAnmMgr::End +8056bbcc Kart::StarAnmMgr::Reset +8056bd08 0x8056bd08 +8056be20 Kart::StarAnmMgr::SetModelColorsImpl +8056bf20 0x8056bf20 +8056c030 Kart::StarAnmMgr::SetModelColors +8056c034 Kart::StarAnmMgr::__dt +8056c074 0x8056c074 +8056c0cc Kart::Part::__dt +8056c10c Kart::Arm::LoadModel +8056c124 Kart::Arm::UpdateMatrix +8056c394 Kart::Body::__ct +8056c3fc Kart::Body::LoadModel +8056c410 Kart::Body::CreateModelDirectors +8056c458 Kart::Body::Init +8056c4b4 Kart::Body::Reset +8056c500 Kart::Body::vf_0x58 +8056c544 0x8056c544 +8056c588 0x8056c588 +8056c604 Kart::Body::GetMatrix +8056c664 Kart::Body::UpdateMatrix +8056c700 0x8056c700 +8056c7b8 0x8056c7b8 +8056c874 Kart::Body::vf_0x60 +8056c8f0 0x8056c8f0 +8056c950 0x8056c950 +8056c964 0x8056c964 +8056c9c4 0x8056c9c4 +8056c9e8 Kart::Body::Load2ndModel +8056ca48 0x8056ca48 +8056cb5c 0x8056cb5c +8056cc74 0x8056cc74 +8056ccc0 Kart::BodyKart::__ct +8056cd84 Kart::Body::__dt +8056cdc4 Kart::BodyKart::SetDispParams +8056ce04 Kart::BodyKart::Load +8056ce98 Kart::BodyKart::UpdateMatrix +8056d118 Kart::BodyKart::vf_0xc +8056d1ac Kart::BodyKart::vf_0x64 +8056d230 Kart::BodyKart::vf_0x24 +8056d280 Kart::BodyKart::ApplyStarColors +8056d2f8 Kart::BodyKart::vf_0x2c +8056d370 Kart::BodyKart::vf_0x30 +8056d3e8 Kart::BodyKart::UpdateModelDrawPriority +8056d470 0x8056d470 +8056d4b0 Kart::BikeHandle::LoadModel +8056d4c0 Kart::BikeHandle::UpdateMatrix +8056d640 0x8056d640 +8056d680 Kart::BikeSwingArm::LoadModel +8056d690 Kart::BikeSwingArm::UpdateMatrix +8056d858 Kart::BodyBike::__ct +8056d8f0 Kart::BikeHandle::__dt +8056d930 Kart::BikeSwingArm::__dt +8056d970 Kart::BodyBike::SetDispParams +8056d984 Kart::BodyBike::LoadModel +8056d998 Kart::BodyBike::Load +8056da0c Kart::BodyBike::vf_0x60 +8056da88 Kart::BodyBike::UpdateMatrix +8056dd54 Kart::Body::GetMatrix +8056de94 Kart::BodyBike::UpdateWheelMatrix +8056dfc0 Kart::BodyBike::vf_0xC +8056e034 Kart::BodyBike::vf_0x64 +8056e094 Kart::BodyBike::vf_0x24 +8056e0b4 Kart::BodyBike::ApplyStarColors +8056e108 Kart::BodyBike::vf_0x2c +8056e15c Kart::BodyBike::vf_0x30 +8056e1b0 Kart::BodyBike::UpdateModelDrawPriority +8056e218 Kart::BodyQuacker::__ct +8056e2bc Kart::BodyBike::__dt +8056e2fc Kart::BodyQuacker::GetMatrix +8056e35c Kart::BodyQuacker::UpdateMatrix +8056e41c Kart::Body::SetHandleDistance +8056e424 Kart::Body::SetFloat_0x98 +8056e42c Kart::BodyQuacker::__dt +8056e46c Kart::Part::vf_0x24 +8056e494 Kart::BodyKart::__dt +8056e4d4 Kart::Arm::__dt +8056e514 0x8056e514 +8056e51c 0x8056e51c +8056e524 0x8056e524 +8056e52c 0x8056e52c +8056e534 0x8056e534 +8056e53c 0x8056e53c +8056e544 0x8056e544 +8056e54c 0x8056e54c +8056e554 0x8056e554 +8056e55c 0x8056e55c +8056e564 0x8056e564 +8056e56c Kart::Collision::__ct +8056e5e4 0x8056e5e4 +8056e624 Kart::Collision::Reset +8056e70c 0x8056e70c +8056e764 Kart::Collision::ProcessVehicleBodyCollision +8056e8d4 Kart::Collision::ProcessWheelCollision +8056e930 Kart::Collision::ProcessMovingRoad +8056ea04 Kart::Collision::ProcessFloor +8056ee24 0x8056ee24 +8056eef4 0x8056eef4 +8056f184 0x8056f184 +8056f26c 0x8056f26c +8056f490 0x8056f490 +8056f510 Kart::Collision::ProcessTriggers +8056f73c 0x8056f73c +8056f7f0 Kart::Collision::CheckKartCollision +80570d20 Kart::Movement::vf_0x30 +80570d24 0x80570d24 +80570da8 0x80570da8 +80571234 0x80571234 +805713b8 0x805713b8 +805713d8 0x805713d8 +805713fc 0x805713fc +80571484 0x80571484 +80571570 0x80571570 +805715f4 0x805715f4 +80571634 Kart::Collision::CheckTriggersAndOOB +805718d4 0x805718d4 +80571c5c 0x80571c5c +80571d04 0x80571d04 +80571d98 0x80571d98 +80571f10 Kart::Collision::CheckObjectCollision +80572544 0x80572544 +80572574 Object::GetId +8057257c Kart::Collision::CheckItemCollision +80572b94 0x80572b94 +80572c20 Kart::Collision::UpdateCollisions +80572f4c 0x80572f4c +805730d4 Kart::Collision::InitHitboxes +80573170 Kart::Collision::HandleNoopCollision +80573178 Kart::Collision::HandleBananaCollision +80573190 Kart::Collision::HandleShellCollision +805731b0 Kart::Collision::HandleBlueShellCollision +805731c8 Kart::Collision::HandleBombCollision +805731e0 Kart::Collision::HandleMushroomCollision +80573224 Kart::Collision::HandleStarCollision +8057325c Kart::Collision::HandleFibCollision +805733c4 0x805733c4 +805733cc 0x805733cc +805733d4 0x805733d4 +805733dc 0x805733dc +805733e4 0x805733e4 +805733ec 0x805733ec +805733f4 0x805733f4 +80573464 0x80573464 +80573518 ObjectCollidable::GetCollision +80573520 0x80573520 +8057353c 0x8057353c +805735ac 0x805735ac +805735b4 0x805735b4 +805735bc 0x805735bc +805735d4 0x805735d4 +805735dc 0x805735dc +805735e4 0x805735e4 +805735ec 0x805735ec +8057363c 0x8057363c +805736c8 0x805736c8 +80573754 0x80573754 +80573790 0x80573790 +805737b8 0x805737b8 +80573824 0x80573824 +80573a2c 0x80573a2c +80573b00 Kart::Collision::ActivateOOB +80573ec4 0x80573ec4 +80573ed4 Kart::Collision::UpdateRespawn +80573ff0 Kart::Collision::__dt +80574030 0x80574030 +80574068 0x80574068 +80574108 0x80574108 +80574114 Kart::Zipper::__ct +80574170 Kart::Zipper::__dt +805741b0 0x805741b0 +805741c8 0x805741c8 +80574210 0x80574210 +805742f4 0x805742f4 +80574340 Kart::Zipper::Update +80574c7c Vec3::RotateQuaternionConjugate +80574c90 0x80574c90 +80574e60 0x80574e60 +805750cc 0x805750cc +805752e8 0x805752e8 +805758a4 Kart::Zipper::End +805758e4 0x805758e4 +80575a44 Kart::Trick::__ct +80575aa8 Kart::Trick::__dt +80575ae8 Kart::Trick::Reset +80575b38 Kart::Trick::UpdateNext +80575c6c 0x80575c6c +80575d50 0x80575d50 +80575d64 0x80575d64 +80575d7c Kart::Trick::TryStart +80575ee8 Kart::Trick::Start +80576018 0x80576018 +80576078 0x80576078 +8057613c 0x8057613c +8057616c Kart::Trick::StartInner +80576230 0x80576230 +805763e4 Kart::Trick::Update +80576460 Kart::Trick::SetupProperties +805764fc Kart::Trick::UpdateRot +80576618 0x80576618 +80576684 0x80576684 +805766b8 Kart::Trick::End +80576744 0x80576744 +80576758 Kart::TrickBike::Start +8057689c Kart::TrickBike::StartInner +80576994 Kart::TrickBike::UpdateRot +80576afc Kart::TrickBike::__ct +80576b3c Kart::BRRESHandle::__ct +80576c54 Kart::BRRESHandle::__dt +80576c94 0x80576c94 +80576d10 0x80576d10 +80576d8c Kart::BRRESHandle::LoadKartBRRES +80576e90 Kart::BRRESHandle::LoadDriverBRRES +80576f2c 0x80576f2c +80576fe8 0x80576fe8 +80577064 Kart::BRRESHandle::LoadBodyModel +80577220 0x80577220 +805772d8 0x805772d8 +805772f0 Kart::BRRESHandle::LoadLeftBackWheelModel +805773a8 Kart::BRRESHandle::LoadLeftFrontWheelModel +80577460 Kart::BRRESHandle::LoadRightBackWheelModel +80577518 Kart::BRRESHandle::LoadRightFrontWheelModel +805775d0 0x805775d0 +805776d4 Kart::BRRESHandle::LoadArmModel +805778ac Kart::BRRESHandle::LoadDriverModels +8057791c Kart::BRRESHandle::LoadBikeHandleModel +805779e4 Kart::BRRESHandle::LoadBikeSwingArmModel +80577a9c Kart::BRRESHandle::LoadBikeFrontWheelModel +80577b54 Kart::BRRESHandle::LoadBikeBackWheelModel +80577c0c Kart::BRRESHandle::LoadBikeSuspensionsModel +80577cc4 0x80577cc4 +80577d7c Kart::BRRESHandle::LoadShadowModel +80577e00 Kart::BRRESHandle::LoadKillerModels +80577f00 0x80577f00 +80577fa4 0x80577fa4 +80577fc4 Kart::Movement::__ct +8057811c Kart::Boost::__dt +8057815c 0x8057815c +8057819c 0x8057819c +805781dc 0x805781dc +8057821c Kart::Movement::CreateTrickZipper +8057829c Kart::Movement::SetTurnParams +805784d4 Kart::Movement::Reset +805788dc Kart::Movement::Update +80579960 0x80579960 +80579968 Kart::Movement::UpdateHopPhysics +805799ac Kart::Movement::Respawn +80579a50 0x80579a50 +80579ce4 0x80579ce4 +80579da8 0x80579da8 +80579f20 0x80579f20 +80579f6c 0x80579f6c +80579fe4 Kart::Movement::SomethingWheelieBump +8057a13c 0x8057a13c +8057a140 Kart::Movement::UpdateDir +8057a8b4 Kart::Movement::UpdateTurn +8057ab68 Kart::Movement::UpdateVehicleSpeed +8057b028 0x8057b028 +8057b108 Kart::Movement::ComputeWallCollisionSpeedFactor +8057b29c Kart::Movement::vf_0x68 +8057b2a0 0x8057b2a0 +8057b77c 0x8057b77c +8057b868 Kart::Movement::GetAccelerationFromSpeed +8057b99c Kart::Movement::ResetBulletSpeed +8057b9ac Kart::Movement::ResetMaxSpeed +8057b9bc Kart::Movement::UpdateAcceleration +8057c3c8 Kart::Movement::GetWheelieSoftSpeedLimitBonus +8057c3d4 Kart::Movement::UpdateOffroad +8057c5e8 0x8057c5e8 +8057c69c Kart::Movement::UpdateRotate +8057cdd4 0x8057cdd4 +8057cf0c Kart::Movement::UpdateVehicleRotationVector +8057d1d4 Kart::Movement::UpdateSSMTRotation +8057d398 Kart::Movement::UpdateUps +8057d888 Kart::Movement::UpdateUpsWhileAirbone +8057da18 Kart::Movement::CheckIfCanHop +8057da5c Kart::Movement::Hop +8057dc40 Kart::Movement::CancelWheelie +8057dc44 Kart::Movement::UpdateManualDrift +8057e0dc Kart::Movement::UpdateAutoDrift +8057e348 0x8057e348 +8057e3f4 Kart::Movement::StartManualDrift +8057e638 0x8057e638 +8057e6f4 0x8057e6f4 +8057e77c 0x8057e77c +8057e804 Kart::Movement::UpdateHopAndSlipdrift +8057e99c 0x8057e99c +8057ea50 0x8057ea50 +8057ea94 0x8057ea94 +8057eab8 0x8057eab8 +8057ec28 0x8057ec28 +8057ed20 0x8057ed20 +8057ed34 0x8057ed34 +8057ed8c 0x8057ed8c +8057ee50 Kart::Movement::UpdateMTCharge +8057efe0 Kart::Movement::GetMTMaxCharge +8057efec Kart::Movement::GetSMTMaxCharge +8057eff8 0x8057eff8 +8057f024 0x8057f024 +8057f05c 0x8057f05c +8057f090 0x8057f090 +8057f1d0 0x8057f1d0 +8057f3d8 Kart::Movement::ActivateMushroom +8057f630 0x8057f630 +8057f7a8 Kart::Movement::EndTrick +8057f96c Kart::Movement::ActivateZipperBoost +8057fb6c 0x8057fb6c +8057fd18 Kart::Movement::TryStartJumpPad +805800bc 0x805800bc +80580268 Kart::Movement::ActivateStar +805803f0 0x805803f0 +80580438 Kart::Movement::ApplyShock +8058051c 0x8058051c +805805ec 0x805805ec +80580648 0x80580648 +805806a4 0x805806a4 +80580768 0x80580768 +80580778 Kart::Movement::ApplyShockEffect +80580998 0x80580998 +80580a84 0x80580a84 +80580b14 Kart::Movement::ActivateMega +80580cdc 0x80580cdc +80580dc0 0x80580dc0 +80580f28 0x80580f28 +80580f9c 0x80580f9c +80581034 0x80581034 +805810a8 0x805810a8 +805813a4 0x805813a4 +805813e8 0x805813e8 +8058160c Kart::Movement::UpdateScale +80581720 Kart::Movement::SetScale +8058173c 0x8058173c +80581778 0x80581778 +80581824 Kart::Movement::CalcBlink +8058196c 0x8058196c +805819a8 0x805819a8 +80581a24 0x80581a24 +80581a28 Kart::Movement::ActivateTC +80581a40 Kart::Movement::DeactivateTC +80581a58 Kart::Movement::ApplyInk +80581b1c Kart::Movement::UpdateCharacterInk +80581c70 0x80581c70 +80581c90 Kart::Movement::SomethingRespawnBoost +8058212c Kart::Movement::ApplyStartBoost +80582278 0x80582278 +805823a4 0x805823a4 +80582488 0x80582488 +805824c8 0x805824c8 +805824f0 0x805824f0 +80582530 Kart::Movement::TryEndJumpPad +80582694 Kart::Movement::UpdateBoost +80582804 0x80582804 +805828cc 0x805828cc +80582d94 0x80582d94 +80582db4 0x80582db4 +80582dd8 0x80582dd8 +80582e34 0x80582e34 +80582f38 0x80582f38 +80582f58 0x80582f58 +80582f7c 0x80582f7c +80582f9c Kart::Movement::ReleaseMT +8058327c 0x8058327c +805832c4 0x805832c4 +805832e4 Kart::Movement::CancelBoost +80583424 0x80583424 +8058348c Kart::Movement::vf_0x24 +80583658 Kart::Movement::OnOOB +805837cc Kart::Movement::vf_0x2c +80583b84 0x80583b84 +80583b88 Kart::Movement::UpdateStickyRoad +80583f2c 0x80583f2c +80584044 Kart::Movement::SetInitialPhysicsValues +80584334 Kart::Movement::DoRespawn +80584648 0x80584648 +8058473c 0x8058473c +80584928 thunk_0x8058473c +8058492c thunk_0x8058473c +80584930 0x80584930 +8058498c Kart::Movement::EnterCannon +80584d58 Kart::Movement::UpdateCannon +805852c8 0x805852c8 +8058539c Kart::Movement::GetCannonPosAndDir +805855bc 0x805855bc +805858ac Kart::Movement::ActivateBullet +80585a04 0x80585a04 +80585a28 0x80585a28 +80585a60 0x80585a60 +80585aa0 0x80585aa0 +80585ae8 0x80585ae8 +80585af8 0x80585af8 +805860bc 0x805860bc +8058677c Kart::Movement::UpdateFromPseaVenice +805869dc Kart::Movement::UpdateDiving +80586d18 0x80586d18 +80586db4 0x80586db4 +80586e90 0x80586e90 +80586fa8 Kart::Movement::UpdateSlipStream +805874c0 0x805874c0 +8058758c Kart::Movement::UpdateWheelie +80587590 Kart::Movement::UpdateSpecialFloor +80587938 0x80587938 +805879a4 0x805879a4 +80587b30 Kart::MovementBike::__ct +80587b78 Kart::Movement::__dt +80587bb8 Kart::MovementBike::CreateTrickZipper +80587c54 Kart::MovementBike::SetTurnParams +80587d00 Kart::MovementBike::Reset +80587d64 Kart::MovementBike::UpdateWheelie +80587d68 Kart::MovementBike::UpdateVehicleRotationVector +80588324 Kart::MovementBike::GetWheelieSoftSpeedLimitBonus +80588350 Kart::MovementBike::StartWheelie +805883c4 Kart::MovementBike::CancelWheelie2 +805883f4 Kart::MovementBike::UpdateWheelie +80588798 Kart::MovementBike::TryStartWheelie +80588860 0x80588860 +80588888 Kart::MovementBike::UpdateMtCharge +80588950 Kart::MovementBike::vf_0x24 +80588b30 Kart::MovementBike::CancelWheelie +80588b58 Kart::MovementBike::OnOoB +80588cac Kart::MovementBike::vf_0x2c +80588d28 Kart::Boost::__ct +80588d74 Kart::Boost::Reset +80588db0 Kart::Boost::Activate +80588e18 Kart::Boost::CancelAll +80588e24 Kart::Boost::Update +80588fc0 Kart::MovementBike::vf_0x30 +80588fd0 Kart::MovementBike::vf_0x68 +80588fe0 Kart::MovementBike::CheckWheelie +80589024 Kart::Movement::GetStarDuration +80589030 0x80589030 +8058905c 0x8058905c +805890b0 0x805890b0 +80589118 0x80589118 +805891c0 0x805891c0 +8058920c 0x8058920c +80589230 0x80589230 +80589254 0x80589254 +8058926c 0x8058926c +80589308 0x80589308 +805893fc 0x805893fc +80589400 Kart::MovementRealLocal::Hop +80589440 Kart::MovementBikeRealLocal::Hop +80589480 Kart::MovementBikeRealLocal::StartWheelie +80589514 Kart::MovementBikeRealLocal::CancelWheelie2 +8058956c Kart::MovementRemote::CanHop +805895cc Kart::MovementBikeRemote::CanHop +8058962c Kart::MovementBikeRemote::TryStartWheelie +805896b8 Kart::Movement::vf_0x58 +805896bc Kart::MovementBike::GetLeanRot +805896c4 Kart::MovementBikeRemote::__dt +80589704 Kart::MovementBike::__dt +80589744 Kart::Movement::CheckWheelie +8058974c Kart::Movement::GetLeanRot +80589758 Kart::MovementRemote::__dt +80589798 Kart::MovementBikeRealLocal::__dt +805897d8 Kart::MovementRealLocal::__dt +80589818 0x80589818 +80589830 0x80589830 +80589844 0x80589844 +8058985c RacedataHandler_consstruct +805898e4 0x805898e4 +80589924 0x80589924 +80589964 0x80589964 +8058998c 0x8058998c +80589f64 0x80589f64 +8058a774 0x8058a774 +8058a7b4 0x8058a7b4 +8058a7f4 0x8058a7f4 +8058a834 0x8058a834 +8058a874 0x8058a874 +8058a8b4 0x8058a8b4 +8058a8f4 0x8058a8f4 +8058a934 0x8058a934 +8058a974 0x8058a974 +8058a9b4 0x8058a9b4 +8058a9f4 0x8058a9f4 +8058aa34 0x8058aa34 +8058aa74 0x8058aa74 +8058aab4 0x8058aab4 +8058aaf4 0x8058aaf4 +8058ab34 0x8058ab34 +8058ab74 0x8058ab74 +8058abb4 0x8058abb4 +8058ac08 0x8058ac08 +8058ac5c 0x8058ac5c +8058acb0 0x8058acb0 +8058ad04 0x8058ad04 +8058ad58 0x8058ad58 +8058add8 0x8058add8 +8058ade0 0x8058ade0 +8058ae48 0x8058ae48 +8058ae50 0x8058ae50 +8058aeb8 0x8058aeb8 +8058aec0 0x8058aec0 +8058af28 0x8058af28 +8058af30 0x8058af30 +8058af98 0x8058af98 +8058afa0 0x8058afa0 +8058b008 0x8058b008 +8058b010 0x8058b010 +8058b078 0x8058b078 +8058b080 0x8058b080 +8058b0e8 0x8058b0e8 +8058b0f0 0x8058b0f0 +8058b10c 0x8058b10c +8058b128 0x8058b128 +8058b144 0x8058b144 +8058b164 0x8058b164 +8058b198 RacedataHandler_processInputs +8058b494 0x8058b494 +8058b8a8 0x8058b8a8 +8058b9f8 0x8058b9f8 +8058bde0 Audio::KartActor::Reset +8058be20 0x8058be20 +8058c088 0x8058c088 +8058c08c 0x8058c08c +8058c0dc 0x8058c0dc +8058c3d8 0x8058c3d8 +8058c490 0x8058c490 +8058c4c0 0x8058c4c0 +8058c5e4 0x8058c5e4 +8058c614 0x8058c614 +8058c61c 0x8058c61c +8058c64c 0x8058c64c +8058c654 0x8058c654 +8058c684 0x8058c684 +8058c68c 0x8058c68c +8058c6bc 0x8058c6bc +8058c6c4 0x8058c6c4 +8058c6f4 0x8058c6f4 +8058c6fc 0x8058c6fc +8058c760 0x8058c760 +8058c7c4 0x8058c7c4 +8058c828 0x8058c828 +8058c88c 0x8058c88c +8058c8f0 0x8058c8f0 +8058c954 0x8058c954 +8058c9b8 0x8058c9b8 +8058ca28 RacedataFactory_construct +8058ca74 0x8058ca74 +8058cab4 0x8058cab4 +8058cab8 0x8058cab8 +8058cabc RacedataFactory_sendRacedataRecord +8058cb30 RacedataFactory_packRacedataRecord +8058d3cc RacedataFactoryFlags_construct +8058d414 RacedataFactoryFlags_destroy +8058d454 0x8058d454 +8058d464 0x8058d464 +8058d47c 0x8058d47c +8058d4a0 0x8058d4a0 +8058d4dc 0x8058d4dc +8058d51c 0x8058d51c +8058d520 0x8058d520 +8058d570 0x8058d570 +8058d644 0x8058d644 +8058d7d8 0x8058d7d8 +8058d8a8 0x8058d8a8 +8058d8d8 0x8058d8d8 +8058d908 0x8058d908 +8058d93c 0x8058d93c +8058d960 0x8058d960 +8058d984 0x8058d984 +8058d9a8 0x8058d9a8 +8058da2c 0x8058da2c +8058da50 0x8058da50 +8058daa4 0x8058daa4 +8058dae0 0x8058dae0 +8058db30 0x8058db30 +8058db54 0x8058db54 +8058dbdc 0x8058dbdc +8058dc0c 0x8058dc0c +8058dc4c 0x8058dc4c +8058dc58 0x8058dc58 +8058dc9c 0x8058dc9c +8058dce0 0x8058dce0 +8058dd24 0x8058dd24 +8058dd60 0x8058dd60 +8058ddbc Kart::Player::__ct +8058deb0 0x8058deb0 +8058def0 Kart::Player::__dt +8058df60 0x8058df60 +8058df7c 0x8058df7c +8058e014 0x8058e014 +8058e188 Kart::Player::InitSub +8058e22c Kart::Player::Init +8058e5f8 Kart::Player::CreateBody +8058e648 0x8058e648 +8058e724 0x8058e724 +8058e7c4 0x8058e7c4 +8058e804 Kart::Player::PrepateTiresAndSUspensions +8058e990 0x8058e990 +8058ea0c Kart::Player::InitWheels +8058ec08 Kart::Wheel::__dt +8058ec48 0x8058ec48 +8058ece0 Kart::Player::CreateSound +8058ed28 0x8058ed28 +8058ed68 0x8058ed68 +8058ee48 Kart::Player::SetStartPosition +8058eeb4 Kart::Player::UpdateKartSub +8058eebc Kart::Player::Update2 +8058f04c 0x8058f04c +8058f1f4 Kart::Wheel::vf_0x24 +8058f20c 0x8058f20c +8058f260 Kart::PlayerBike::CreateBody +8058f2e8 Kart::PlayerBike::InitWheels +8058f4ac Kart::WheelFront::__dt +8058f4ec Kart::WheelBikeFront::__dt +8058f52c Kart::Suspensions::__dt +8058f56c Kart::PlayerBike::CreateSound +8058f5b4 Kart::Manager::SetupPlayer +8058f820 Kart::Player::CreateModel +8058f8b0 Kart::PlayerBike::__dt +8058f924 0x8058f924 +8058f990 0x8058f990 +8058f9d0 0x8058f9d0 +8058fa10 Kart::Manager::Disposer__dt +8058faa8 Kart::Manager::CreateInstance +8058faf8 Kart::Manager::DestroyInstance +8058fb2c Kart::Manager::__ct +8058fdd4 Kart::Manager::__dt +8058fee0 Kart::Manager::Init +8058ffe8 Kart::Manager::Update +80590100 Kart::Manager::GetKartPlayer +80590110 0x80590110 +80590128 Kart::Link::InitList +80590138 Kart::Link::LinkKartList +8059018c Kart::Link::__ct +805901d0 Kart::Link::SetPointersFromPlayer +8059020c Kart::Link::GetKartPosition +80590224 Kart::Player::GetPreviousPosition +80590238 Kart::Link::SetKartPosition +80590264 Kart::Link::GetMtx +80590278 Kart::Link::GetKartBodyMtx +80590288 Kart::Link::SetKartRotation +805902dc Kart::Link::GetWheelCount0 +805902ec Kart::Link::GetWheelCount1 +805902fc Kart::Link::GetWheelCountReciprocal +8059030c Kart::Link::GetWheelCountPlus1Reciprocal +8059031c Kart::Link::GetWheelPosition +80590338 Kart::Link::WheelHasFloorCollision +80590350 Kart::Link::SetBody0x94 +80590368 Kart::Link::SetBodyHandleDistance +80590380 Kart::Link::SetStartBoost +80590390 Kart::Link::GetWheelSpeed +805903ac Kart::Link::GetPhysicsHolder +805903bc Kart::Link::GetPhysicsHolder(const) +805903cc Kart::Link::GetPhysics +805903e0 Kart::Link::GetPhysics(const) +805903f4 Kart::Link::GetControllerHolder +8059041c Kart::Link::ActivateRumble(3 args) +80590478 Kart::Link::ActivateRumble(1 arg) +80590570 Kart::Link::ActivateRumble(no arg) +80590650 Kart::Link::IsLocal +80590664 Kart::Link::IsCPU +80590678 Kart::Link::IsGhost +8059068c Kart::Link::IsMii +8059069c Kart::Link::GetBody +805906a8 Kart::Link::GetBody(const) +805906b4 Kart::Link::GetSuspensions +805906c8 Kart::Link::GetSuspensions(const) +805906dc Kart::Link::GetWheels +805906f0 Kart::Link::GetWheels(const) +80590704 Kart::Link::GetWheelPhysicsHolder +8059071c Kart::Link::GetWheelPhysicsHolder(const) +80590734 Kart::Link::GetWheelPhysics +8059074c Kart::Link::GetWheelPhysics(const) +80590764 Kart::Link::GetSub +80590770 Kart::Link::GetSub(const) +8059077c Kart::Link::GetMovement +80590788 Kart::Link::GetMovement(const) +80590794 Kart::Link::GetKartActor +805907a0 Kart::Link::GetCharacterActor +805907b0 Kart::Link::PlayCharacterSound +805907c0 Kart::Link::GetObject +805907cc Kart::Link::GetShadow +805907d8 Kart::Link::GetPhysicsHolderHitboxGroup +805907ec Kart::Link::GetPhysicsHolderHitboxGroup(const) +80590800 Kart::Link::GetWheelPhysicsHitboxGroup +8059081c Kart::Link::GetVehicleBodyCollisionData +80590834 Kart::Link::GetWheelCollisionData +8059084c Kart::Link::GetCollision +80590858 Kart::Link::GetCollision(const) +80590864 Kart::Link::GetStatsAndBsp +80590874 Kart::Link::GetStats +80590888 Kart::Link::GetBsp +8059089c Kart::Link::GetBspHitboxes +805908b4 Kart::Link::GetBspWheel +805908d4 Kart::Link::GetValues0x18 +805908e4 Kart::Link::GetKartDriverDispEntry +805908f4 Kart::Link::GetRotatingItemsParams +8059090c Kart::Link::GetKartPartsDispEntry +8059091c Kart::Link::GetBikePartsDispEntry +8059092c Kart::Link::GetExhaustParticleSet2 +80590958 Kart::Link::GetExhaustParticleSet +8059098c Kart::Link::GetBalloonsPosition +805909b8 Kart::Link::GetDriverDispEntry +805909c8 Kart::Link::GetCameraVerticalDistance +805909f4 Kart::Link::GetBaseHopStickX +80590a04 Kart::Link::GetHopStickX +80590a10 Kart::Link::GetType +80590a28 Kart::Link::GetStatsWheelCount +80590a40 Kart::Link::GetDriverController +80590a4c Kart::Link::GetTriggersCollisionRadius +80590a5c Kart::Link::GetPlayerIdx +80590a6c Kart::Link::IsBike +80590a7c Kart::Link::GetKartId +80590a8c Kart::Link::ToggleKartVisible +80590a9c Kart::Link::ToggleVisible +80590c44 Kart::Link::GetBodyMatCol0(copy) +80590c6c Kart::Link::GetBodyMatCol1(copy) +80590c94 Kart::Link::GetBodyMatCol2(copy) +80590cbc Kart::Link::GetBodyMatCol0 +80590cd0 Kart::Link::GetBodyMatCol1 +80590ce4 Kart::Link::GetBodyMatCol2 +80590cf8 Kart::Link::GetEngineSpeed +80590d08 Kart::Link::GetSpeed +80590d20 Kart::Link::GetDamage +80590d2c Kart::Link::GetDamage(const) +80590d38 Kart::Link::80590d38 +80590d5c Kart::Link::SetDamageType +80590d88 Kart::Link::SetAffectedByPOW +80590d94 Kart::Link::80590d94 +80590da4 Kart::Link::GetCurDamageType +80590db4 Kart::Link::CancelBoost +80590dc0 Kart::Link::GetSpeedRatioCapped +80590dd0 Kart::Link::GetSpeedRatio +80590de0 Kart::Link::HasCamera +80590df8 Kart::Link::SetCamera +80590e04 Kart::Link::GetScreenIdx +80590e28 Kart::Link::UpdateCameraOnRespawn +80590e40 Kart::Link::UpdateCameraOnBoost +80590e58 Kart::Link::ResetCameraFov +80590e70 Kart::Link::UpdateCameraOnExplosion +80590e88 Kart::Link::80590e88 +80590e8c Kart::Link::80590e8c +80590ea4 Kart::Link::80590ea4 +80590ed8 Kart::Link::StopCloseCamera +80590ef8 Kart::Link::SetCamera +80590f18 Kart::Link::StopCameraMovAndRotation +80590f38 Kart::Link::80590f38 +80590f58 Kart::Link::80590f58 +80590f70 Kart::Link::UpdateCameraOnOOBHit +80590f90 Kart::Link::ActivateStarAnm +80590f9c Kart::Link::80590f9c +8059102c Kart::Link::UpdateStarAnm +80591038 Kart::Link::StopStarAnm +80591044 Kart::Link::80591044 +80591050 Kart::Link::80591050 +80591070 Kart::Link::GetStarAnm +8059107c Kart::Link::GetStarColor +8059108c Kart::Link::GetModelsVisibility +80591098 Kart::Link::GetModelsVisibility +805910a4 Kart::Link::GetMatTEVHandler +805910b0 Kart::Link::GetBaseSpeed +805910c0 Kart::Link::GetAIController +805910cc Kart::Link::GetBodyClosestFloorFlags +805910e4 Kart::Link::GetBodyClosestFloorSettings +805910fc Kart::Link::IsWheelHittingWall +80591138 Kart::Link::GetClosestFloorKCLFlags +80591170 Kart::Link::GetClosestFloorKCLSettingss +805911a8 Kart::Link::GetBodyKCLIntensity +805911c0 Kart::Link::805911c0 +805911d8 Kart::Link::GetDriftState +805911e8 Kart::Link::GetMTCharge +805911f8 Kart::Link::GetSMTCharge +80591208 Kart::Link::GetMTMaxCharge +80591214 Kart::Link::GetSMTMaxCharge +80591220 Kart::Link::TranslateBody +80591264 Kart::Link::80591264 +80591314 Kart::Link::IsRespawning +80591344 Kart::Link::80591344 +80591378 Kart::Link::80591378 +805913a8 Kart::Link::805913a8 +805913fc Kart::Link::IsWaitingForBackwards +80591418 Kart::Link::GetBackwardsAllowCounter +80591428 Kart::Link::Get1stLight +80591434 Kart::Link::Get2ndLight +80591440 Kart::Link::GetRacedataFactory +8059144c Kart::Link::GetRacedataHandler +80591458 Kart::Link::GetRacedataFactoryFlags +80591468 Kart::Link::80591468 +80591478 Kart::Link::SetScale +805914bc Kart::Link::GetScale +805914cc Kart::Link::IsWheelieing +805914e4 Kart::Link::GetStartBoostCharge +805914f4 Kart::Link::805914f4 +80591520 Kart::Link::GetEntity +8059152c Kart::Link::GetClipInfo +80591538 Kart::Link::CallOnAllPartsFunc +80591608 Kart::Link::AreKartModelsVisible +80591618 Kart::Link::GetKiller +80591624 Kart::Link::GetBillViewMtx +80591664 Kart::Link::ResetInertia +8059170c Kart::Link::8059170c +80591784 Kart::Link::StartOobWipe +805917a0 Kart::Link::GetOobWipeState +805917b0 Kart::Link::805917b0 +805917c0 Kart::Link::805917c0 +805917f4 Kart::Link::805917f4 +80591828 Kart::Link::80591828 +8059184c Kart::Link::8059184c +80591898 Kart::Link::80591898 +805918bc Kart::Link::IncrementItemsHitCount +805918e0 Kart::Link::IncrementItemsCollisionCount +80591904 Kart::Link::GetZipper +80591914 Kart::Link::GetTrick +80591924 Kart::Link::InitLights +8059197c Kart::Link::SetLightObjIdx +80591998 0x80591998 +805919e8 0x805919e8 +805919f4 Kart::InitParamBinsFromCommon +80591b74 Kart::InitParamBinsFromRace +80591c9c Kart::ResetParamBins +80591ce8 Kart::GetParamBinFromCommon +80591d00 Kart::GetParamBinFromRace +80591d4c Kart::GetParamBin +80591dbc Kart::GetBSP +80591dd0 Kart::LoadKartAndDriverParamBinsFromCommon +80591e58 Kart::LoadKartParamBinFromCommon +80591ea8 Kart::LoadDriverParamBinFromCommon +80591ef8 Kart::LoadStatsParamFileFromCommon +80591f4c Kart::GetKartParamEntry +80591f60 Kart::GetDriverParamEntry +80591fa4 Kart::ComputeStats +8059240c Kart::LoadKartDriverDisp +80592498 Kart::GetKartDriverDispEntry +805924cc Kart::LoadKartPartsDisp +80592558 Kart::GetKartPartsDispEntry +80592594 Kart::LoadBikePartsDisp +80592620 Kart::GetBikePartsDispEntry +80592660 Kart::SetMiiDispParam +80592678 Kart::LoadMiiDisp +805926c8 Kart::LoadMiiDispFromMiiBody +80592718 Kart::LoadCameraParam +8059275c Kart::LoadDriverDisp +805927ac Kart::GetMiiDispEntry +805927c0 Kart::GetDriverDispEntry +805927d4 Kart::Link::GetCameraParamBin +80592848 Kart::Part::__ct +805928dc 0x805928dc +80592920 Kart::Part::CreateModelDirectors +80592a28 Kart::Part::LoadModel +80592a30 Kart::Part::Create2ndModelDirector +80592b54 Kart::Part::Load +80592b64 Kart::Part::UpdateMatrix +80592bf8 0x80592bf8 +80592cbc Kart::Part::vf_0xC +80592cdc Kart::Part::SetModelScale +80592d04 Kart::Part::SetModelMtx +80592d88 Kart::Part::ToggleModelVisible +80592db0 Kart::Part::vf_0x20 +80592de4 0x80592de4 +80592df8 Kart::Part::ApplyStarColors +80592e5c Kart::Part::vf_0x2c +80592ee0 Kart::Part::vf_0x30 +80592f4c Kart::Part::UpdateModelDrawPriority +80592fa8 0x80592fa8 +80592fc0 Kart::Values::__ct +8059308c 0x8059308c +805930e4 0x805930e4 +80593124 0x80593124 +80593128 0x80593128 +80593138 0x80593138 +80593310 0x80593310 +8059345c 0x8059345c +8059377c 0x8059377c +805937d4 0x805937d4 +80593814 0x80593814 +80593c20 0x80593c20 +80593c78 0x80593c78 +80593cb8 0x80593cb8 +80593d0c 0x80593d0c +80593d1c 0x80593d1c +80593d54 0x80593d54 +80593dbc 0x80593dbc +80593e08 0x80593e08 +80593e18 0x80593e18 +80593fa4 0x80593fa4 +80594094 0x80594094 +805940d4 0x805940d4 +80594134 0x80594134 +805941bc 0x805941bc +805943b4 Kart::Status::__ct +8059455c Kart::Status::Reset +80594594 Kart::Status::Init +80594634 0x80594634 +80594644 0x80594644 +80594704 0x80594704 +8059474c Kart::Status::UpdateCollisionsAndMore +8059487c Kart::Status::UpdateFromInput +80594bd4 Kart::Status::UpdateCollisions +805958ec 0x805958ec +805958f0 0x805958f0 +80595918 Kart::Status::UpdateStartBoostCharge +805959d4 Kart::Status::ComputeStartBoost +80595af8 Kart::Status::ApplyStartBoost +80595c5c 0x80595c5c +80595ca4 0x80595ca4 +80595cb4 0x80595cb4 +80595cc4 Kart::Status::__dt +80595d04 0x80595d04 +80595d48 Kart::Sub::Init +80595f78 Kart::Sub::InitParts +805960fc 0x805960fc +8059617c Kart::Sub::ResetPhysics +805963e4 0x805963e4 +80596454 Kart::Sub::SetPointers +80596480 Kart::Sub::Update +80596cfc Kart::Player::ApplyPhysics +8059783c Kart::Sub::UpdateTransforms +80597934 0x80597934 +805979ec 0x805979ec +80597a40 0x80597a40 +80597a88 Kart::Sub::UpdateMovingRoad +80597d4c Kart::Player::ApplyCollisionForce +805980d8 0x805980d8 +8059828c Kart::Sub::InitEntity +80598338 0x80598338 +805983dc 0x805983dc +805983f4 0x805983f4 +8059840c 0x8059840c +80598630 0x80598630 +80598744 0x80598744 +80598890 0x80598890 +80598ac8 Kart::Sub::__dt +80598b08 Kart::Suspensions::__ct +80598b50 Kart::Suspensions::LoadModel +80598b60 Kart::Suspensions::Init +80598bd4 Kart::Suspensions::InitPhysicsHolder +80598bdc Kart::Suspensions::Reset +80598be4 0x80598be4 +80598bec Kart::Suspensions::UpdateMatrix +80598fe8 Kart::SuspensionsBike::LoadModel +80598ff8 Kart::SuspensionsBike::UpdateMatrix +80599124 0x80599124 +80599134 0x80599134 +8059938c 0x8059938c +805993cc Kart::SuspensionsBike::__dt +8059940c WheelPhysics::__ct +80599470 WheelPhysics::InitHitboxGroup +805994d4 0x805994d4 +80599508 WheelPhysics::Reset +805995c0 0x805995c0 +80599690 WheelPhysics::UpdateCollision +80599ad0 WheelPhysics::Realign +80599d9c 0x80599d9c +80599dc0 0x80599dc0 +80599eac WheelPhysics::HasFloorCollision +80599ebc AnimatedModelExplodable_GetWorldMatrix +80599ec8 WheelPhysics::GetCollisionData +80599ed4 WheelPhysicsHolder::__ct +80599f54 WheelPhysicsHolder::Reset +80599fa0 0x80599fa0 +8059a02c 0x8059a02c +8059a278 WheelPhysicsHolder::Update +8059a4f8 Vec3::MultMtxByVec +8059a574 WheelPhysicsHolder::ApplySuspensions +8059a9c4 WheelPhysics::__dt +8059aa04 0x8059aa04 +8059aa44 Kart::Wheel::__ct +8059aab0 Kart::Wheel::Init +8059ab14 Kart::Wheel::CreatePhysics +8059ab68 0x8059ab68 +8059ab70 Kart::Wheel::LoadModel +8059abf4 Kart::WheelFront::LoadModel +8059ac1c Kart::WheelFront::CreatePhysics +8059ac70 Kart::Wheel::UpdateMatrix +8059ae3c 0x8059ae3c +8059aecc Kart::WheelFront::UpdateMatrix +8059b038 Kart::WheelBikeFront::CreatePhysics +8059b08c Kart::WheelBikeFront::LoadModel +8059b09c Kart::WheelBikeFront::UpdateMatrix +8059b1fc Kart::WheelBikeRear::CreatePhysics +8059b250 Kart::WheelBikeRear::LoadModel +8059b260 Kart::WheelBikeRear::UpdateMatrix +8059b3b8 0x8059b3b8 +8059b524 0x8059b524 +8059b564 Kart::WheelBikeRear::__dt +8059b5a4 0x8059b5a4 +8059b600 Kart::Killer::ModelCalc::ExecWorldCBImpl +8059b658 Kart::Killer::__ct +8059b7b8 Kart::Killer::Activate +8059bc44 Kart::Killer::Update +8059be8c Kart::Killer::Init +8059c0b8 Kart::Killer::ToggleVisible +8059c118 cancelBullet +8059c29c 0x8059c29c +8059c44c 0x8059c44c +8059c798 0x8059c798 +8059c868 0x8059c868 +8059c888 0x8059c888 +8059c954 0x8059c954 +8059cab4 0x8059cab4 +8059cb54 0x8059cb54 +8059cc60 Kart::Killer::UpdateDuration +8059ce24 0x8059ce24 +8059cf04 0x8059cf04 +8059cfc4 0x8059cfc4 +8059d084 0x8059d084 +8059d0c0 Kart::Killer::UpdateUps +8059d2b8 0x8059d2b8 +8059d2d4 Kart::Killer::UpdateSettings +8059d44c Kart::Killer::UpdateGravity +8059d59c 0x8059d59c +8059d658 0x8059d658 +8059d8e0 0x8059d8e0 +8059d964 0x8059d964 +8059d97c 0x8059d97c +8059dae4 0x8059dae4 +8059dcdc 0x8059dcdc +8059de9c Kart::Killer::__dt +8059dedc Kart::Killer::ModelCalc::__dt +8059df1c Kart::Killer::ModelCalc::__dt(thunk) +8059df24 MenuModelMgr::Disposer__dt +8059dfbc MenuModelMgr::CreateInstance +8059e04c MenuModelMgr::DestroyInstance +8059e080 MenuModelMgr::__ct +8059e0e4 MenuModelMgr::__dt +8059e1fc MenuModelMgr::Start +8059e250 MenuModelMgr::Init +8059e4a8 MenuModelMgr::Update +8059e4fc MenuModelMgr::Draw +8059e550 MenuModelMgr::ResetKartModels +8059e568 MenuModelMgr::RequestDriverModel +8059e580 MenuModelMgr::RequestKartModel +8059e598 MenuModelMgr::RequestBackModel +8059e5a4 MenuModelMgr::TogglePlayerModel +8059e5bc 0x8059e5bc +8059e5d4 MenuModelMgr::ToggleBackModel +8059e5dc MenuModelMgr::GetTexObj +8059e5f0 0x8059e5f0 +8059e618 0x8059e618 +8059e634 MenuModelMgr::GetDriverTexture +8059e644 MenuModelMgr::GetKartTexture +8059e69c 0x8059e69c +8059e6ac 0x8059e6ac +8059e76c ScnMgrMenu::__ct +8059e7c8 ScnMgrMenu::__dt +8059e820 ScnMgrMenu::UpdateCameras +8059e8b4 0x8059e8b4 +8059e8c4 ScnMgrMenu::LoadLights +8059e920 ScnMgrMenu::CreateScnGroup +8059e978 ScnMgrMenu::SetAnmScn +8059e98c ScnMgrMii::__ct +8059e9e4 ScnMgrMii::__dt +8059ea3c ScnMgrMii::LoadLights +8059eaa4 0x8059eaa4 +8059eae0 0x8059eae0 +8059eae4 0x8059eae4 +8059eb94 0x8059eb94 +8059ebf8 0x8059ebf8 +8059ec50 0x8059ec50 +8059eca8 RendererMenu::__ct +8059ed30 RendererMenu::Calc +8059ed68 RendererMenu::DrawModels +8059f4f0 MenuModel::vf_0x18 +8059f508 RendererMenu::Draw +8059f5bc Kart::PhysicsHolder::Init +8059f678 Kart::Physics::__dt +8059f6b8 0x8059f6b8 +8059f6f8 Kart::PhysicsHolder::__dt +8059f788 0x8059f788 +8059f7c8 Kart::PhysicsHolder::Reset +8059f968 Kart::PhysicsHolder::Update +8059fc08 0x8059fc08 +8059fc30 0x8059fc30 +8059fc48 0x8059fc48 +8059fd0c 0x8059fd0c +8059fdd0 0x8059fdd0 +8059fe94 0x8059fe94 +8059ff58 0x8059ff58 +805a0050 0x805a0050 +805a00d0 0x805a00d0 +805a014c 0x805a014c +805a01cc 0x805a01cc +805a02b8 0x805a02b8 +805a0340 Kart::PhysicsHolder::UpdateMat +805a03c4 0x805a03c4 +805a0410 Kart::PhysicsHolder::ResetQuaternions +805a0480 0x805a0480 +805a04a0 Kart::PhysicsHolder::__ct +805a0550 0x805a0550 +805a05a0 AutoCameraMoverRace::__dt +805a05e0 CamMove::__dt +805a0620 0x805a0620 +805a0660 AutoCameraMover::__dt +805a06a0 0x805a06a0 +805a06e0 0x805a06e0 +805a0868 0x805a0868 +805a08ac 0x805a08ac +805a08ec 0x805a08ec +805a08f0 0x805a08f0 +805a0a1c 0x805a0a1c +805a0abc 0x805a0abc +805a0b70 0x805a0b70 +805a0c14 0x805a0c14 +805a0c88 0x805a0c88 +805a0e00 0x805a0e00 +805a0ecc 0x805a0ecc +805a0fa0 0x805a0fa0 +805a11b8 0x805a11b8 +805a131c 0x805a131c +805a13c0 0x805a13c0 +805a1424 0x805a1424 +805a1478 AutoCameraMoverRace::vf_0x14 +805a1480 AutoCameraMoverRace::vf_0x18 +805a1488 0x805a1488 +805a14c8 0x805a14c8 +805a1508 0x805a1508 +805a153c 0x805a153c +805a1560 0x805a1560 +805a15e0 0x805a15e0 +805a1650 0x805a1650 +805a1750 0x805a1750 +805a17e0 0x805a17e0 +805a1820 0x805a1820 +805a1860 0x805a1860 +805a1864 CameraLink::__ct +805a18bc GameCamera::__ct +805a19d8 CameraLink::__dt +805a1a18 GameCamera::Update +805a1a34 CameraMgr::__ct +805a1a44 CameraLink::InitCameraList +805a1a64 CameraMgr::SetInstance +805a1a7c CameraLink::Register +805a1a8c 0x805a1a8c +805a1af0 CameraLink::UpdateAllCameras +805a1b78 CameraMgr::Update +805a1b7c CameraMgr::__dt +805a1bbc GameCamera::__dt +805a1bfc GameCamera::vf_0xC +805a1c00 GameCamera::GetPosition +805a1c1c GameCamera::GetViewMatrix(const) +805a1c24 GameCamera::Update(thunk) +805a1c2c GameCamera::vf_0xC(thunk) +805a1c34 GameCamera::__dt(thunk) +805a1c3c GameCamValues::Reset +805a1cf4 RaceCamera::CopyVec(2vecs) +805a1d10 RaceCamera::_ct +805a1fac 0x805a1fac +805a1fb0 Vec3::__ct(no args) +805a1fb4 0x805a1fb4 +805a1ff4 0x805a1ff4 +805a2034 RaceCamera::Init +805a21c8 RaceCamera::GetCamParams +805a21d0 RaceCamera::Update +805a2b08 0x805a2b08 +805a2b20 RaceCamera::CopyVec(1vec) +805a2b3c RaceCamera::CheckBitfield334 +805a2b54 0x805a2b54 +805a2b5c RaceCamera::NegateVec +805a2b84 0x805a2b84 +805a2c34 0x805a2c34 +805a2c94 RaceCamera::SubtractVecs +805a2cc8 RaceCamera::AddVecs(3vecs) +805a2cfc 0x805a2cfc +805a2fe0 RaceCamera::LinearTransfo(2vecs) +805a3020 RaceCamera::DotVec +805a3050 RaceCamera::CheckKartStatusField1 +805a3070 0x805a3070 +805a3314 RaceCamera::CheckKartStatusField0 +805a3334 RaceCamera::IsAutomatic +805a3348 RaceCamera::ClampFloat +805a337c RaceCamera::AcosDeg +805a3468 0x805a3468 +805a34b0 RaceCamera::UpdateCamValues +805a3e2c RaceCamera::CrossVec +805a3e78 RaceCamera::NormalizeAndGetNorm +805a3e7c RaceCamera::CompareFloats +805a3e9c PSMTXMultVec +805a3ea0 RaceCamera::ScaleVec(2vecs) +805a3ec8 0x805a3ec8 +805a3ed0 0x805a3ed0 +805a3ed8 0x805a3ed8 +805a3ee0 0x805a3ee0 +805a3f90 0x805a3f90 +805a3fc4 RaceCamera::SubtractVecsPS +805a3fe8 RaceCamera::AddVecs(2vecs) +805a401c RaceCamera::ScaleVec(3vecs) +805a4044 0x805a4044 +805a404c 0x805a404c +805a408c 0x805a408c +805a4094 RaceCamera::GetVecMag +805a4098 RaceCamera::NormalizeVecPS +805a40d0 0x805a40d0 +805a463c 0x805a463c +805a47a4 0x805a47a4 +805a4930 0x805a4930 +805a49bc RaceCamera::OnRespawn +805a4be4 0x805a4be4 +805a4c08 0x805a4c08 +805a4cdc RaceCamera::FlushPrevValues +805a4d10 0x805a4d10 +805a4dbc RaceCamera::OnBoost +805a4ddc RaceCamera::ResetAddedFov +805a4dec RaceCamera::OnExplosion +805a4e20 0x805a4e20 +805a4e34 0x805a4e34 +805a5548 0x805a5548 +805a57cc 0x805a57cc +805a5d30 0x805a5d30 +805a5d70 RaceCamera::vf_0x50 +805a65c4 0x805a65c4 +805a65f8 0x805a65f8 +805a6628 0x805a6628 +805a66d8 0x805a66d8 +805a67bc 0x805a67bc +805a6880 0x805a6880 +805a68f8 0x805a68f8 +805a6ab8 0x805a6ab8 +805a6b98 0x805a6b98 +805a6c04 0x805a6c04 +805a6c48 0x805a6c48 +805a6c58 RaceCamera::GetViewMtx +805a6dcc 0x805a6dcc +805a6de4 0x805a6de4 +805a6e30 0x805a6e30 +805a6e40 BCPInfo::Load +805a6eb4 0x805a6eb4 +805a6f04 0x805a6f04 +805a6f44 OpeningPan::Load +805a70a0 OpeningPan::Init +805a72dc 0x805a72dc +805a7358 OpeningPan::Calc +805a7568 0x805a7568 +805a758c cPN_Feather_SetNodeWeight +805a759c 0x805a759c +805a75ac FormationSet_GetFormationSpec +805a75bc 0x805a75bc +805a75cc 0x805a75cc +805a76d0 0x805a76d0 +805a772c 0x805a772c +805a77f0 0x805a77f0 +805a7820 0x805a7820 +805a834c RaceCameraMgr::Disposer__dt +805a83e4 RaceCameraMgr::CreateInstance +805a8434 RaceCameraMgr::DestroyInstance +805a8468 RaceCameraMgr::__ct +805a8cdc RaceCameraMgr::__dt +805a8d94 0x805a8d94 +805a8f7c RaceCamera::__dt +805a8fbc RaceCameraMgr::HasEveryOpeningPanEnded +805a900c RaceCameraMgr::Update +805a906c RaceCameraMgr::ApplyShaking +805a92b4 0x805a92b4 +805a9430 0x805a9430 +805a94d8 0x805a94d8 +805a9530 0x805a9530 +805a96ac 0x805a96ac +805a9708 0x805a9708 +805a989c 0x805a989c +805a9904 0x805a9904 +805a9970 0x805a9970 +805a99b0 0x805a99b0 +805a99e4 0x805a99e4 +805a9a14 0x805a9a14 +805a9a40 0x805a9a40 +805a9b04 RaceCameraMgr::ChangeFocusedPlayer +805a9b64 0x805a9b64 +805a9ba4 OpeningPan::__dt +805a9be4 RaceCamera::__dt(thunk) +805a9bec RaceCamera::Update(thunk) +805a9bf4 RaceCamera::Init(thunk) +805a9bfc 0x805a9bfc +805a9c04 AutoCameraMover::__ct +805a9cb8 0x805a9cb8 +805a9d30 0x805a9d30 +805a9d40 0x805a9d40 +805a9d50 0x805a9d50 +805a9edc 0x805a9edc +805a9f54 0x805a9f54 +805aa138 0x805aa138 +805aa140 0x805aa140 +805aa22c 0x805aa22c +805aa710 0x805aa710 +805aa7e8 0x805aa7e8 +805aa898 0x805aa898 +805aa8e4 0x805aa8e4 +805aa940 0x805aa940 +805aa9fc 0x805aa9fc +805aabe4 0x805aabe4 +805aac64 0x805aac64 +805aad08 0x805aad08 +805aaf64 AutoCameraMoverRace::__ct +805ab574 AutoCameraMoverRace::ResetActions +805ab57c AutoCameraMoverRace::vf_0x10 +805abad0 0x805abad0 +805abc90 0x805abc90 +805abd94 0x805abd94 +805abe38 0x805abd94_switch +805abe3c 0x805abd94_caseD_0 +805abe4c 0x805abd94_caseD_1 +805abe5c 0x805abd94_caseD_2 +805abe6c 0x805abd94_caseD_3 +805abe7c 0x805abd94_caseD_4 +805abe8c 0x805abd94_caseD_6 +805abe9c 0x805abd94_caseD_5 +805abea8 0x805abd94_caseD_7 +805abecc AutoCameraMoverRace::OnAction0Start +805abed0 AutoCameraMoverRace::OnAction0Update +805abfb4 AutoCameraMoverRace::OnAction1Start +805abfe0 AutoCameraMoverRace::OnAction1Update +805ac118 AutoCameraMoverRace::OnAction2Start +805ac1f0 AutoCameraMoverRace::OnAction2Update +805ac328 AutoCameraMoverRace::OnRaceEndStart +805ac384 AutoCameraMoverRace::OnRaceEndUpdate +805ac4a4 AutoCameraMoverRace::OnAction4Start +805ac560 AutoCameraMoverRace::OnAction4Update +805ac66c AutoCameraMoverRace::OnAction5Start +805ac744 AutoCameraMoverRace::OnAction5Update +805ac8bc AutoCameraMoverRace::OnAction6Start +805aca2c AutoCameraMoverRace::OnAction6Update +805acb1c 0x805acb1c +805acc34 0x805acc34 +805ace1c 0x805ace1c +805ace8c 0x805ace8c +805acf5c 0x805acf5c +805ad18c 0x805ad18c +805ad1b0 0x805ad1b0 +805ad228 DemoCamera_ct +805ad420 0x805ad420 +805ad460 DemoCamera_init +805ad5b4 0x805ad5b4 +805ad5c8 0x805ad5c8 +805ad81c 0x805ad81c +805ad8ac 0x805ad8ac +805ad924 0x805ad924 +805ada28 0x805ada28 +805adae4 0x805adae4 +805adb40 0x805adb40 +805adcb0 0x805adcb0 +805add98 0x805add98 +805addd8 0x805addd8 +805ade20 0x805ade20 +805ade50 0x805ade50 +805ade98 CamMove::OnStart +805adee0 CamMove::Update +805adf10 CamMove::OnEnd +805adf58 0x805adf58 +805adff0 BFGHolder::__ct +805ae0f8 BFGHolder::Update +805ae35c 0x805ae35c +805ae3bc 0x805ae3bc +805ae440 FogManager::__ct +805ae53c FogManager::Update +805ae5f4 0x805ae5f4 +805ae710 0x805ae710 +805ae734 FogManager::__dt +805ae774 BFGHolder::__dt +805ae7b4 0x805ae7b4 +805ae8d0 0x805ae8d0 +805ae9ec VEC3_perpInPlane +805aead8 0x805aead8 +805aeb88 Vec3_projUnit +805aebd0 VEC3_rejUnit +805aec24 VEC3_projAndRej +805aec8c 0x805aec8c +805aeca8 0x805aeca8 +805aed14 0x805aed14 +805aee14 0x805aee14 +805aeec4 GetPath +805aef6c 0x805aef6c +805af020 0x805af020 +805af048 0x805af048 +805af0f0 0x805af0f0 +805af148 0x805af148 +805af1bc 0x805af1bc +805af228 0x805af228 +805af330 0x805af330 +805af370 ScnMgrCreator::Disposer__dt +805af408 ScnMgrCreator::CreateInstance +805af48c 0x805af48c +805af4cc ScnMgrCreator::DestroyInstance +805af500 ScnMgrCreator::__dt +805af5a8 ScnMgrCreator::CreateScnMgr +805af774 ScnMgrCreator::CreateRace +805af948 0x805af948 +805af9e8 ScnMgrCreator::CreateMenu +805afab0 ScnMgrCreator::CreateGlobe +805afb78 0x805afb78 +805afc48 0x805afc48 +805afc58 0x805afc58 +805afc98 0x805afc98 +805afd14 0x805afd14 +805afe20 0x805afe20 +805afe88 0x805afe88 +805b0000 0x805b0000 +805b00b4 0x805b00b4 +805b029c 0x805b029c +805b0494 0x805b0494 +805b052c 0x805b052c +805b0b28 0x805b0b28 +805b0e40 0x805b0e40 +805b12fc 0x805b12fc +805b1300 ScnMgrRace::__ct +805b1354 ScnMgrRace::__ct +805b13ac 0x805b13ac +805b144c ScnMgrRace::805b144c +805b1470 ScnMgrRace::SetModelXluDrawPriority +805b14e0 ScnMgrRace::LoadPostEffects +805b1a18 ScnMgrRace::LoadLights +805b1a74 ScnMgrRace::vf_0xC +805b1c78 ScnMgrRace::CalcSelf +805b1cd8 ScnMgrRace::Draw +805b1d5c 0x805b1d5c +805b1f2c 0x805b1f2c +805b2110 ScnMgrRace::UpdateCameras +805b2168 0x805b2168 +805b21e8 0x805b21e8 +805b2244 0x805b2244 +805b237c 0x805b237c +805b2550 0x805b2550 +805b2568 0x805b2568 +805b2654 0x805b2654 +805b2740 0x805b2740 +805b328c 0x805b328c +805b3330 0x805b3330 +805b3470 0x805b3470 +805b354c 0x805b354c +805b3670 0x805b3670 +805b36e4 0x805b36e4 +805b36e8 0x805b36e8 +805b3a58 0x805b3a58 +805b3a6c ScnProcShadows::Draw +805b3a78 ScnProcShadows2::Draw +805b3a84 0x805b3a84 +805b3a90 0x805b3a90 +805b3a9c ScnProcEffects::Draw +805b3ad4 ScnProcCourseFilterEffect::Draw +805b3af4 ScnProcEffectsSelfItem::Draw +805b3b00 ScnMgrRace::Init +805b3ea8 ScnProcEffectsSelfItem::__dt +805b3f00 ScnProcCourseFilterEffect::__dt +805b3f58 ScnProcEffects::__dt +805b3fb0 0x805b3fb0 +805b4008 0x805b4008 +805b4060 ScnProcShadows2::__dt +805b40b8 ScnProcShadows::__dt +805b4110 RendererRaceUIScreen::__ct +805b4154 RendererRaceUIScreen::__dt +805b41ac RendererRaceUIScreen::Draw +805b423c RendererRaceUIOrtho::__ct +805b4280 RendererRaceUIOrtho::__dt +805b42d8 RendererRaceUIOrtho::Draw +805b4314 RendererRaceUIOrtho::Calc +805b4318 RendererRaceUIScreen::Calc +805b431c RendererRaceModels::__ct +805b4368 RendererRaceModels::DrawModels +805b43bc RendererRaceModels::vf_0x1c +805b4488 RendererRaceModels::Draw +805b450c 0x805b450c +805b4684 RendererRaceModels::__dt +805b46dc 0x805b46dc +805b46e8 0x805b46e8 +805b48ac 0x805b48ac +805b4a30 0x805b4a30 +805b4a8c 0x805b4a8c +805b4a98 0x805b4a98 +805b4af8 Kart::Physics::__ct +805b4b54 Kart::Physics::Reset +805b4d24 Kart::Physics::ResetSpeed +805b4dc0 Kart::Physics::Init(Reset Thunk) +805b4dc4 Kart::Physics::Init +805b4e84 Kart::Physics::InitInertia +805b4f34 Kart::Physics::CalcInvInertia +805b4f44 0x805b4f44 +805b50f8 0x805b50f8 +805b5170 Kart::Physics::Update +805b5b64 0x805b5b64 +805b5b68 Kart::Physics::Stabilize +805b5ce8 0x805b5ce8 +805b5e40 0x805b5e40 +805b5f44 0x805b5f44 +805b5f78 0x805b5f78 +805b6150 Kart::Physics::ApplyWheelSuspension +805b629c 0x805b629c +805b6388 0x805b6388 +805b63bc 0x805b63bc +805b6438 0x805b6438 +805b6448 Kart::PhysicsBike::Stabilize +805b66e4 Kart::PhysicsBike::__dt +805b6724 Kart::Collision::UpdateCollisionsInner +805b6a9c 0x805b6a9c +805b6d48 Kart::Collision::ApplyBodyCollision +805b6f4c Kart::Collision::UpdateWheelCollision +805b725c 0x805b725c +805b72b8 0x805b72b8 +805b78d0 0x805b78d0 +805b7928 0x805b7928 +805b7f48 Hitbox::Set +805b7f84 Hitbox::Reset +805b7fbc Hitbox::Update +805b80a8 0x805b80a8 +805b8158 0x805b8158 +805b81d0 0x805b81d0 +805b81e4 0x805b81e4 +805b821c CollisionData::__ct +805b82bc HitboxGroup::__ct +805b8330 HitboxGroup::Reset +805b83d8 0x805b83d8 +805b8420 0x805b8420 +805b8480 0x805b8480 +805b84c0 HitboxGroup::__ct(hitboxes) +805b86a8 0x805b86a8 +805b875c HitboxGroup::__ct(radius,position) +805b883c HitboxGroup::UpdateBoundingRadius +805b8984 ShadowModelDirector::__ct(ClipInfo) +805b89f4 ShadowModelDirector::__ct +805b8a48 ShadowModelDirector::__dt +805b8aa0 ShadowModelDirector::SetScnOptions +805b8ba4 ShadowModelsMgr::Disposer__dt +805b8c3c ShadowModelsMgr::CreateInstance +805b8cfc ShadowModelsMgr::DestroyInstance +805b8d30 ShadowModelsMgr::__ct +805b8dc4 ShadowModelsMgr::__dt +805b8e60 ShadowModelsMgr::InsertModel +805b8e68 ShadowModelsMgr::InsertModel2 +805b8e70 ShadowModelsMgr::Draw +805b8f40 ShadowModelsMgr::Draw2 +805b9010 MatModelDirector::__ct +805b906c MatModelDirector::__ct(scnMgrIdx) +805b90c8 MatModelDirector::__ct(ClipInfo) +805b9124 MatModelDirector::__dt +805b918c MatModelDirector::AppendToScnMgr +805b91d0 MatModelDirector::SetMaterialNames +805b91e0 MatModelDirector::CopyMaterialsTevColors +805b92c8 MatModelDirector::ReplaceDefaultScnMdlTevColorDL +805b92f8 MatModelDirector::__dt(thunk) +805b9300 NdevArgsExtractor::__ct +805b9304 NdevArgsExtractor::__dt +805b9344 NdevArgsExtractor::ExtractAllArgs +805ba08c NdevArgsExtractor::ExtractSection +805ba184 NdevArgsExtractor::ExtractLicense +805ba274 NdevArgsExtractor::ExtractPad +805ba3d8 NdevArgsExtractor::ExtractGhostType +805ba4c8 NdevArgsExtractor::ExtractCourseId +805ba5b8 NdevArgsExtractor::ExtractGhostLicense +805ba6e0 NdevArgsExtractor::ExtractFriendIdx +805ba798 NdevArgsExtractor::ExtractW +805ba88c NdevArgsExtractor::ExtractD +805ba98c NdevArgsExtractor::ExtractInt +805baa60 NdevArgsExtractor::ExtractU64 +805bab40 Pages::ActionLess::__ct +805bab84 Pages::ActionLess::__dt +805babec Pages::ActionLess::OnInit +805bac40 Pages::ActionLess::OnActivate +805bac4c Pages::ActionLess::AfterControlUpdate +805bac84 Pages::ActionLess::RequestEnd +805bac90 Pages::AutoEndingTransparent::__ct +805bacf8 Pages::AutoEndingTransparent::__dt +805bad8c Pages::AutoEndingTransparent::OnInit +805bae80 Pages::AutoEndingTransparent::ResetText +805bae90 Pages::AutoEndingTransparent::SetMessageWindowText +805bae98 Pages::AutoEnding::__ct +805baf10 CtrlMenuPageTitleText::__dt +805baf68 Pages::AutoEnding::__dt +805bb024 Pages::AutoEnding::OnInit +805bb1c0 Pages::AutoEnding::ResetText +805bb210 Pages::AutoEnding::SetTitleText +805bb218 Pages::AutoEnding::SetMessageWindowText +805bb220 Page::vf_0x5c +805bb224 Page::OnSectionChange +805bb228 Page::OnResume +805bb22c Page::OnUpdate +805bb230 Page::BeforeControlUpdate +805bb234 Page::AfterExitAnimations +805bb238 Page::BeforeExitAnimations +805bb23c Page::AfterEntranceAnimations +805bb240 Page::BeforeEntranceAnimations +805bb244 Page::OnDeactivate +805bb248 Page::OnDispose +805bb24c Page::IsDVDEnabled +805bb254 Page::IsHomeMenuWorking +805bb25c Page::Getpage +805bb264 0x805bb264 +805bb26c Pages::AutoEnding::GetRuntimeTypeInfo +805bb278 Page::GetNextPage +805bb280 Pages::AutoEndingTransparent::GetRuntimeTypeInfo +805bb28c Pages::ActionLess::GetRuntimeTypeInfo +805bb298 ActionLessPage_cpp_sinit_ +805bb2b8 Pages::AwardFade::GetPlayersVolumeMgr +805bb2c4 Pages::AwardFade::__ct +805bb348 Pages::AwardFade::__dt +805bb3bc Pages::AwardFade::OnInit +805bb4dc Pages::AwardFade::OnActivate +805bb528 Pages::AwardFade::BeforeEntranceAnimations +805bb52c Pages::AwardFade::BeforeExitAnimations +805bb530 Pages::AwardFade::AfterControlUpdate +805bb678 Pages::AwardFade::HandleClick +805bb724 AwardDemoCongratulations::GetClassName +805bb734 AwardDemoCongratulations::__ct +805bb770 AwardDemoCongratulations::__dt +805bb7c8 AwardDemoCongratulations::Load +805bb8a0 AwardDemoCongratulations::InitSelf +805bb8d8 0x805bb8d8 +805bb910 0x805bb910 +805bb948 AwardDemoResultItem::GetClassName +805bb958 AwardDemoResultItem::__ct +805bb994 AwardDemoResultItem::__dt +805bb9ec AwardDemoResultItem::Load +805bbb98 AwardDemoResultItem::InitSelf +805bbbd0 AwardDemoResultItem::SetValues +805bc03c Pages::AwardResults::__ct +805bc108 Pages::AwardResults::__dt +805bc1b0 Pages::AwardResults::OnInit +805bc620 Pages::AwardResults::OnActivate +805bc640 Pages::AwardResults::BeforeEntranceAnimations +805bc660 Pages::AwardResults::AfterControlUpdate +805bc73c Pages::AwardResults::HandleClick +805bc8dc Pages::AwardResults::InitConfig +805bc9dc Pages::AwardResults::SetWinOrLose +805bcba0 0x805bcba0 +805bcc24 Pages::AwardResults::PrepareCongratulations +805bcdc4 Pages::AwardResults::PrepareResultItems +805bcf30 Pages::AwardResults::PrepareCup +805bcfbc Pages::AwardResults::PrepareCup_switch +805bcfc0 Pages::AwardResults::PrepareCup_caseD_0 +805bcfc8 Pages::AwardResults::PrepareCup_caseD_1 +805bcfd0 Pages::AwardResults::PrepareCup_caseD_2 +805bcfd8 Pages::AwardResults::PrepareCup_caseD_3 +805bcfe0 Pages::AwardResults::PrepareCup_caseD_4 +805bcfe8 Pages::AwardResults::PrepareCup_caseD_5 +805bcff0 Pages::AwardResults::PrepareCup_caseD_6 +805bcff8 Pages::AwardResults::PrepareCup_caseD_7 +805bd050 Pages::AwardResults::InitRank +805bd2cc Pages::AwardResults::GetRuntimeTypeInfo +805bd2d8 UIControl::805bd2d8 +805bd2dc UIControl::OnPlayAnimSound +805bd2e0 UIControl::OnUpdate +805bd2e4 AwardDemoResultItem::GetRuntimeTypeInfo +805bd2f0 AwardDemoCongratulations::GetRuntimeTypeInfo +805bd2fc 0x805bd2fc +805bd304 Pages::AwardFade::GetRuntimeTypeInfo +805bd310 TriggerPtmf +805bd340 TriggerPtmf +805bd370 AwardPages_cpp_sinit_ +805bd39c PushButton::GetClassName +805bd3a8 PushButton::__ct +805bd4b0 PushButton::__dt +805bd518 PushButton::Load +805bd704 ManipulatorManager::GetRuntimeTypeInfo +805bd710 PushButton::OnLoad +805bd714 0x805bd714 +805bd720 PushButton::LoadWithAnims +805bdac8 PushButton::SetOnClickHandler +805bdae0 PushButton::SetOnSelectHandler +805bdae8 PushButton::SetOnDeSelectHandler +805bdaf0 PushButton::SelectInitial +805bdb44 PushButton::Select +805bdb58 PushButton::SelectFocus +805bdbd4 PushButton::SetPlayerBitfield +805bdbe0 PushButton::Init +805bdd98 PushButton::Update +805bdf44 PushButton::IsSelected +805bdf88 PushButton::GetAnimationFrameSize +805bdffc PushButton::HandleSelect +805be12c PushButton::OnSelect +805be130 PushButton::HandleDeselect +805be354 PushButton::OnDeselect +805be358 PushButton::HandleClick +805be42c PushButton::OnClick +805be430 PushButton::SetSelectionSoundId +805be438 PushButtonScaleFade::GetClassName +805be448 PushButtonScaleFade::__ct +805be55c PushButtonScaleFade::__dt +805be5c8 PushButtonScaleFade::SetPositionAnim +805be600 0x805be600 +805be604 PushButtonScaleFade::GetRuntimeTypeInfo +805be610 PushButton::GetRuntimeTypeInfo +805be61c PushButton::TriggerPtmf +805be64c PushButton_cpp_sinit_ +805be66c Pages::AddMKChannel::__ct +805be7f4 CtrlMenuBackButton::__dt +805be84c Pages::AddMKChannel::__dt +805be8f8 Pages::AddMKChannel::OnInit +805beb28 Pages::AddMKChannel::OnDispose +805beb2c Pages::AddMKChannel::OnActivate +805beb30 Pages::AddMKChannel::OnDeactivate +805beb34 Pages::AddMKChannel::GetNextPage +805beb4c Pages::AddMKChannel::OnAddButtonClick +805bed68 Page::GetRuntimeTypeInfo +805bed74 Pages::AddMKChannel::OnCancelButtonClick +805bee00 Pages::AddMKChannel::OnBackButtonClick +805bee3c Pages::AddMKChannel::OnBackPress +805bee4c Pages::MKChannelExplanation::__ct +805bef64 Pages::MKChannelExplanation::__dt +805bf004 Pages::MKChannelExplanation::OnInit +805bf1a0 Pages::MKChannelExplanation::OnActivate +805bf1ac Pages::MKChannelExplanation::GetNextPage +805bf1e4 Pages::MKChannelExplanation::OnButtonClick +805bf290 Pages::MKChannelExplanation::OnBackPress +805bf2d8 0x805bf2d8 +805bf2dc Pages::MKChannelExplanation::GetRuntimeTypeInfo +805bf2e8 Pages::AddMKChannel::GetRuntimeTypeInfo +805bf2f4 Pages::MKChannelExplanation::TriggerButtonPtmf +805bf324 Pages::AddMKChannel::TriggerButtonPtmf +805bf354 Pages::MKChannelExplanation::TriggerBackPtmf +805bf384 Pages::AddMKChannel::TriggerBackPtmf +805bf3b4 AddMKChannelPage_cpp_sinit_ +805bf3d0 NotifyButton::GetClassName +805bf3dc NotifyButton::__ct +805bf418 NotifyButton::__dt +805bf470 NotifyButton::Load +805bf52c NotifyButton::InitSelf +805bf600 NotifyButton::OnUpdate +805bf998 NotifyButton::OnClick +805bf9dc NotifyButton::OnClick_switch +805bf9e0 NotifyButton::OnClick_caseD_0 +805bf9e8 NotifyButton::OnClick_caseD_1 +805bf9f0 NotifyButton::OnClick_caseD_2 +805bfaf8 NotifyButton::OnClick_caseD_3 +805bfb00 NotifyButton::OnClick_caseD_5 +805bfb08 NotifyButton::OnClick_caseD_7 +805bfb10 NotifyButton::OnClick_caseD_8 +805bfb18 NotifyButton::OnClick_caseD_9 +805bfb20 NotifyButton::OnClick_caseD_a +805bfb70 NotifyButton::OnClick_switch_805bfb90::CaseD_a +805bfb94 0x805bfb94 +805bfb9c 0x805bfb9c +805bfba4 0x805bfba4 +805bfbac 0x805bfbac +805bfbb4 0x805bfbb4 +805bfbbc 0x805bfbbc +805bfbc4 0x805bfbc4 +805bfbcc 0x805bfbcc +805bfbd4 0x805bfbd4 +805bfbdc 0x805bfbdc +805bfbe4 0x805bfbe4 +805bfbf4 NotifyButton::CheckStatusAvailabilities +805bfe1c NotifyButton::UpdateText +805bfe58 NotifyButton::UpdateText_switch +805bfe5c NotifyButton::UpdateText_caseD_0 +805bfe64 NotifyButton::UpdateText_caseD_1 +805bfe6c NotifyButton::UpdateText_caseD_2 +805bfe74 NotifyButton::UpdateText_caseD_3 +805bfe7c NotifyButton::UpdateText_caseD_4 +805bfe84 NotifyButton::UpdateText_caseD_5 +805bfe8c NotifyButton::UpdateText_caseD_6 +805bfe94 NotifyButton::UpdateText_caseD_7 +805bfe9c NotifyButton::UpdateText_caseD_8 +805bfea4 NotifyButton::UpdateText_caseD_9 +805bfeac NotifyButton::UpdateText_caseD_a +805bff18 Pages::Channel::__ct +805c00c0 CtrlMenuInstructionText::__dt +805c0118 Pages::Channel::__dt +805c01fc Pages::Channel::OnInit +805c0884 Pages::Channel::OnDispose +805c0888 Pages::Channel::OnActivate +805c08e0 Pages::Channel::OnButtonClick +805c0b78 Pages::Channel::OnButtonSelect +805c0c20 Pages::Channel::OnButtonSelect_switch +805c0c24 Pages::Channel::OnButtonSelect_switch_805c0c20::CaseD_0 +805c0c2c Pages::Channel::OnButtonSelect_switch_805c0c20::CaseD_1 +805c0c34 Pages::Channel::OnButtonSelect_switch_805c0c20::CaseD_2 +805c0c3c Pages::Channel::OnButtonSelect_switch_805c0c20::CaseD_3 +805c0c44 Pages::Channel::OnButtonSelect_switch_805c0c20::CaseD_4 +805c0c4c Pages::Channel::OnButtonSelect_switch_805c0c20::CaseD_5 +805c0c54 Pages::Channel::OnButtonSelect_switch_805c0c20::CaseD_6 +805c0c5c Pages::Channel::OnButtonSelect_switch_805c0c20::CaseD_7 +805c0c64 Pages::Channel::OnButtonSelect_switch_805c0c20::CaseD_8 +805c0c6c Pages::Channel::OnButtonSelect_switch_805c0c20::CaseD_9 +805c0c74 Pages::Channel::OnButtonSelect_switch_805c0c20::CaseD_a +805c0c90 Pages::Channel::OnBackPress +805c0cc8 Pages::ChannelGhostChoice::__ct +805c0e38 Pages::ChannelGhostChoice::__dt +805c0ee8 Pages::ChannelGhostChoice::OnInit +805c1200 Pages::ChannelGhostChoice::OnActivate +805c1258 Pages::ChannelGhostChoice::OnButtonClick +805c1340 Pages::ChannelGhostChoice::OnButtonSelect +805c1394 Pages::ChannelGhostChoice::OnBackPress +805c13ac Pages::ChannelRankingChoice::__ct +805c151c Pages::ChannelRankingChoice::__dt +805c15cc Pages::ChannelRankingChoice::OnInit +805c183c Pages::ChannelRankingChoice::OnActivate +805c188c Pages::ChannelRankingChoice::GetNextPage +805c18a4 Pages::ChannelRankingChoice::OnButtonClick +805c1ad0 Pages::ChannelRankingChoice::OnButtonSelect +805c1b24 Pages::ChannelRankingChoice::OnBackPress +805c1b34 Pages::ChannelRankingChoice::GetRuntimeTypeInfo +805c1b40 Pages::ChannelGhostChoice::GetNextPage +805c1b48 Pages::ChannelGhostChoice::GetRuntimeTypeInfo +805c1b54 Pages::Channel::GetNextPage +805c1b5c Pages::Channel::GetRuntimeTypeInfo +805c1b68 NotifyButton::GetRuntimeTypeInfo +805c1b74 TriggerPtmf +805c1ba4 TriggerPtmf +805c1bd4 TriggerPtmf +805c1c04 TriggerPtmf +805c1c34 TriggerPtmf +805c1c64 TriggerPtmf +805c1c94 ChannelPage_cpp_sinit_ +805c1cc0 Pages::Approve::__ct +805c1e18 Pages::Approve::__dt +805c1eb8 Pages::Approve::OnInit +805c208c Pages::Approve::OnActivate +805c2118 Pages::Approve::OnDeactivate +805c211c Pages::Approve::Reset +805c21a8 Pages::Approve::SetTitleMessage +805c21b0 Pages::Approve::SetMessageWindowMessage +805c21b8 Pages::Approve::SetApproveButtonMessage +805c21c0 Pages::Approve::OnClick +805c223c Pages::Approve::OnBackButtonClick +805c22b8 Pages::Approve::OnBackPress +805c2324 Pages::Approve::BeforeEntranceAnimations +805c2340 0x805c2340 +805c235c 0x805c235c +805c2364 Pages::Approve::GetRuntimeTypeInfo +805c2370 Pages::Approve::TriggerApprovePtmf +805c23a0 Pages::Approve::TriggerDisapprovePtmf +805c23d0 ApprovePage_cpp_sinit_ +805c23e4 ControlGroup::__ct +805c2400 ControlGroup::__dt +805c2460 ControlGroup::Init(UIControl) +805c2620 ControlGroup::Init(Page) +805c27dc ControlGroup::SetControl +805c2804 ControlGroup::805c2804 +805c2868 ControlGroup::InitControls +805c28d8 ControlGroup::Update +805c2948 ControlGroup::Draw +805c2a5c ControlGroup::ResetControlsPosition +805c2ac0 ControlGroup::GetControl +805c2ad0 ControlGroup::DoAction +805c2c18 ControlLoader::__ct +805c2c20 ControlLoader::__dt +805c2c60 ControlLoader::Load +805c2e80 ControlLoader::LoadBRCTR +805c2f34 ControlLoader::LoadAnimations +805c3218 ControlLoader::IsStringInArray +805c32b0 ControlLoader::FindStringInArray +805c3328 ControlLoader::GetVariant +805c33a8 ControlLoader::LoadVariant +805c3470 ControlLoader::LoadMessages +805c3550 ControlLoader::LoadPictures +805c35f8 Pages::BatteryMgr::__ct +805c363c Pages::BatteryMgr::__dt +805c36a4 Pages::BatteryMgr::OnInit +805c3700 Pages::BatteryMgr::OnActivate +805c374c Pages::BatteryMgr::BeforeEntranceAnimations +805c3750 0x805c3750 +805c3754 Pages::BatteryMgr::BeforeExitAnimations +805c3758 Pages::BatteryMgr::AfterControlUpdate +805c37e0 Pages::LowBattery::__ct +805c3874 Pages::LowBattery::__dt +805c3900 Pages::LowBattery::OnInit +805c3a28 Pages::LowBattery::OnActivate +805c3a8c Pages::LowBattery::AfterControlUpdate +805c3ac4 Pages::LowBattery::OnClick +805c3ad8 Pages::LowBattery::BeforeEntranceAnimations +805c3b1c Pages::LowBattery::AfterExitAnimations +805c3b60 Pages::LowBattery::GetRuntimeTypeInfo +805c3b6c Pages::BatteryMgr::GetNextPage +805c3b74 Pages::BatteryMgr::GetRuntimeTypeInfo +805c3b80 TriggerPtmf +805c3bb0 LowBatteryPage_cpp_sinit_ +805c3bcc CountDown::__ct +805c3bec CountDown::__dt +805c3c2c Countdown::SetInitial +805c3c44 Countdown::Update +805c3cc8 CountDownTimerControl::GetClassName +805c3cd4 CountDownTimerControl::__ct +805c3d18 CountDownTimerControl::__dt +805c3d70 CountDownTimerControl::Load +805c3dfc CountDownTimerControl::InitSelf +805c407c CountDownTimerControl::OnUpdate +805c4380 CountDownTimerControl::AnimateCurrentCountDown +805c4430 CountDownTimerControl::Reset +805c44e0 0x805c44e0 +805c4748 CountDownTimerControl::GetRuntimeTypeInfo +805c4754 CountDown_cpp_sinit_ +805c4768 0x805c4768 +805c481c 0x805c481c +805c4884 0x805c4884 +805c48f0 0x805c48f0 +805c4ce8 Page::EndActivationState +805c4cec 0x805c4cec +805c4d0c Page::EndExitState +805c4d10 0x805c4d10 +805c4e80 0x805c4e80 +805c5388 0x805c5388 +805c558c 0x805c558c +805c56ac 0x805c56ac +805c56cc 0x805c56cc +805c5728 0x805c5728 +805c5730 0x805c5730 +805c573c TriggerPtmf +805c576c TriggerPtmf +805c579c 0x805c579c +805c57b0 0x805c57b0 +805c5834 0x805c5834 +805c589c 0x805c589c +805c5918 0x805c5918 +805c5ad8 Page::EndExitState +805c5adc 0x805c5adc +805c5ae0 0x805c5ae0 +805c6158 0x805c6158 +805c67f0 0x805c67f0 +805c6828 0x805c6828 +805c6a48 0x805c6a48 +805c6b18 0x805c6a48_switch +805c6b1c 0x805c6a48_caseD_0 +805c6b24 0x805c6a48_caseD_1 +805c6b2c 0x805c6a48_caseD_2 +805c6b34 0x805c6a48_caseD_3 +805c6b3c 0x805c6a48_caseD_4 +805c6b44 0x805c6a48_caseD_5 +805c6b4c 0x805c6a48_caseD_6 +805c6b54 0x805c6a48_caseD_8 +805c6b5c 0x805c6a48_caseD_9 +805c6b64 0x805c6a48_caseD_7 +805c6c08 0x805c6c08 +805c6c78 0x805c6c78 +805c6eb8 0x805c6eb8 +805c6f94 0x805c6f94 +805c7268 0x805c7268 +805c73b8 0x805c73b8 +805c73f8 0x805c73f8 +805c7738 0x805c7738 +805c7824 0x805c7824 +805c7908 0x805c7908 +805c79e8 0x805c79e8 +805c7bb4 0x805c7bb4 +805c7f0c 0x805c7f0c +805c8008 0x805c8008 +805c80a0 0x805c80a0 +805c8108 0x805c8108 +805c8118 0x805c8118 +805c82e8 0x805c82e8 +805c8398 0x805c8398 +805c8490 0x805c8490 +805c8564 0x805c8564 +805c8608 0x805c8608 +805c87a8 0x805c87a8 +805c87e4 0x805c87e4 +805c87ec 0x805c87ec +805c8830 0x805c8830 +805c8874 0x805c8874 +805c8880 0x805c8880 +805c8888 0x805c8888 +805c8894 0x805c8894 +805c889c 0x805c889c +805c88a8 TriggerPtmf +805c88d8 TriggerPtmf +805c8908 TriggerPtmf +805c8938 TriggerPtmf +805c8968 TriggerPtmf +805c8998 __sinit_DownloadGhostList_cpp +805c89b8 DpdPointerControl::GetClassName +805c89c4 DpdPointerControl::DpadpointerControl +805c8a08 DpdPointerControl::__dt +805c8a60 DpdPointerControl::Load +805c8d6c DpdPointerControl::InitSelf +805c8ebc DpdPointerControl::OnUpdate +805c9534 Pages::DpdPointer::__ct +805c9598 Pages::DpdPointer::__dt +805c960c Pages::DpdPointer::OnInit +805c9698 Pages::DpdPointer::OnActivate +805c96a4 Pages::DpdPointer::BeforeControlUpdate +805c970c Pages::DpdPointer::CheckForConditions +805c9714 Pages::System::AddPageLayer +805c971c Pages::System::Func_0x20 +805c9720 Pages::System::ChangeSection +805c9724 Pages::System::IsVisible +805c972c Pages::DpdPointer::GetClassName +805c9738 Pages::DpdPointer::GetRuntimeTypeInfo +805c9744 DpdPointerControl::GetRuntimeTypeInfo +805c9750 __sinit_DpdPointer_cpp +805c9774 Pages::Empty::__ct +805c97b8 Pages::Empty::__dt +805c9820 Pages::Empty::OnInit +805c9888 Page::OnActivate +805c988c Pages::Empty::BeforeExitAnimations +805c9890 Pages::Empty::BeforeEntranceAnimations +805c9894 NumericEditBox::GetClassName +805c98a0 NumericEditBox::__ct +805c99f8 NumericEditBox::__dt +805c9a7c NumericEditBox::Load +805c9ebc NumericEditBox::vf_0x3c +805c9ec0 NumericEditBox::SelectInitial +805c9f14 NumericEditBox::Select +805c9f34 NumericEditBox::RemoveAllDigits +805c9fe8 NumericEditBox::HasFullDigits +805ca028 NumericEditBox::HasUnusedDigit +805ca068 NumericEditBox::ComputeSum +805ca0c8 NumericEditBox::AddDigit +805ca250 NumericEditBox::RemoveDigit +805ca438 NumericEditBox::Init +805ca53c NumericEditBox::Update +805ca6e8 NumericEditBox::HandleUpPress +805ca7c0 NumericEditBox::HandleDownPress +805ca870 NumericEditBox::HandleClick +805ca94c NumericEditBox::HandleRightPress +805caa24 NumericEditBox::HandleLeftPress +805caae0 NumericEditBox::EditLetter::GetClassName +805caaf0 NumericEditBox::EditLetter::__ct +805cab2c NumericEditBox::EditLetter::__dt +805cab84 NumericEditBox::EditLetter::Load +805cac0c NumericEditBox::EditLetter::Init +805cac8c NumericEditBox::EditLetter::Update +805cac90 NumericEditBox::EditLetter::AddDigit +805cace4 NumericEditBox::EditLetter::RemoveDigit +805cacf0 NumericEditBox::EditLetter::ToggleCursor +805cad3c NumericEditBox::EditLetter::AnimateDigit +805cad74 0x805cad74 +805cad78 NumericEditBox::GetRuntimeTypeInfo +805cad84 NumericEditBox::EditLetter::GetRuntimeTypeInfo +805cad90 TriggerPtmf +805cadc0 NumericEditBox_cpp_sinit_ +805caddc 0x805caddc +805cade8 0x805cade8 +805cae24 StaffRollControl::__dt +805cae7c StaffRollControl::Load +805caf30 StaffRollControl::InitSelf +805caf68 StaffRollControl::AppearAnimated +805cafa0 StaffRollControl::DisappearAnimated +805cafd8 Pages::StaffRoll::__ct +805cb074 StaffRollText::__ct +805cb0d0 StaffRollText::__dt +805cb144 Pages::StaffRoll::__dt +805cb1c4 Pages::StaffRoll::OnInit +805cb704 Pages::StaffRoll::OnActivate +805cb754 Pages::StaffRoll::BeforeEntranceAnimations +805cb758 Pages::StaffRoll::BeforeExitAnimations +805cb75c Pages::StaffRoll::AfterControlUpdate +805cbad8 Pages::StaffRoll::SetHasNotStarted +805cbaf0 Pages::StaffRoll::OnClick +805cbafc EndingMovieControl::GetClassName +805cbb0c EndingMovieControl::__ct +805cbb48 EndingMovieControl::__dt +805cbba0 EndingMovieControl::Load +805cbc98 EndingMovieControl::InitSelf +805cbcd0 EndingMovieControl::FadeIn +805cbd08 EndingMovieControl::FadeOut +805cbd40 EndingMovieControl::IsCurrentAnimation1Or2 +805cbd88 Pages::EndingMovie::__ct +805cbe24 Pages::EndingMovie::__dt +805cbea4 Pages::EndingMovie::OnInit +805cc0a8 Pages::EndingMovie::OnActivate +805cc1b8 Pages::EndingMovie::AfterEntranceAnimations +805cc1bc Pages::EndingMovie::AfterControlUpdate +805cc2ec Pages::EndingMovie::OnMovieActivate +805cc3b8 Pages::CreditsCongrats::__ct +805cc44c Pages::CreditsCongrats::__dt +805cc4fc Pages::CreditsCongrats::OnInit +805cc6e8 Pages::CreditsCongrats::OnActivate +805cc864 Pages::CreditsCongrats::AfterControlUpdate +805ccc04 Pages::CreditsCongrats::OnResumt +805cd1ac Pages::CreditsCongrats::OnClick +805cd1b8 Pages::CreditsCongrats::Publish +805cd2e8 Pages::CreditsCongrats::AfterPublish +805cd3e8 Pages::CreditsCongrats::AfterPublishMsg +805cd4e8 Pages::CreditsCongrats::AfterDontPostTooMany +805cd5e8 Pages::CreditsCongrats::HandlePublishError +805cd6b8 Pages::CreditsCongrats::HandlePublishError_switch +805cd6bc Pages::CreditsCongrats::HandlePublishError_caseD_1 +805cd6c4 Pages::CreditsCongrats::HandlePublishError_caseD_2 +805cd6cc Pages::CreditsCongrats::HandlePublishError_caseD_3 +805cd6d4 Pages::CreditsCongrats::HandlePublishError_caseD_4 +805cd6dc Pages::CreditsCongrats::HandlePublishError_caseD_5 +805cd6e4 Pages::CreditsCongrats::HandlePublishError_caseD_6 +805cd6ec Pages::CreditsCongrats::HandlePublishError_caseD_8 +805cd6f4 Pages::CreditsCongrats::HandlePublishError_caseD_9 +805cd6fc Pages::CreditsCongrats::HandlePublishError_caseD_7 +805cd710 Pages::CreditsCongrats::HandlePublishError_caseD_0 +805cd784 0x805cd784 +805cd850 Pages::CreditsCongrats::GetRuntimeTypeInfo +805cd85c Pages::EndingMovie::GetRuntimeTypeInfo +805cd868 EndingMovieControl::GetRuntimeTypeInfo +805cd874 Pages::StaffRoll::GetRuntimeTypeInfo +805cd880 StaffRollControl::GetRuntimeTypeInfo +805cd88c TriggerPtmf +805cd8bc TriggerPtmf +805cd8ec TriggerPtmf +805cd91c Ending_cpp_sinit_ +805cd94c Text::Info::__ct +805cda34 Text::PaneHandler::__ct +805cda50 Text::PaneHandler::__dt +805cdac0 Text::Mgr::CreateInstance +805cdb68 Text::Mgr::DestroyInstance +805cdbe4 Text::PaneHandler::Init +805cdd00 Text::PaneHandler::SetMessage +805cdf28 0x805cdf28 +805ce438 Text::PaneHandler::SetMessage_switch +805ce43c Text::PaneHandler::SetMessage_case0 +805ce4cc Text::PaneHandler::SetMessage_case10 +805ce534 Text::PaneHandler::SetMessage_case12 +805ce584 Text::PaneHandler::SetMessage_case13 +805ce630 Text::PaneHandler::SetMessage_case14 +805ce79c Text::PaneHandler::SetMessage_case15 +805ce8d8 Text::PaneHandler::SetMessage_case1 +805cedc4 ManipulatorHandler::CheckPlayerPointer +805cedcc Text::PaneHandler::ResetMessage +805cee08 Text::PaneHandler::Draw +805ceed8 0x805ceed8 +805ceef4 0x805ceef4 +805cef10 Text::PaneHandler::GetBMGInfo +805cefc4 0x805cefc4 +805cf068 Text::GlyphParams::__ct +805cf06c Text::GlyphParams::__dt +805cf0ac Text::GlyphParams::SetCharacter +805cf110 Text::GlyphUnicodeExternal::__ct +805cf114 Text::GlyphUnicodeExternal::__dt +805cf154 Text::GlyphUnicodeExternal::SetCharacter +805cf27c Text::GlyphDrawer::__dt +805cf2bc Text::GlyphDrawer::Init +805cf598 Text::GlyphDrawer::Draw +805cf680 Text::GlyphDrawer::DrawExternalUnicode +805cf7e4 Text::GlyphDrawer::SetupGXColors +805cf9ec Text::GlyphDrawer::SetupGXColors_switch +805cf9f0 Text::GlyphDrawer::SetupGXColors_caseD_1 +805cfa08 Text::GlyphDrawer::SetupGXColors_caseD_10 +805cfa20 Text::GlyphDrawer::SetupGXColors_caseD_11 +805cfa3c Text::GlyphDrawer::SetupGXColors_caseD_12 +805cfa58 Text::GlyphDrawer::SetupGXColors_caseD_13 +805cfa74 Text::GlyphDrawer::SetupGXColors_caseD_14 +805cfa90 Text::GlyphDrawer::SetupGXColors_caseD_15 +805cfaac Text::GlyphDrawer::SetupGXColors_caseD_16 +805cfac8 Text::GlyphDrawer::SetupGXColors_caseD_17 +805cfae0 Text::GlyphDrawer::SetupGXColors_caseD_20 +805cfaf8 Text::GlyphDrawer::SetupGXColors_caseD_21 +805cfb14 Text::GlyphDrawer::SetupGXColors_caseD_30 +805cfb2c Text::GlyphDrawer::SetupGXColors_caseD_31 +805cfb48 Text::GlyphDrawer::SetupGXColors_caseD_32 +805cfb60 Text::GlyphDrawer::SetupGXColors_caseD_33 +805cfb78 Text::GlyphDrawer::SetupGXColors_caseD_40 +805cfb94 Text::GlyphDrawer::SetupGXColors_caseD_18 +805cfbd8 Text::GlobalHandler::__ct +805cfbe4 Text::GlobalHandler::__dt +805cfc24 Text::GlobalHandler::SetPaneHandler +805cff74 0x805cff74 +805d00b0 Text::GlobalHandler::SetCharacter +805d0324 Text::GlobalHandler::SetExternalUnicodeChar +805d0584 0x805d0584 +805d061c 0x805d061c +805d0660 0x805d0660 +805d0674 Text::GlobalHandler::Calc +805d0c90 0x805d0c90 +805d0cd4 Text::GlobalHandler::CalcMessageParams +805d0d98 Text::GlobalHandler::GetGlyphParamsAndIncrement +805d0dd0 Text::GlobalHandler::GetUnicodeGlyphParamsAndIncrement +805d0e08 Text::GlobalHandler::SetPaneHandlerAndPane +805d0f34 Text::GameFont::__ct +805d0f80 Text::GameFont::__dt +805d0ff0 Text::GameFont::Load +805d1044 Text::GameFont::GetTextureByCode +805d1134 Text::GameFont::GetTextureByIdx +805d1214 Text::GameFont::Init +805d130c Text::FontMgr::__ct +805d132c Text::FontMgr::__dt +805d13c4 Text::FontMgr::CreateInstance +805d1414 Text::FontMgr::DestroyInstance +805d14a4 Text::FontMgr::Init +805d1758 Text::FontMgr::InitFont +805d1804 Text::FontMgr::GetFont +805d1888 HandleDisplayControl::GetClassName +805d1894 HandleDisplayControl::__ct +805d18d0 HandleDisplayControl::__dt +805d1928 HandleDisplayControl::Load +805d19a8 0x805d19a8 +805d1a0c HandleDisplayControl::InitSelf +805d1ab8 Pages::WheelOnlyComp::__ct +805d1b24 Pages::WheelOnlyComp::__dt +805d1ba8 Pages::WheelOnlyComp::OnInit +805d1cd0 Pages::WheelOnlyComp::OnActivate +805d1ce4 Pages::WheelOnlyComp::AfterEntranceAnimations +805d1cf8 Pages::WheelOnlyComp::AfterControlUpdate +805d1e48 Pages::WheelOnlyComp::GetRuntimeTypeInfo +805d1e54 HandleDisplayControl::GetRuntimeTypeInfo +805d1e60 WheelOnlyComp_cpp_sinit_ +805d1e84 FriendList::__ct +805d1ef8 0x805d21ef8 +805d1f38 FriendListEntry::__ct +805d1f80 FriendListEntry::__dt +805d1fc0 0x805d1fc0 +805d2000 0x805d2000 +805d2040 FriendList::__dt +805d20a4 FriendList::Init +805d21b4 FriendList::SaveToRKSYS +805d2264 FriendList::Update +805d22c8 0x805d22c8 +805d23d8 0x805d23d8 +805d24c0 FriendList::GetFriendCount +805d255c FriendList::AddFriend +805d2700 FriendList::RemoveFriend +805d2778 FriendList::GetFriendIdx +805d2874 FriendList::GetFriendEntry +805d2890 FriendList::GetFriend +805d28c8 FriendList::GetFriendStatus +805d2900 FriendList::IsFriendStatusLessThan25 +805d294c FriendList::IsUpdated +805d2968 FriendList::UpdateKeyController +805d2990 0x805d2990 +805d29dc 0x805d29dc +805d2a04 0x805d2a04 +805d2a50 FriendList::UpdateStatuses +805d2aa8 0x805d2aa8 +805d2b44 FriendList::UpdateStatuses_switch +805d2b48 FriendList::UpdateStatuses_caseD_1 +805d2b50 FriendList::UpdateStatuses_caseD_2 +805d2b58 FriendList::UpdateStatuses_caseD_3 +805d2b60 FriendList::UpdateStatuses_caseD_5 +805d2b68 FriendList::UpdateStatuses_caseD_6 +805d2b70 FriendList::UpdateStatuses_caseD_7 +805d2b78 FriendList::UpdateStatuses_caseD_8 +805d2b80 FriendList::UpdateStatuses_caseD_9 +805d2b88 FriendList::UpdateStatuses_caseD_a +805d2b90 FriendList::UpdateStatuses_caseD_b +805d2bf4 FriendList::UpdateStatuses_caseD_4 +805d2c00 FriendList::UpdateStatuses_caseD_c +805d2c58 FriendList::UpdateStatuses_caseD_d +805d2cc0 FriendList::UpdateStatuses_caseD_e +805d2d18 FriendList::UpdateStatuses_caseD_0 +805d2d44 FriendList::UpdateFriendData +805d3108 0x805d3108 +805d310c 0x805d310c +805d3134 0x805d3134 +805d3138 0x805d3138 +805d3158 0x805d3158 +805d315c 0x805d315c +805d31a8 0x805d31a8 +805d31ac 0x805d31ac +805d31f8 0x805d31f8 +805d31fc FriendCodesController::Update +805d32b8 FriendCodesController::TrySaveMgrToRKSYS +805d331c FriendCodesController::SaveMgrToRKSYS +805d3450 FriendCodesController::ResetMgrKeys +805d3460 FriendReceiver::TrySaveToRKSYS +805d34d4 FriendReceiver::TryReceiveData +805d36bc FriendReceiver::SaveToRKSYS +805d376c FriendReceiver::vf_0x10 +805d3800 FriendButton::GetClassName +805d380c FriendButton::__ct +805d3850 FriendButton::__dt +805d38a8 FriendButton::Load +805d3980 FriendButton::SetMii +805d3b5c 0x805d3b5c +805d3c54 FriendButton::InitSelf +805d3c58 FriendButton::OnUpdate +805d4028 FriendButton::OnSelect +805d4120 FriendButton::OnDeselect +805d41c8 Pages::FriendList::__ct +805d43a8 Pages::FriendList::__dt +805d446c Pages::FriendList::OnInit +805d4830 Pages::FriendList::OnDispose +805d4834 Pages::FriendList::OnActivate +805d4a54 Pages::FriendList::BeforeEntranceAnimations +805d4b5c Pages::FriendList::BeforeExitAnimations +805d4b70 Pages::FriendList::OnDeactivate +805d4c10 Pages::FriendList::AfterControlUpdate +805d4c8c Pages::FriendList::AddNewFriend +805d4d08 Pages::FriendList::OnFriendButtonClick +805d4e50 Pages::FriendList::OnBackButtonClick +805d4ee4 Pages::FriendList::OnRightArrowPress +805d5014 Pages::FriendList::OnLeftArrowPress +805d510c Pages::FriendList::OnBackPress +805d518c Pages::FriendList::CompareFriendIndexes +805d521c Pages::FriendList::FriendStatusToValue +805d523c Pages::FriendList::FriendStatusToValue_switch +805d5240 Pages::FriendList::FriendStatusToValue_Case_fffffffe +805d5248 Pages::FriendList::FriendStatusToValue_Case_ffffffff +805d5250 Pages::FriendList::FriendStatusToValue_Case_0 +805d5258 Pages::FriendList::FriendStatusToValue_Case_1 +805d5260 Pages::FriendList::FriendStatusToValue_Case_2 +805d5268 Pages::FriendList::FriendStatusToValue_Case_3 +805d5270 Pages::FriendList::FriendStatusToValue_Case_4 +805d5278 Pages::FriendList::FriendStatusToValue_Case_5 +805d5280 Pages::FriendList::FriendStatusToValue_Case_6 +805d5288 Pages::FriendList::FriendStatusToValue_Case_7 +805d5290 Pages::FriendList::FriendStatusToValue_Case_8 +805d5298 Pages::FriendList::FriendStatusToValue_Case_9 +805d52a0 Pages::FriendList::FriendStatusToValue_Case_a +805d52a8 Pages::FriendList::FriendStatusToValue_Case_b +805d52b0 Pages::FriendList::FriendStatusToValue_Case_c +805d52b8 Pages::FriendList::FriendStatusToValue_Case_d +805d52c0 Pages::FriendList::FriendStatusToValue_Case_e +805d52c8 Pages::FriendList::FriendStatusToValue_Case_f +805d52d0 Pages::FriendList::FriendStatusToValue_Case_10 +805d52d8 Pages::FriendList::FriendStatusToValue_Case_11 +805d52e0 Pages::FriendList::FriendStatusToValue_Case_12 +805d52e8 Pages::FriendList::FriendStatusToValue_Case_13 +805d52f0 Pages::FriendList::FriendStatusToValue_Case_14 +805d52f8 Pages::FriendList::FriendStatusToValue_Case_15 +805d5300 Pages::FriendList::FriendStatusToValue_Case_16 +805d5308 Pages::FriendList::FriendStatusToValue_Case_17 +805d5310 Pages::FriendList::FriendStatusToValue_Case_18 +805d5318 Pages::FriendList::FriendStatusToValue_Case_19 +805d5320 Pages::FriendList::FriendStatusToValue_Case_1a +805d5330 Pages::FriendList::UpdatePageCounter +805d53f8 Pages::FriendList::UpdateMiiGroup +805d5500 JoinFriendButton::GetClassName +805d5510 JoinFriendButton::__ct +805d554c JoinFriendButton::__dt +805d55a4 JoinFriendButton::__dt +805d56ac JoinFriendButton::SetStatus +805d5818 JoinFriendButton::OnUpdate +805d585c Pages::FriendInfo::__ct +805d5a14 Pages::FriendInfo::__dt +805d5af4 Pages::FriendInfo::OnInit +805d6000 Pages::FriendInfo::OnActivate +805d6128 Pages::FriendInfo::AfterControlUpdate +805d6390 Pages::FriendInfo::PlaySound +805d6470 Pages::FriendInfo::SetInfoText +805d6654 0x805d6654 +805d6678 Pages::FriendInfo::OnJoinButtonClick +805d690c Pages::FriendInfo::OnRemoveButtonClick +805d6950 Pages::FriendInfo::OnBackButtonClick +805d6994 Pages::FriendInfo::OnBackPress +805d69ac Pages::FriendRemove::__ct +805d6b04 Pages::FriendRemove::__dt +805d6ba4 Pages::FriendRemove::OnInit +805d6d40 Pages::FriendRemove::OnActivate +805d6e10 Pages::FriendRemove::OnDeactivate +805d6ec8 Pages::FriendRemove::OnResume +805d6f9c Pages::FriendRemove::OnRemoveButtonClick +805d7190 Pages::FriendRemove::OnCancelAndBackButtonClick +805d71ec Pages::FriendRemove::OnBackPress +805d7204 Pages::FriendRemoving::__ct +805d7278 Pages::FriendRemoving::__dt +805d7308 Pages::FriendRemoving::OnInit +805d7404 Pages::FriendRemoving::OnActivate +805d74f4 Pages::FriendRemoving::BeforeExitAnimations +805d7658 Pages::FriendRemoving::AfterControlUpdate +805d796c Pages::FriendJoinMgr::__ct +805d79b0 Pages::FriendJoinMgr::__dt +805d7a18 Pages::FriendJoinMgr::OnInit +805d7a84 Pages::FriendJoinMgr::OnActivate +805d7d54 Pages::FriendJoinMgr::BeforeEntranceAnimations +805d7d58 Pages::FriendJoinMgr::AfterEntranceAnimations +805d7d68 Pages::FriendJoinMgr::BeforeExitAnimations +805d7d6c Pages::FriendJoinMgr::GetNextPage +805d7d74 Pages::FriendJoinMgr::GetRuntimeTypeInfo +805d7d80 Pages::FriendRemoving::IsHomeMenuWorking +805d7d88 Pages::FriendRemoving::GetNextPage +805d7d90 Pages::FriendRemoving::GetRuntimeTypeInfo +805d7d9c Pages::FriendRemove::GetNextPage +805d7da4 Pages::FriendRemove::GetRuntimeTypeInfo +805d7db0 Pages::FriendInfo::GetNextPage +805d7db8 Pages::FriendInfo::GetRuntimeTypeInfo +805d7dc4 JoinFriendButton::GetRuntimeTypeInfo +805d7dd0 Pages::FriendList::GetNextPage +805d7dd8 Pages::FriendList::GetRuntimeTypeInfo +805d7de4 FriendButton::GetRuntimeTypeInfo +805d7df0 TriggerPtmf +805d7e20 TriggerPtmf +805d7e50 TriggerPtmf +805d7e80 TriggerPtmf +805d7eb0 TriggerPtmf +805d7ee0 TriggerPtmf +805d7f10 TriggerPtmf +805d7f40 FriendListPage_cpp_sinit_ +805d7f78 Pages::FriendRoom::__ct +805d8160 Pages::FriendRoom::__dt +805d820c Pages::FriendRoom::OnInit +805d8444 Pages::FriendRoom::OnActivate +805d84fc Pages::FriendRoom::OnDeactivate +805d8508 Pages::FriendRoom::AfterControlUpdate +805d8c98 Pages::FriendRoom::OnResume +805d8f40 Pages::FriendRoom::SetStatus +805d8f48 0x805d8f48 +805d8f84 Pages::FriendRoom::OnMessagesButtonClick +805d906c Pages::FriendRoom::OnStartButtonClick +805d9154 Pages::FriendRoom::OnAddFriendsButtonClick +805d9160 Pages::FriendRoom::OnBackButtonClick +805d92a0 Pages::FriendRoom::OnButtonSelect +805d930c Pages::FriendRoom::OnBackPress +805d944c FriendMatchingPlayer::GetClassName +805d945c FriendMatchingPlayer::__ct +805d94d0 FriendMatchingPlayer::__dt +805d9528 FriendMatchingPlayer::Load +805d9614 FriendMatchingPlayer::InitSelf +805d9700 FriendMatchingPlayer::OnUpdate +805d987c 0x805d987c +805d9994 0x805d9994 +805d99f8 0x805d99f8 +805d9aa8 FriendMatchingPlayer::OnMessageSent +805d9b38 Pages::FriendRoomManager::__ct +805d9bcc Pages::FriendRoomManager::__dt +805d9c74 Pages::FriendRoomManager::OnInit +805d9db0 Pages::FriendRoomManager::OnActivate +805d9f20 Pages::FriendRoomManager::AfterEntranceAnimations +805da028 Pages::FriendRoomManager::BeforeExitAnimations +805da0f4 Pages::FriendRoomManager::OnDeactivate +805da140 Pages::FriendRoomManager::AfterControlUpdate +805dac50 AvoidController_UseMinimumAvoidance +805dac60 0x805dac60 +805dacb0 Pages::FriendRoomManager::GetMessageBmg +805dada4 Pages::FriendRoomManager::SetToSendPacket +805dae28 0x805dae28 +805dae30 0x805dae30 +805dae58 0x805dae58 +805dae68 0x805dae68 +805dae7c 0x805dae7c +805dae80 0x805dae80 +805daec0 0x805daec0 +805daf38 0x805daf38 +805db204 0x805db204 +805db278 0x805db278 +805db320 0x805db320 +805db348 0x805db348 +805db358 0x805db358 +805db634 0x805db634 +805db6b8 MessageSelectControl::GetClassName +805db6c8 MessageSelectControl::__ct +805db724 MessageSelectControl::__dt +805db798 MessageSelectControl::Load +805db8ec MessageSelectControl::InitSelf +805db8f0 MessageSelectControl::OnUpdate +805db9e8 MessageSelectControl::SetPositionAnim +805dba20 MessageSelectControl::Activate +805dbab8 MessageSelectControl::Deactivate +805dbb28 MessageSelectControl::ChangeToPage +805dbb88 MessageSelectControl::ChangeFromPage +805dbc20 MessageSelectControl::IsActivated +805dbc58 MessageSelectControl::IsDeactivated +805dbc8c MessageSelectControl::SetButtonBmg +805dbcc4 MessageSelectControl::SetOnClickHandler +805dbd24 MessageSelectControl::SelectInitialButton +805dbd34 MessageSelectControl::GetSelectedButtonId +805dbd94 Pages::FriendRoomMessages::__ct +805dbfdc SheetSelectControlScaleFade::__dt +805dc034 Pages::FriendRoomMessages::__dt +805dc104 Pages::FriendRoomMessages::OnInit +805dc378 Pages::FriendRoomMessages::OnActivate +805dc71c Pages::FriendRoomMessages::OnDeactivate +805dc7b8 Pages::FriendRoomMessages::BeforeControlUpdate +805dc840 0x805dc840 +805dca9c Pages::FriendRoomMessages::End +805dcab4 Pages::FriendRoomMessages::UpdateMessage +805dcc70 Pages::FriendRoomMessages::OnMessageButtonClick +805dcd78 Pages::FriendRoomMessages::OnModeButtonClick +805dce80 Pages::FriendRoomMessages::OnAddFriendsButtonClick +805dd0c8 Pages::FriendRoomMessages::OnRightArrowPress +805dd1d4 Pages::FriendRoomMessages::OnLeftArrowPress +805dd2dc Pages::FriendRoomMessages::OnBackButtonClick +805dd318 Pages::FriendRoomMessages::OnBackPress +805dd330 Pages::FriendRoomWaiting::__ct +805dd38c Pages::FriendRoomWaiting::__dt +805dd418 Pages::FriendRoomWaiting::OnInit +805dd5ac Pages::FriendRoomWaiting::OnActivate +805dd724 Pages::FriendRoomWaiting::OnDeactivate +805dd734 Pages::FriendRoomWaiting::BeforeEntranceAnimations +805dd7c0 Pages::FriendRoomWaiting::BeforeExitAnimations +805dd7c4 Pages::FriendRoomWaiting::AfterControlUpdate +805ddcc8 Pages::FriendRoomWaiting::OnResume +805de088 0x805de088 +805de090 Pages::FriendRoomWaiting::StartRoom +805de838 Pages::Menu::GetRuntimeTypeInfo +805de844 0x805de844 +805de84c Pages::FriendRoomWaiting::IsHomeMenuWorking +805de854 Pages::FriendRoomWaiting::GetRuntimeTypeInfo +805de860 Pages::FriendRoomMessages::GetRuntimeTypeInfo +805de86c MessageSelectControl::GetRuntimeTypeInfo +805de878 Pages::FriendRoomManager::GetRuntimeTypeInfo +805de884 FriendMatchingPlayer::GetRuntimeTypeInfo +805de890 Pages::FriendRoom::GetRuntimeTypeInfo +805de89c TriggerPtmf +805de8cc TriggerPtmf +805de8fc TriggerPtmf +805de92c TriggerPtmf +805de95c TriggerPtmf +805de98c TriggerPtmf +805de9bc FriendRoomPage_cpp_sinit_ +805de9f0 Pages::GhostRaceExplanation::__ct +805deb08 Pages::GhostRaceExplanation::__dt +805deba8 Pages::GhostRaceExplanation::OnInit +805ded24 Pages::GhostRaceExplanation::OnActivate +805deeac Pages::GhostRaceExplanation::GetNextPage +805deefc Pages::GhostRaceExplanation::OnButtonClick +805def88 Pages::GhostRaceExplanation::OnBackPress +805defd4 Pages::GhostRaceMgr::__ct +805df058 Pages::GhostRaceMgr::__dt +805df0c0 Pages::GhostRaceMgr::OnInit +805df1d0 Pages::GhostRaceMgr::OnActivate +805df2f0 Pages::GhostRaceMgr::BeforeEntranceAnimations +805df2f4 Pages::GhostRaceMgr::BeforeExitAnimations +805df2f8 Pages::GhostRaceMgr::AfterControlUpdate +805df540 Pages::GhostRaceMgr::OnResume +805df978 Pages::GhostRaceMgr::DownloadGhost +805e00f4 0x805e00f4 +805e0204 0x805e0204 +805e0344 Pages::GhostRaceMgr::PrepareRace +805e053c Pages::GhostRaceMgr::OnErrorBoxClick +805e0584 Pages::GhostRaceInfo::__ct +805e069c Pages::GhostRaceInfo::__dt +805e073c Pages::GhostRaceInfo::OnInit +805e08b4 Pages::GhostRaceInfo::OnActivate +805e08c0 Pages::GhostRaceInfo::UpdateGhostInfo +805e0918 Pages::GhostRaceInfo::OnButtonClick +805e0a44 Pages::GhostRaceInfo::OnBackPress +805e0afc Pages::GhostRaceInfo::GetRuntimeTypeInfo +805e0b08 Pages::GhostRaceMgr::GetNextPage +805e0b10 Pages::GhostRaceMgr::GetRuntimeTypeInfo +805e0b1c Pages::GhostRaceExplanation::GetRuntimeTypeInfo +805e0b28 TriggerPtmf +805e0b58 TriggerPtmf +805e0b88 TriggerPtmf +805e0bb8 TriggerPtmf +805e0be8 TriggerPtmf +805e0c18 GhostRacePage_cpp_sinit_ +805e0c38 Pages::GhostManager::__ct +805e0cb8 Pages::GhostManager::__dt +805e0d84 Pages::GhostManager::OnInit +805e0ffc Pages::GhostManager::OnActivate +805e10f8 Pages::GhostManager::BeforeEntranceAnimations +805e10fc Pages::GhostManager::BeforeExitAnimations +805e1100 Pages::GhostManager::OnDeactivate +805e1104 Pages::GhostManager::AfterControlUpdate +805e1108 Pages::GhostManager::OnSectionChange +805e11b0 Pages::GhostManager::RequestAllGhosts +805e1214 Pages::GhostManager::UpdateState +805e124c Pages::GhostManager::UpdateState_switch +805e1250 Pages::GhostManager::UpdateState_case_1 +805e1360 Pages::GhostManager::UpdateState_case_2 +805e1384 Pages::GhostManager::UpdateState_case_3 +805e13bc Pages::GhostManager::UpdateState_case_4 +805e13f4 Pages::GhostManager::UpdateState_case_5 +805e142c Pages::GhostManager::UpdateState_case_6 +805e145c Pages::GhostManager::UpdateState_case_7 +805e1480 Pages::GhostManager::UpdateState_case_9 +805e14ac Pages::GhostManager::UpdateState_case_a +805e14d8 Pages::GhostManager::UpdateState_case_b +805e1504 Pages::GhostManager::UpdateState_case_c +805e1528 Pages::GhostManager::UpdateState_case_d +805e1548 Pages::GhostManager::UpdateState_case_0 +805e15c4 Pages::GhostManager::AreStaffGhostsLoading +805e163c 0x805e163c +805e1748 Pages::GhostManager::PrepareRaceState +805e17cc Pages::GhostManager::PrepareReplayState +805e1800 0x805e1800 +805e1820 0x805e1820 +805e1894 0x805e1894 +805e1998 Pages::GhostManager::RequestGhost +805e1b04 Pages::GhostManager::SetupGhostRace +805e1d5c Pages::GhostManager::SetupGhostReplay +805e1ef8 0x805e1ef8 +805e1fd0 0x805e1fd0 +805e208c GhostList::__ct +805e209c GhostList::__dt +805e20dc GhostList::FillWithAllGhosts +805e2198 GhostList::FillWithAllDLdGhosts +805e2200 GhostList::Init +805e22c8 GhostList::Reset +805e22d4 GhostList::FillFromGroup +805e241c GhostList::Sort +805e2430 GhostList::CompareEntries +805e2500 GhostList::GetHeader +805e2528 GhostList::CheckIsNew +805e2554 GhostList::AllAreGhostsNrw +805e2588 GhostList::SetSectionParamsGhostValues +805e2610 GhostList::EraseEntry +805e2720 Pages::GhostManager::GetRuntimeTypeInfo +805e272c GhostManagerPage_cpp_sinit_ +805e2740 GhostInfoControl::GetClassName +805e274c GhostInfoControl::__ct +805e27dc GhostInfoControl::__dt +805e2844 GhostInfoControl::Load +805e2964 GhostInfoControl::InitSelf +805e2968 GhostInfoControl::OnUpdate +805e296c GhostInfoControl::UpdateInfo +805e2c80 0x805e2c80 +805e2d40 GhostInfoControl::UpdateInfoAnimate +805e2da0 0x805e2da0 +805e2df0 0x805e2df0 +805e2e28 0x805e2e28 +805e2e60 0x805e2e60 +805e2e98 0x805e2e98 +805e2ed0 0x805e2ed0 +805e2f08 0x805e2f08 +805e2f40 GhostInfoControl::GetRuntimeTypeInfo +805e2f4c GhostInfoControl_cpp_sinit_ +805e2f60 SectionParams::__ct +805e2ff8 SectionParams::__dt +805e3080 SectionParams::Reset +805e3224 SectionParams::ResetMiisGhostsAndSubs +805e32ac SectionParams::ResetBattleParams +805e32c4 0x805e32c4 +805e32ec SectionParams::RandomizeVSTracks +805e346c SectionParams::SetVStracks +805e36a0 SectionParams::IsCourseUnlockedInOneCC +805e37b0 SectionParams::IsCupUnlockedInOneCC +805e3858 SectionParams::RandomizeBattleArenas +805e38fc SectionParams::SetBattleArenas +805e39d8 SectionParams::CopyMiisToRacedata +805e3b98 OnlineParams::ResetRaceNumberAndCC +805e3ba8 OnlineParams::Reset +805e3d38 OnlineParams::CalcRank +805e3d48 OnlineParams::SetRankBMG +805e3d6c 0x805e3d6c +805e3d80 0x805e3d80 +805e3d98 0x805e3d98 +805e3dbc 0x805e3dbc +805e3dc8 TitleParams::__ct +805e3dfc TitleParams::Reset +805e3efc TitleParams::ForceDemo +805e3f08 0x805e3f08 +805e3f4c 0x805e3f4c +805e3f5c 0x805e3f5c +805e3f68 SectionParams::TrackIdxToCourseId +805e3fa0 SectionParams::CourseIdToTrackIdx +805e3ff8 SectionParams::CourseIdToCupIdx +805e4074 SectionParams::ArenaIdToCupIdx +805e40a8 SectionParams::ResetLicenseParams +805e422c Pages::Globe::__ct +805e42c0 Pages::Globe::MessageWindow::__dt +805e4318 Pages::Globe::__dt +805e439c Pages::Globe::OnInit +805e45bc Pages::Globe::OnActivate +805e46c0 Pages::Globe::BeforeEntranceAnimations +805e46c4 Pages::Globe::BeforeExitAnimations +805e46c8 Pages::Globe::AfterControlUpdate +805e50b4 0x805e50b4 +805e5254 0x805e5254 +805e5530 0x805e5530 +805e57f0 0x805e57f0 +805e5904 0x805e5904 +805e5a0c Pages::Globe::SetMessage +805e5de0 0x805e5de0 +805e5ee4 0x805e5ee4 +805e5fe8 0x805e5fe8 +805e60ec 0x805e60ec +805e6238 0x805e6238 +805e6384 0x805e6384 +805e646c 0x805e646c +805e64e4 0x805e64e4 +805e65b4 0x805e65b4 +805e6624 0x805e6624 +805e66b8 0x805e66b8 +805e6708 0x805e6708 +805e6744 Pages::Globe::GetFriendActivityBMG +805e6760 Pages::Globe::GetFriendActivityBMG_switch +805e6764 Pages::Globe::GetFriendActivityBMG_caseD_1 +805e676c Pages::Globe::GetFriendActivityBMG_caseD_2 +805e6774 Pages::Globe::GetFriendActivityBMG_caseD_3 +805e677c Pages::Globe::GetFriendActivityBMG_caseD_4 +805e6784 Pages::Globe::GetFriendActivityBMG_caseD_5 +805e678c Pages::Globe::GetFriendActivityBMG_caseD_6 +805e6794 Pages::Globe::GetFriendActivityBMG_caseD_7 +805e679c Pages::Globe::GetFriendActivityBMG_caseD_8 +805e67a4 Pages::Globe::GetFriendActivityBMG_caseD_9 +805e67ac Pages::Globe::GetFriendActivityBMG_caseD_a +805e67b4 Pages::Globe::GetFriendActivityBMG_caseD_b +805e67bc Pages::Globe::GetFriendActivityBMG_caseD_c +805e67c4 Pages::Globe::GetFriendActivityBMG_caseD_d +805e67cc Pages::Globe::GetFriendActivityBMG_caseD_e +805e67d4 Pages::Globe::GetFriendActivityBMG_caseD_f +805e67dc Pages::Globe::GetFriendActivityBMG_caseD_10 +805e67e4 Pages::Globe::GetFriendActivityBMG_caseD_11 +805e67ec Pages::Globe::GetFriendActivityBMG_caseD_12 +805e67f4 Pages::Globe::GetFriendActivityBMG_caseD_13 +805e67fc Pages::Globe::GetFriendActivityBMG_caseD_14 +805e6804 Pages::Globe::GetFriendActivityBMG_caseD_15 +805e680c Pages::Globe::GetFriendActivityBMG_caseD_16 +805e6814 Pages::Globe::GetFriendActivityBMG_caseD_17 +805e681c Pages::Globe::GetFriendActivityBMG_caseD_18 +805e6824 Pages::Globe::GetFriendActivityBMG_caseD_19 +805e682c Pages::Globe::GetFriendActivityBMG_caseD_1a +805e6834 Pages::Globe::GetFriendActivityBMG_caseD_0 +805e683c Pages::Globe::MessageWindow::GetClassName +805e684c 0x805e684c +805e6930 Pages::Globe::MessageWindow::InitSelf +805e6994 0x805e6994 +805e6a30 0x805e6a30 +805e6ae0 0x805e6ae0 +805e6b14 Pages::Globe::GetRuntimeTypeInfo +805e6b20 Pages::Globe::MessageWindow::GetRuntimeTypeInfo +805e6b2c GlobePage_cpp_sinit_ +805e6b50 0x805e6b50 +805e6b9c 0x805e6b9c +805e6c04 0x805e6c04 +805e6c70 0x805e6c70 +805e6d28 Page::EndActivationState +805e6d2c 0x805e6d2c +805e6ddc 0x805e6ddc +805e6e58 0x805e6e58 +805e7220 0x805e7220 +805e7440 0x805e7440 +805e744c 0x805e744c +805e7460 PaneManager::SearchForPane +805e74f8 PaneManager::GetParentPane +805e7500 0x805e7500 +805e7700 PaneManager::IsPaneAndParentsVisible +805e77f8 PaneManager::IsPaneVisible +805e7804 PaneManager::SetTextBoxMessage +805e78f0 PaneManager::DoAction +805e7a6c PaneManager::CheckForUserInfo +805e7aac PaneManager::CheckForUserInfo2 +805e7aec PaneManager::GetNextUserInfo +805e7b40 PaneManager::ConvertColorS10ToUT +805e7bf4 0x805e7bf4 +805e7c1c PaneManagerHolder::__dt +805e7ca8 PaneManagerHolder::__ct +805e7d94 PicturePaneManager::__ct +805e7db0 TextPaneManager::__ct +805e7dcc MoviePaneManager::__ct +805e7de8 0x805e7de8 +805e7dec PaneManagerHolder::InsertPicturePane +805e7e3c PaneManagerHolder::InsertTextPane +805e7e8c PaneManagerHolder::InsertMoviePane +805e7edc PaneManagerHolder::Draw +805e7f38 PicturePaneManager::Draw +805e805c TextPaneManager::Draw +805e8178 MoviePaneManager::Draw +805e8294 PaneManagerHolder::InsertManager +805e82c4 Layout::__ct +805e830c Layout::__dt +805e8368 Layout::GetPaneByName +805e8380 Layout::CreateResourceAccessor +805e8528 Layout::Build +805e85a8 MainLayout::__ct +805e8630 MainLayout::__dt +805e86f4 MainLayout::CreateResourceAccessor +805e889c MainLayout::Build +805e9028 MainLayout::GetTextPaneHandlerByName +805e90a0 MainLayout::GetTextPaneHandlerByPane +805e90e8 MainLayout::GetMoviePaneHandlerByName +805e9160 MainLayout::GetMoviePaneHandlerByPane +805e91a8 MainLayout::Animate +805e91bc MainLayout::Update +805e9268 MainLayout::Draw +805e92d0 Text::PaneHandler::Initializer::Calc +805e93d4 MoviePaneHander::Initializer::Calc +805e94d8 PaneTypeCounter::Calc +805e9708 PaneManager::Initializer::Calc +805e9b28 PictureLayout::__ct +805e9bc0 PictureLayout::__dt +805e9c20 PictureLayout::CreateResourceAccessor +805e9dd8 PictureLayout::Build +805e9e78 PictureLayout::SetPrevLayout +805e9e80 PictureLayout::IsLinkedToLYT +805e9eec PictureLayoutList::__ct +805e9ef8 PictureLayoutList::__dt +805e9f80 PictureLayoutList::Attach +805ea28c LayoutResourceAccessorList::__ct +805ea298 LayoutResourceAccessorList::__dt +805ea358 LayoutFont::__dt +805ea398 0x805ea398 +805ea3d8 0x805ea3d8 +805ea418 LayoutResources::__dt +805ea490 LayoutResourceLink::__dt +805ea4d0 LayoutResourceAccessorList::Attach +805ea66c LayoutResourceAccessor::Init +805ea770 LayoutResourceLink::__ct +805ea780 0x805ea780 +805ea784 0x805ea784 +805ea7c4 0x805ea7c4 +805ea818 LicenseControl::GetClassName +805ea824 LicenseButton::GetClassName +805ea834 Pages::LicenseSelect::__ct +805eaa6c LicenseButton::__ct +805eaaa8 LicenseButton::__dt +805eab00 Pages::LicenseSelect::__dt +805eabb8 Pages::LicenseSelect::OnInit +805eae94 Pages::LicenseSelect::SetControlMii +805eb774 Pages::LicenseSelect::OnActivate +805eb798 Pages::LicenseSelect::OnResume +805eb79c Pages::LicenseSelect::AfterControlUpdate +805eb7a0 Pages::LicenseSelect::OnLicenseButtonClick +805ebb44 Pages::LicenseSelect::OnOptionsButtonClick +805ebc8c Pages::LicenseSelect::OnBackButtonClick +805ebcd0 Pages::LicenseSelect::OnBackPress +805ebce8 Pages::LicenseSelect::OnNewLicenseApprove +805ebd44 Pages::LicenseSelect::OnNewLicenseDisapprove +805ebda0 Pages::LicenseSelect::OnNewLicenseBack +805ebdac Pages::LicenseChangeMii::__ct +805ebf10 LicenseControl::__dt +805ebf68 Pages::LicenseChangeMii::__dt +805ec008 Pages::LicenseChangeMii::OnInit +805ec274 Pages::LicenseChangeMii::OnActivate +805ec4e8 Pages::LicenseChangeMii::SetControlMii +805ecdc8 Pages::LicenseChangeMii::UpdateMiiGroup +805ecdd8 Pages::LicenseChangeMii::PrepareMessageBox +805ecf2c 0x805ecf2c +805ecff0 0x805ecff0 +805ed16c 0x805ed16c +805ed2d4 Pages::LicenseChangeMii::OnClick +805ed450 Pages::LicenseChangeMii::OnMiiApprove +805ed45c Pages::LicenseChangeMii::OnMiiDisapprove +805ed468 Pages::LicenseChangeMii::OnMessageBoxClick +805ed4ec Pages::LicenseSettings::__ct +805ed70c Pages::LicenseSettings::__dt +805ed7e4 Pages::LicenseSettings::OnInit +805edbf8 Pages::LicenseSettings::OnActivate +805edc50 Pages::LicenseSettings::OnRecordsButtonClick +805edc94 Pages::LicenseSettings::OnChangeMiiButtonClick +805edcd8 Pages::LicenseSettings::OnEraseLicenseButtonClick +805edd1c Pages::LicenseSettings::OnBackPress +805edd68 Pages::LicenseSettings::OnSelectButton +805eddd4 Pages::LicenseSettings::OnBackPress +805eddf4 Pages::LicenseErase::__ct +805edf84 Pages::LicenseErase::__dt +805ee030 Pages::LicenseErase::OnInit +805ee238 Pages::LicenseErase::OnActivate +805ee244 Pages::LicenseErase::OnResume +805ee41c Pages::LicenseErase::GetNextPage +805ee434 Pages::LicenseErase::OnYesButtonClick +805ee578 Pages::LicenseErase::OnNoButtonClick +805ee5cc Pages::LicenseErase::OnBackPress +805ee5dc Pages::LicenseErase::OnErasedLicense +805ee624 Pages::LicenseErase::GetRuntimeTypeInfo +805ee630 Pages::LicenseSettings::GetNextPage +805ee638 Pages::LicenseSettings::GetRuntimeTypeInfo +805ee644 Pages::LicenseChangeMii::GetNextPage +805ee64c Pages::LicenseChangeMii::GetRuntimeTypeInfo +805ee658 Pages::LicenseSelect::GetNextPage +805ee660 Pages::LicenseSelect::GetRuntimeTypeInfo +805ee66c LicenseButton::GetRuntimeTypeInfo +805ee678 LicenseControl::GetRuntimeTypeInfo +805ee684 LicenseUI_cpp_sinit_ +805ee6b4 TriggerPtmf +805ee6e4 TriggerPtmf +805ee714 TriggerPtmf +805ee744 TriggerPtmf +805ee774 TriggerPtmf +805ee7a4 TriggerPtmf +805ee7d4 TriggerPtmf +805ee804 TriggerPtmf +805ee834 TriggerPtmf +805ee864 TriggerPtmf +805ee894 0x805ee894 +805ee8d0 MiiNameMsgPrinter::__ct +805ee900 MiiNameMsgPrinter::__dt +805ee958 MiiNameMsgPrinter::Load +805eea78 MiiNameMsgPrinter::PrintMessage +805eeb68 ManipulatorManager::__ct +805eeb8c ManipulatorManager::__dt +805eebcc ManipulatorManager::Init +805eec50 ManipulatorManager::UpdatePlayerBitfield2 +805eec58 ManipulatorManager::CheckActions +805eec5c ManipulatorManager::Activate +805eece8 ManipulatorManager::OnLoad +805eed70 ManipulatorManager::Load +805eed84 ManipulatorManager::OnReset +805eed98 ManipulatorManager::Update +805eee0c ManipulatorManager::GetControllerHolder +805eee24 ButtonInfo::__ct +805eeed0 ButtonInfo::__dt +805eef10 ButtonInfo::Reset +805eef20 ButtonInfo::Update +805ef138 ButtonInfo::Init +805ef1bc 0x805ef1bc +805ef240 PageManipulatorManager::__ct +805ef2fc PageManipulatorManager::__dt +805ef364 PageManipulatorManager::Init +805ef43c PageManipulatorManager::CheckActions +805ef5a8 PageManipulatorManager::Activate +805ef688 PageManipulatorManager::OnReset +805ef768 PageManipulatorManager::SetGlobalHandler +805ef780 ControlButtonInfo::__ct +805ef848 ControlButtonInfo::__dt +805ef888 ControlButtonInfo::Reset +805ef898 ControlButtonInfo::Update +805ef958 ControlButtonInfo::Init +805ef9ec ControlButtonInfo::LockNonDirectionalActions +805efa70 ControlButtonInfo::IsADirectionHeldForMultipleOf15 +805efadc ControlButtonInfo::GetFramesOnCurControl +805efaf8 ControlManipulator::__ct +805efbd4 ControlManipulator::__dt +805efc40 0x805efc40 +805efc48 ControlManipulator::Init +805efcf8 ControlManipulator::SetAction +805efd10 0x805efd10 +805efd14 ControlManipulator::SetEnabledHudSlots +805efd1c 0x805efd1c +805efdc8 ControlManipulator::HasDirectionalHandler +805efe40 ControlManipulator::TriggerAction +805efe94 0x805efe94 +805efef4 ControlManipulator::Load +805f0040 ControlManipulator::Update +805f03dc ControlManipulator::GetHudColor +805f0440 ControlManipulator::GetHudSlotIdColor +805f04d8 ControlManipulator::GetHudSlotIdColorSingle +805f058c ControlBoundingBox::__ct +805f05cc ControlBoundingBox::__dt +805f060c 0x805f060c +805f0614 0x805f0614 +805f0618 ControlBoundingBox::CalculateBounds +805f08a0 ControlBoundingBox::TriggerOnSelectHandler +805f08f0 ControlBoundingBox::TriggerOnDeselectHandler +805f0940 0x805f0940 +805f09a8 ControlsManipulatorManager::__ct +805f0a98 ControlManipulatorHolder::__ct +805f0b94 ControlManipulatorHolder::__dt +805f0bd4 ControlsManipulatorManager::__dt +805f0c48 ControlsManipulatorManager::Init +805f0d38 ControlManipulator::CalcDistanceBothWrapping +805f0d44 ControlsManipulatorManager::AddControlManipulator +805f0d84 ControlsManipulatorManager::SetGlobalHandler +805f0da0 ControlsManipulatorManager::SetControlManipulator +805f0db4 ControlsManipulatorManager::SetDistanceFunc +805f0e18 ControlManipulator::CalcDistanceVerticalWrappingOnly +805f0e24 ControlManipulator::CalcDistanceNoWrapping +805f0e30 ControlsManipulatorManager::GetHolderList +805f0e94 ControlsManipulatorManager::CheckActions +805f1bc0 ControlsManipulatorManager::Activate +805f1cf4 ControlsManipulatorManager::OnLoad +805f1e14 ControlsManipulatorManager::OnReset +805f1f3c ControlsManipulatorManager::OnActivate +805f1f40 ControlsManipulatorManager::Update +805f20ec ControlsManipulatorManager::SetChildControlManipulator +805f2100 ControlsManipulatorManager::InitHolders +805f2134 ControlsManipulatorManager::IsActionAllowed +805f216c ControlsManipulatorManager::HasDirectionalGlobalHandler +805f21e4 ControlsManipulatorManager::Func_805f21e4 +805f226c ControlsManipulatorManager::Func_805f226c +805f22d4 ControlsManipulatorManager::CheckPlayerPointer +805f2384 ControlManipulator::CalcDistance +805f244c ControlManipulator::CalcDistance_Switch +805f2450 ControlManipulator::CalcDistance_Case_1 +805f26a4 ControlManipulator::CalcDistance_Case_5 +805f292c ControlManipulator::CalcDistance_Case_0 +805f2948 ControlManipulator::CalcDistanceLinear +805f2ae8 ControlManipulator::CalcDistanceLinear_Switch +805f2aec ControlManipulator::CalcDistanceLinear_case_1 +805f2b3c ControlManipulator::CalcDistanceLinear_case_4 +805f2b8c ControlManipulator::CalcDistanceLinear_case_5 +805f2bec ControlManipulator::CalcDistanceLinear_case_6 +805f2c4c ControlManipulator::CalcDistanceLinear_case_0 +805f2c64 0x805f2c64 +805f2cd4 ControlsManipulatorManager:IsWiimoteCursorEnabled +805f2cdc ControlsManipulatorManager::GetRuntimeTypeInfo +805f2ce8 ManipulatorManager::IsPointerEnabled +805f2cf0 ManipulatorManager::OnActivate +805f2cf4 ManipulatorManager::GetHolderList +805f2cf8 PageManipulatorManager::GetRuntimeTypeInfo +805f2d04 0x805f2d04 +805f2d24 BackGroundModelControl::GetClassName +805f2d30 BackGroundModelControl::__ct +805f2d6c BackGroundModelControl::__dt +805f2dc4 BackGroundModelControl::Load +805f2e84 BackGroundModelControl::RequestModel +805f2ea4 BackGroundModelControl::InitSelf +805f2ea8 BackGroundModelControl::OnUpdate +805f3238 DriverModelControl::GetClassName +805f3248 DriverModelControl::__ct +805f3284 DriverModelControl::__dt +805f32dc DriverModelControl::Load +805f346c DriverModelControl::SetAnimationType +805f3494 DriverModelControl::RequestModel +805f3568 DriverModelControl::InitSelf +805f356c DriverModelControl::OnUpdate +805f37e4 DriverModelControl::SetPositionAnim +805f3830 DriverModelControl::SetModelPositionOnAnim +805f39ac VehicleModelControl::GetClassName +805f39bc VehicleModelControl::__ct +805f39f8 VehicleModelControl::__dt +805f3a50 VehicleModelControl::Load +805f3b98 VehicleModelControl::SetAnimationType +805f3bc4 VehicleModelControl::RequestNewModel +805f3c8c VehicleModelControl::InitSelf +805f3cf8 VehicleModelControl::OnUpdate +805f40a4 VehicleModelControl::SetPositionAnim +805f4110 VehicleModelControl::SetModelPositionOnAnim +805f42a4 NoteModelControl::GetClassName +805f42b4 NoteModelControl::__ct +805f42f0 NoteModelControl::__dt +805f4348 NoteModelControl::Load +805f44a0 NoteModelControl::SetAnimationType +805f44c0 NoteModelControl::InitSelf +805f45f8 NoteModelControl::OnUpdate +805f4cfc NoteModelControl::SetPositionAnim +805f4e4c NoteModelControl::SetModelPositionOnAnim +805f4f7c NoteModelControl::GetRuntimeTypeInfo +805f4f88 VehicleModelControl::GetRuntimeTypeInfo +805f4f94 DriverModelControl::GetRuntimeTypeInfo +805f4fa0 BackGroundModelControl::GetRuntimeTypeInfo +805f4fac ModelControl_cpp_sinit_ +805f4fd0 Pages::ModelRenderer::__ct +805f51c8 Pages::ModelRenderer::__dt +805f5230 Pages::ModelRenderer::OnInit +805f5504 Pages::ModelRenderer::OnDispose +805f5520 Pages::ModelRenderer::OnActivate +805f56d4 Pages::ModelRenderer::BeforeEntranceAnimations +805f56d8 Pages::ModelRenderer::BeforeExitAnimations +805f56dc Pages::ModelRenderer::BeforeControlUpdate +805f56e0 Pages::ModelRenderer::RequestCharacterModel +805f570c Pages::ModelRenderer::LoadKartModelsByCharacter +805f57b8 0x805f57b8 +805f5958 Pages::ModelRenderer::RequestKartModel +805f5984 Pages::ModelRenderer::RequestBackgroundModel +805f59ac 0x805f59ac +805f5a30 Pages::ModelRenderer::SetBackgroundModelVisibility +805f5a4c Pages::ModelRenderer::GetModelTexObj +805f5a70 Pages::ModelRenderer::GetBackgroundModelTexObj +805f5a98 Pages::ModelRenderer::GetVariantType +805f5c94 Pages::ModelRenderer::PrepareParams +805f5d58 Pages::ModelRenderer::GetModelCount +805f5e28 ModelRendererParams::Reset +805f5e44 0x805f5e44 +805f5fa4 ModelValuesHolder::GetValues +805f5fdc Pages::ModelRenderer::GetRuntimeTypeInfo +805f5fe8 ModelRendererPage_cpp_sinit_ +805f5ffc ModelRendererValuesHolder::Init +805f8b24 ModelRendererValues::SetValues +805f8b34 BMGHolder::__ct +805f8b50 BMGHolder::__dt +805f8b90 BMGHolder::Load +805f8c00 BMGHolder::Init +805f8c88 BMGHolder::GetMsgId +805f8cf0 BMGHolder::GetMessageById +805f8d2c BMGHolder::GetFontIndex +805f8d5c Pages::Click::__ct +805f8de0 Pages::Click::__dt +805f8e48 Pages::Click::OnInit +805f8eb8 Pages::Click:::OnLoad +805f8edc Pages::Click::AfterControlUpdate +805f8f40 Pages::Click::Reset +805f8f54 Pages::Click::OnClick +805f8fac Pages::MessageBoxTransparent::__ct +805f9054 Pages::MessageBoxTransparent::__dt +805f90e8 Pages::MessageBoxTransparent::OnInit +805f9224 Pages::MessageBoxTransparent::Reset +805f9244 Pages::MessageBoxTransparent::SetMessageWindowText +805f924c Pages::MessageBoxTransparent::BeforeEntranceAnimations +805f9290 Pages::MessageBox::AfterExitAnimations +805f92d4 Pages::MessageBoxTransparent::SetButtonImage +805f931c Pages::MessageBox::__ct +805f93dc Pages::MessageBox::__dt +805f9474 Pages::MessageBox::OnInit +805f95c0 Pages::MessageBox::Reset +805f9620 Pages::MessageBox::SetTitleText +805f9628 Pages::MessageBox::SetMessageWindowText +805f9630 Pages::MessageBox::SetButtonImage +805f9678 Pages::MessageBox::GetNextPage +805f9680 Pages::MessageBox::GetRuntimeTypeInfo +805f968c Pages::MessageBoxTransparent::GetRuntimeTypeInfo +805f9698 Pages::Click::GetRuntimeTypeInfo +805f96a4 Pages::Click::TriggerPtmf +805f96d4 MessagePagecpp_sinit_ +805f96f4 MessageWindowControl::GetClassName +805f9700 MessageWindowControl::__ct +805f973c MessageWindowControl::__dt +805f9794 MessageWindowControl::Load +805f9810 MessageWindowControlScaleFade::GetClassName +805f9820 MessageWindowControlScaleFade::__ct +805f985c MessageWindowControlScaleFade::__dt +805f98b8 MessageWindowControlScaleFade::SetPositionAnim +805f98f0 0x805f98f0 +805f98f4 SimpleMessageWindowControl::GetClassName +805f9900 SimpleMessageWindowControl::__ct +805f993c SimpleMessageWindowControl::__dt +805f9994 SimpleMessageWindowControl::Load +805f9a48 SimpleMessageWindowControl::InitSelf +805f9aac BlackBackControlForMessage::GetClassName +805f9abc BlackBackControlForMessage::__ct +805f9af8 BlackBackControlForMessage::__dt +805f9b50 BlackBackControlForMessage::Load +805f9bc4 BlackBackControlForMessage::SetPositionAnim +805f9c24 BusySymbol::GetClassName +805f9c34 BusySymbol::__ct +805f9c70 BusySymbol::__dt +805f9cc8 BusySymbol::Load +805f9d5c BusySymbol::InitSelf +805f9d94 BusySymbol::SetPositionAnim +805f9df4 BusySymbol::OnUpdate +805f9e10 BusySymbol::GetRuntimeTypeInfo +805f9e1c BlackBackControlForMessage::GetRuntimeTypeInfo +805f9e28 SimpleMessageWindowControl::GetRuntimeTypeInfo +805f9e34 MessageWindowControlScaleFade::GetRuntimeTypeInfo +805f9e40 MessageWindowControl::GetRuntimeTypeInfo +805f9e4c 0x805f9e4c +805f9e78 MiiGroup::__ct +805f9f18 MiiCreationParams2::__dt +805f9f58 MiiGroup::__dt +805fa04c MiiGroup::Init +805fa370 MiiGroup::Reset +805fa3c4 MiiGroup::ResetCreationParams +805fa3fc MiiGroup::AddMii +805fa580 MiiGroup::LoadMiddleDB2Mii +805fa610 MiiGroup::LoadMii(RFLStoreData) +805fa678 MiiGroup::LoadOfficialMii +805fa6e0 MiiGroup::LoadStoreMii +805fa748 MiiGroup::LoadDefaultMii +805fa7b0 0x805fa7b0 +805fa8b8 MiiGroup::ReplaceMiiByPlayerMii +805fa930 MiiGroup::GetMii +805fa940 0x805fa940 +805fa964 MiiGroup::GetMiiTexObj +805fa97c MiiGroup::HasAllMiiTexObjs +805faa84 MiiGroup::DeleteMii +805faba8 0x805faba8 +805fabf4 MiiGroup::CopyMii +805faf34 MiiGroup::CopyMiiToOtherGroup +805fb38c 0x805fb38c +805fb4cc 0x805fb4cc +805fb504 0x805fb504 +805fb53c 0x805fb53c +805fb558 0x805fb558 +805fb5b8 0x805fb5b8 +805fb5bc Pages::BackModel::__ct +805fb610 Pages::BackModel::__dt +805fb690 Pages::BackModel::OnInit +805fb7ac Pages::BackModel::OnInit_switch +805fb7b0 Pages::BackModel::OnInit_case_60 +805fb7c0 Pages::BackModel::OnInit_case_49 +805fb7d0 Pages::BackModel::OnInit_case_4b +805fb7e0 Pages::BackModel::OnInit_case_4c +805fb7ec Pages::BackModel::OnInit_case_4d +805fb800 Pages::BackModel::GetRuntimeTypeInfo +805fb80c BackModelPage_cpp_sinit_ +805fb820 MoviePaneHandler::__ct +805fb850 MoviePaneHandler::__dt +805fb890 MoviePaneHandler::Init +805fb99c MoviePaneHandler::Crop(Rect*) +805fba68 MoviePaneHandler::Crop(4floats) +805fbb60 MoviePaneHandler::Draw +805fcd40 MoviePlayer::__ct +805fcd90 MoviePlayer::__dt +805fcde8 MoviePlayer::Init +805fcdec MoviePlayer::Quit +805fce7c MoviePlayer::Update +805fd0a8 MoviePlayer::Prepare +805fd31c 0x805fd31c +805fd398 MoviePlayer::Close +805fd3e0 MoviePlayer::GetCurTHPTextureInfo +805fd3fc MoviePlayer::IsPlaying +805fd438 MoviePlayer::Pause +805fd454 MoviePlayer::Play +805fd470 MoviePlayer::GetCurTextureIdx +805fd478 MoviePlayer::Reprepare +805fd4c4 0x805fd4c4 +805fd518 Pages::Options::__ct +805fd638 Pages::Options::__dt +805fd6e4 Pages::Options::OnInit +805fd8e4 Pages::Options::OnDispose +805fd8e8 Pages::Options::OnActivate +805fd90c Pages::Options::OnButtonClick +805fda80 Pages::Options::OnBackPress +805fdaa8 Pages::WifiOptions::__ct +805fdbd8 Pages::WifiOptions::__dt +805fdc9c Pages::WifiOptions::OnInit +805fdf34 Pages::WifiOptions::OnActivate +805fdf58 Pages::WifiOptions::OnButtonClick +805fe110 Pages::WifiOptions::OnBackPress +805fe128 Pages::OptionExplanation::__ct +805fe240 Pages::OptionExplanation::__dt +805fe2e0 Pages::OptionExplanation::OnInit +805fe460 Pages::OptionExplanation::OnActivate +805fe48c Pages::OptionExplanation::OnButtonClick +805fe57c Pages::OptionExplanation::OnBackPress +805fe5f8 Pages::OptionChoice::__ct +805fe728 Pages::OptionChoice::__dt +805fe7d4 Pages::OptionChoice::OnInit +805fea58 Pages::OptionChoice::OnActivate +805fea88 Pages::OptionChoice::OnResume +805feb60 Pages::OptionChoice::SetTitleText +805feb6c Pages::OptionChoice::SetMessageWindowText +805feb78 Pages::OptionChoice::SetButtonText +805febdc Pages::OptionChoice::SetInitialButton +805febe4 Pages::OptionChoice::OnButtonClick +805fed8c Pages::OptionChoice::OnBackPress +805feda4 Pages::RegionOptions::__ct +805feefc Pages::RegionOptions::__dt +805fef9c Pages::RegionOptions::OnInit +805ff128 Pages::RegionOptions::OnActivate +805ff134 Pages::RegionOptions::GetNextPage +805ff194 Pages::RegionOptions::OnButtonClick +805ff1e0 Pages::RegionOptions::OnBackButtonClick +805ff2a0 Pages::RegionOptions::OnBackPress +805ff328 Pages::MasterOptions::__ct +805ff3a4 Pages::MasterOptions::__dt +805ff40c Pages::MasterOptions::OnInit +805ff478 Pages::MasterOptions::OnActivate +805ff4d8 Pages::MasterOptions::BeforeEntranceAnimations +805ff4dc Pages::MasterOptions::BeforeExitAnimations +805ff4e0 Pages::MasterOptions::AfterControlUpdate +805ff6c4 Pages::MasterOptions::OnResume +805ff700 Pages::MasterOptions::OnResume_switch +805ff704 Pages::MasterOptions::OnResume_case0 +805ff8a8 Pages::MasterOptions::OnResume_case1 +805ff948 Pages::MasterOptions::OnResume_case2 +80600038 Pages::MasterOptions::OnResume_case4 +80600060 Pages::MasterOptions::OnResume_case5 +80600068 Pages::MasterOptions::OnResume_case7 +80600090 Pages::MasterOptions::OnResume_case8 +80600098 Pages::MasterOptions::OnResume_case9 +806000a8 Pages::MasterOptions::OnResume_case3 +806000c4 Pages::MasterOptions::SetNextPageType +80600128 Pages::MasterOptions::LoadOptionFirstExplanation +806002e4 Pages::MasterOptions::LoadOptionNextExplanation +806003e0 Pages::MasterOptions::LoadOptionChoice +8060079c Pages::MasterOptions::LoadOptionWaitingNetwork +806008cc Pages::MasterOptions::LoadOptionWaiting +806009fc Pages::MasterOptions::LoadOptionChoiceSaved +80600dc0 Pages::MasterOptions::LoadOptionChoiceSavedNetwork +80600e90 Pages::MasterOptions::LoadOptionChoiceSavedNetwork_switch +80600e94 Pages::MasterOptions::LoadOptionChoiceSavedNetwork_case1 +80600e9c Pages::MasterOptions::LoadOptionChoiceSavedNetwork_case2 +80600ea4 Pages::MasterOptions::LoadOptionChoiceSavedNetwork_case3 +80600eac Pages::MasterOptions::LoadOptionChoiceSavedNetwork_case4 +80600eb4 Pages::MasterOptions::LoadOptionChoiceSavedNetwork_case5 +80600ebc Pages::MasterOptions::LoadOptionChoiceSavedNetwork_case6 +80600ec4 Pages::MasterOptions::LoadOptionChoiceSavedNetwork_case8 +80600ecc Pages::MasterOptions::LoadOptionChoiceSavedNetwork_case9 +80600ed4 Pages::MasterOptions::LoadOptionChoiceSavedNetwork_case7 +80600efc Pages::MasterOptions::LoadOptionChoiceSavedNetwork_case0 +80600f88 Pages::MasterOptions::OnClick +80601014 Pages::MasterOptions::GetNextPage +8060101c Pages::MasterOptions::GetRuntimeTypeInfo +80601028 Pages::RegionOptions::GetRuntimeTypeInfo +80601034 Pages::OptionChoice::GetRuntimeTypeInfo +80601040 Pages::OptionExplanation::GetRuntimeTypeInfo +8060104c Pages::WifiOptions::GetNextPage +80601054 Pages::WifiOptions::GetRuntimeTypeInfo +80601060 Pages::Options::GetNextPage +80601068 Pages::Options::GetRuntimeTypeInfo +80601074 Pages::RegionOptions::TriggerPtmf +806010a4 Pages::OptionChoice::TriggerPtmf +806010d4 Pages::OptionExplanation::TriggerPtmf +80601104 Pages::WifiOptions::TriggerPtmf +80601134 Pages::Options::TriggerPtmf +80601164 Pages::MasterOptions::TriggerPtmf +80601194 Pages::RegionOptions::TriggerBackPtmf +806011c4 Pages::OptionChoice::TriggerBackPtmf +806011f4 Pages::OptionExplanation::TriggerBackPtmf +80601224 Pages::WifiOptions::TriggerBackPtmf +80601254 Pages::Options::TriggerBackPtmf +80601284 Optionscpp_sinit_ +806012b0 PadControl::GetClassName +806012bc PadControl::__ct +806012f8 PadControl::__dt +80601350 PadControl::Load +80601410 PadControl::InitSelf +8060147c PadControl::OnUpdate +80601554 PadControl::SetParams +80601564 PadControl::SetPadID +8060156c PadControl::RegisterPad +806015a4 PadControl::UpdateFromParams +806016fc 0x806016fc +8060170c 0x8060170c +80601748 0x80601748 +806017a4 0x806017a4 +806017d4 RecognizePadWindow::GetClassName +806017e4 RecognizePadWindow::__ct +80601820 RecognizePadWindow::__dt +80601878 RecognizePadWindow::Load +806018f4 RecognizePadWindow::InitSelf +806018f8 RecognizePadWindow::OnUpdate +806018fc RecognizePadWindow::SetPositionAnim +80601934 RecognizePadWindow::GetRuntimeTypeInfo +80601940 0x80601940 +8060194c PadControl::GetRuntimeTypeInfo +80601958 PadControl_sinit_ +8060197c Page::__ct +80601a04 Page::__dt +80601a60 Page::Init +80601ad8 Page::OnInit +80601adc Page::Dispose +80601aec Page::Activate +80601c08 Page::Deactivate +80601c48 Page::IncrementDuration +80601c64 Page::CheckActions +80601d24 Page::UpdateState +80601f44 Page::Exit +80602144 Page::Enter +806022cc Page::UpdateControls +80602338 Page::Update +8060235c Page::Draw +80602378 Page::ResetManipulatorManager +8060238c 0x8060238c +80602390 Page::Resume +80602428 Page::HandleSectionChange +8060244c Page::SetAnimDirection +8060245c Page::InitControlGroup +8060246c Page::AddControl +80602474 Page::SetManipulatorManager +8060247c Page::EndState +80602488 Page::EndStateAnimated +806024a4 Page::EndExitState +806024b0 Page::ChangeSectionBySceneChange +80602530 Page::ChangeSectionByReinit +806025b0 Page::AddPageLayer +8060265c Page::AnimateControls +806027ac Page::ControlAnimator::Calc +806028cc Pages::System::GetAnimDelay +80602920 Page::MaxAnimDelayGetter::Calc +80602960 Page::OnStateChangeAnimator::Calc +80602970 Page::StartEntrance +806029f4 Page::EndEntrance +80602b04 Page::StartExit +80602c60 Page::EndExit +80602ce8 Page::80602ce8 +80602cf8 Page::PlaySound +80602d0c 0x80602d0c +80602d20 Page::SetTransitionSound +80602d2c Pages::System::__ct +80602dc0 Pages::System::__dt +80602e54 Pages::System::Setup +80602eec Pages::System::CheckForConditions +80602ef4 SystemPagecpp_sinit_ +80602f04 Pages::RegisteredPads::__ct +80602f68 Pages::RegisteredPads::__dt +80602fe8 Pages::RegisteredPads::OnInit +806030dc Pages::RegisteredPads::OnActivate +80603250 Pages::RegisteredPads::BeforeEntranceAnimations +80603254 Pages::RegisteredPads::AfterEntranceAnimations +80603258 Pages::RegisteredPads::AfterControlUpdate +80603304 Pages::RegisteredPads::AfterExitAnimations +80603308 Pages::RegisteredPads::GetNextPage +80603350 Pages::RegisteredPads::Register +806033b8 ConnectController::GetClassName +806033c8 ConnectController::__ct +80603404 ConnectController::__dt +8060345c ConnectController::Load +806035d8 ConnectController::InitSelf +80603690 ConnectController::OnUpdate +80603764 Pages::RegisterPad::__ct +80603890 CtrlMenuDummyBack::__dt +806038e8 Pages::RegisterPad::__dt +806039a4 Pages::RegisterPad::OnInit +80603b90 Pages::RegisterPad::OnActivate +80603ba4 Pages::RegisterPad::AfterControlUpdate +80603f10 Pages::RegisterPad::GetNextPage +80603f28 Pages::RegisterPad::IsDVDEnabled +80603f70 Pages::RegisterPad::OnBackPress +80603fbc Pages::RegisterPad::OnStartPress +80604074 EasyPairingController::GetClassName +80604084 EasyPairingController::__ct +806040c0 EasyPairingController::__dt +80604118 EasyPairingController::Load +806042a8 EasyPairingController::InitSelf +806043b8 EasyPairingController::OnUpdate +80604518 0x80604518 +80604584 Pages::PairWiimotes::__ct +80604638 Pages::PairWiimotes::__dt +806046dc Pages::PairWiimotes::OnInit +80604940 Pages::PairWiimotes::OnActivate +806049c0 Pages::PairWiimotes::AfterEntranceAnimations +806049cc Pages::PairWiimotes::BeforeEntranceAnimations +806049d8 Pages::PairWiimotes::BeforeExitAnimations +806049e4 Pages::PairWiimotes::OnDeactivate +80604a88 Pages::PairWiimotes::AfterControlUpdate +80604b00 Pages::PairWiimotes::SyncWiimotes +80604c84 Pages::PairWiimotes::OnBackAndStartPress +80604cd8 Pages::ConfirmPads::__ct +80604ea0 Pages::ConfirmPads::__dt +80604f50 Pages::ConfirmPads::OnInit +80605198 Pages::ConfirmPads::OnActivate +806051e4 Pages::ConfirmPads::OnDeactivate +80605204 Pages::ConfirmPads::OnOkButtonClick +80605260 Pages::ConfirmPads::OnChangeButtonClick +80605340 Pages::ConfirmPads::OnBackButtonClick +80605470 Pages::ConfirmPads::OnButtonSelect +806054c4 Pages::ConfirmPads::OnBackPress +806055cc Pages::ConfirmPads::GetNextPage +806055d4 Pages::ConfirmPads::GetRuntimeTypeInfo +806055e0 Pages::PairWiimotes::GetRuntimeTypeInfo +806055ec EasyPairingController::GetRuntimeTypeInfo +806055f8 Pages::RegisterPad::IsHomeMenuWorking +80605600 Pages::RegisterPad::GetRuntimeTypeInfo +8060560c ConnectController::GetRuntimeTypeInfo +80605618 Pages::RegisterPad::GetRuntimeTypeInfo +80605624 TriggerPtmf +80605654 TriggerPtmf +80605684 TriggerPtmf +806056b4 TriggerPtmf +806056e4 RegisterPadPage_cpp_sinit_ +80605718 RadioButtonControl::GetClassName +80605724 RadioButtonControl::__ct +806058c8 RadioButtonControl::__dt +8060594c RadioButtonControl::Load +80605d50 RadioButtonControl::OnLoadingEnd +80605d54 RadioButtonControl::SetOnClickHandler +80605d5c RadioButtonControl::SetOnSelectHandler +80605d64 RadioButtonControl::SetOnDeselectHandler +80605d6c RadioButtonControl::SetOnChangeHandler +80605d74 RadioButtonControl::SelectInitialButton +80605dfc 0x80605dfc +80605e1c 0x80605e1c +80605eb4 0x80605eb4 +80605f24 RadioButtonControl::Init +80606100 RadioButtonControl::Update +806062d8 RadioButtonControl::HandleSelect +80606434 RadioButtonControl::HandleDeselect +80606568 RadioButtonControl::HandleButtonSelect +80606620 RadioButtonControl::HandleClick +806066d4 RadioButtonControl::HandleRightPress +80606790 RadioButtonControl::HandleLeftPress +80606848 RadioButtonControl::OptionButton::GetClassName +80606858 RadioButtonControl::OptionButton::__ct +80606894 RadioButtonControl::OptionButton::__dt +806068ec RadioButtonControl::OptionButton::Load +80606994 RadioButtonControl::OptionButton::Init +80606a14 RadioButtonControl::OptionButton::Update +80606ad0 RadioButtonControl::OptionButton::GetParentControl +80606bd0 GetInstanceUnusedControl? +80606bdc GetInstanceUnusedControl? +80606be8 0x80606be8 +80606c48 RadioButtonControl::OptionButton::ToggleChose +80606dc4 0x80606dc4 +80606e24 RadioButtonControl::OptionButton::ToggleSelect +80606f88 RadioButtonControl::OptionButton::SetBorderColours +8060703c RadioButtonControl::OptionButton::SetColor_BaseColours +806070e4 RadioButtonControl::Func_0x40 +806070e8 RadioButtonControl::GetRuntimeTypeInfo +806070f4 RadioButtonControl::OptionButton::GetRuntimeTypeInfo +80607100 RadioButtonControl_cpp_sinit_ +80607130 RadioButtonControl_cpp_sinit_ +8060714c RandomMatchingPlayer::GetClassName +80607158 RandomMatchingPlayer::__ct +806071cc RandomMatchingPlayer::__dt +80607224 RandomMatchingPlayer::Load +80607368 RandomMatchingPlayer::InitSelf +806074c0 RandomMatchingPlayer::OnUpdate +80607730 0x80607730 +8060786c 0x8060786c +80607924 0x80607924 +806079bc 0x806079bc +80607b40 0x80607b40 +80607b58 RandomMatchingPlayer::SetMii +80607be4 RandomMatchingPlayer::IsShowing +80607c1c RandomMatchingPlayer::OnMiiShowing +80607cdc RandomMatchingPlayer::CalcAngle +80607d2c 0x80607d2c +80607d88 MatchingMessageWindow::GetClassName +80607d98 MatchingMessageWindow::__ct +80607dd4 MatchingMessageWindow::__dt +80607e2c MatchingMessageWindow::Load +80607e90 MatchingMessageWindow::InitSelf +80607ef4 MatchingMessageWindow::OnUpdate +80607ef8 MatchingMessageWindow::SetMessage +80607fb0 MatchingMessageWindow::Hide +80608060 Pages::GlobeSearch::__ct +80608194 Pages::GlobeSearch::__dt +80608268 Pages::GlobeSearch::OnInit +80608494 Pages::GlobeSearch::OnActivate +80608d18 Pages::GlobeSearch::BeforeEntranceAnimations +80608d1c Pages::GlobeSearch::BeforeExitAnimations +80608d6c Pages::GlobeSearch::BeforeControlUpdate +806092c0 Pages::GlobeSearch::AfterControlUpdate +80609a30 Pages::GlobeSearch::OnResume +80609cb8 0x80609cb8 +80609cf8 0x80609cf8 +80609e84 Pages::GlobeSearch::PrepareLiveView +8060a40c 0x8060a40c +8060a658 0x8060a658 +8060a670 Pages::GlobeSearch::GetNextPage +8060a678 Pages::GlobeSearch::IsHomeMenuWorking +8060a680 Pages::GlobeSearch::GetRuntimeTypeInfo +8060a68c MatchingMessageWindow::GetRuntimeTypeInfo +8060a698 RandomMatchingPlayer::GetRuntimeTypeInfo +8060a6a4 TriggerPtmf +8060a6d4 TriggerPtmf +8060a704 __sinit_RandomMatchingPage_cpp +8060a72c RankingDetailItem_getTypeName +8060a738 RankingDetailItem_ct +8060a774 RankingDetailPage_dt +8060a7cc 0x8060a7cc +8060a8f0 0x8060a8f0 +8060a928 0x8060a928 +8060aad4 0x8060aad4 +8060ad38 RankingDetailPage_ct +8060ae60 0x8060ae60 +8060af14 0x8060af14 +8060b1b8 0x8060b1b8 +8060b1fc 0x8060b1fc +8060b200 0x8060b200 +8060b308 0x8060b308 +8060b5ec 0x8060b5ec +8060b610 0x8060b610 +8060b650 0x8060b650 +8060b73c 0x8060b73c +8060b79c 0x8060b79c +8060b82c 0x8060b82c +8060bb58 0x8060bb58 +8060bcdc 0x8060bcdc +8060bd18 0x8060bd18 +8060bd28 0x8060bd28 +8060bd8c 0x8060bd8c +8060bdb0 0x8060bdb0 +8060bdf0 0x8060bdf0 +8060be58 0x8060be58 +8060bee0 0x8060bee0 +8060bf90 0x8060bf90 +8060c0dc 0x8060c0dc +8060c234 0x8060c234 +8060c50c 0x8060c50c +8060c53c 0x8060c53c +8060c548 0x8060c548 +8060c7a4 0x8060c7a4 +8060cbbc 0x8060cbbc +8060cf6c 0x8060cf6c +8060d084 0x8060d084 +8060d1e8 0x8060d1e8 +8060d1f4 0x8060d1f4 +8060d200 0x8060d200 +8060d20c TriggerPtmf +8060d23c TriggerPtmf +8060d26c 0x8060d26c +8060d294 0x8060d294 +8060d40c 0x8060d40c +8060d4b4 0x8060d4b4 +8060d694 0x8060d694 +8060d6a8 0x8060d6a8 +8060d6ac 0x8060d6ac +8060d7ac 0x8060d7ac +8060d800 0x8060d800 +8060d9f4 0x8060d9f4 +8060da38 0x8060da38 +8060da50 0x8060da50 +8060dc20 0x8060dc20 +8060dca4 0x8060dca4 +8060dd0c 0x8060dd0c +8060dd78 0x8060dd78 +8060dd88 0x8060dd88 +8060dd94 0x8060dd94 +8060dd9c 0x8060dd9c +8060e564 0x8060e564 +8060e59c 0x8060e564_switch +8060e5a0 0x8060e564_caseD_0 +8060e5a8 0x8060e564_caseD_1 +8060e74c 0x8060e564_caseD_2 +8060e820 0x8060e564_caseD_3 +8060e8cc 0x8060e564_caseD_4 +8060e8d4 0x8060e564_caseD_5 +8060e9b8 0x8060e564_caseD_6 +8060eb0c 0x8060e564_caseD_7 +8060ec00 0x8060e564_caseD_8 +8060ee50 0x8060e564_caseD_b +8060ee58 0x8060e564_caseD_c +8060ee60 0x8060e564_caseD_d +8060ef2c 0x8060e564_caseD_10 +8060ef44 0x8060e564_caseD_9 +8060ef5c 0x8060ef5c +80610480 0x80610480 +8061055c 0x8061055c +80610668 0x80610668 +80610794 0x80610794 +80610d68 0x80610d68 +806111a0 0x806111a0 +806112a4 0x806112a4 +806113bc 0x806113bc +8061157c 0x8061157c +8061164c 0x8061157c_switch +80611650 0x8061157c_caseD_1 +80611658 0x8061157c_caseD_2 +80611660 0x8061157c_caseD_3 +80611668 0x8061157c_caseD_4 +80611670 0x8061157c_caseD_5 +80611678 0x8061157c_caseD_6 +80611680 0x8061157c_caseD_8 +80611688 0x8061157c_caseD_9 +80611690 0x8061157c_caseD_7 +806116c0 0x8061157c_caseD_0 +80611734 0x80611734 +806117a0 0x806117a0 +806117a8 0x806117a8 +806117b4 0x806117b4 +806117c0 TriggerPtmf +806117f0 TriggerPtmf +80611820 TriggerPtmf +80611850 0x80611850 +8061186c RankingGraphControl::GetClassName +80611878 RankingGraphControl::__ct +806118f0 RankingGraphControl::__dt +80611990 RankingGraphControl::LoadAssets +80611a18 0x80611a18 +80611a3c RankingGraphControl::UpdateImpl +8061214c 0x8061214c +806121e8 0x806121e8 +80612260 0x80612260 +806122d8 0x806122d8 +80612320 0x80612320 +80612370 0x80612370 +806123b8 RankingGraphControlTimeAttack::GetClassName +806123c8 RankingGraphControlTimeAttack::__ct +8061250c RankingGraphControlTimeAttack::__dt +8061267c RankingGraphControlTimeAttack::Load +80612a68 0x80612a68 +80612a8c 0x80612a8c +80612a90 RankingGraphControlTimeAttack::GetWindow +80612afc 0x80612afc +80612c3c RankingGraphControlTimeAttack::DisplayItems +80612c9c RankingGraphControlTimeAttack::DisableAllItems +80612dbc RankingGraphControlTimeAttack:: DisplayItemsOnlineRankings +80613864 0x80613864 +806138a4 RankingGraphControlTimeAttack:: DisplayItemsFriendsRankings +80613c90 0x80613c90 +80613ca0 0x80613ca0 +80613dc0 0x80613dc0 +80613efc 0x80613efc +80614224 0x80614224 +80614248 0x80614248 +8061424c 0x8061424c +80614298 0x80614298 +806142f8 0x806142f8 +806143dc 0x806143dc +80614c5c 0x80614c5c +80614ffc 0x80614ffc +80615008 RankingGraphControlTimeAttack::GetRuntimeTypeInfo +80615014 RankingGraphControl::GetRuntimeTypeInfo +80615020 0x80615020 +80615044 0x80615044 +806150b0 0x806150b0 +80615134 0x80615134 +8061525c 0x8061525c +8061526c 0x8061526c +806152a8 0x806152a8 +80615300 0x80615300 +80615378 0x80615378 +806153b0 0x806153b0 +806153b4 Pages::RankingGraph::__ct +80615650 Pages::RankingGraph::__dt +80615738 Pages::RankingGraph::OnInit +80615bd8 Pages::RankingGraph::OnDispose +80615c8c Pages::RankingGraph::OnActivate +80615e50 0x80615e50 +80615f4c Pages::RankingGraph::OnTAUpDownChange +80615f68 Pages::RankingGraph::OnTAUpDownClick +80615fb4 Pages::RankingGraph::OnTATextChange +80616038 Pages::RankingGraph::OnCompetitionUpDownChange +80616054 Pages::RankingGraph::OnCompetitionTextChange +80616180 Pages::RankingGraph::OnRankingTypeUpDownChange +806162a4 Pages::RankingGraph::OnRankingTypeTextChange +806162b4 Pages::RankingGraph::OnBackButtonClick +80616300 Pages::RankingGraph::OnBackPress +80616320 Pages::RankingGraph::SetBottomText +80616328 0x80616328 +80616458 0x80616458 +80616538 0x80616538 +806165bc 0x806165bc +806165cc 0x806165cc +806165dc 0x806165dc +80616700 Pages::RankingGraph::GetNextPage +80616708 Pages::RankingGraph::GetRuntimeTypeInfo +80616714 0x80616714 +80616720 0x80616720 +8061672c TriggerPtmf +8061675c TriggerPtmf +8061678c TriggerPtmf +806167bc TriggerPtmf +806167ec TriggerPtmf +8061681c 0x8061681c +80616844 0x80616844 +80616994 RankingItem::GetClassName +806169a0 0x806169a0 +806169f0 RankingItem::__dt +80616a48 RankingItem::FinishLoad +80616c24 RankingItem::OnUpdate +806170b0 0x806170b0 +806170c4 RankingItem::Disable +806170d8 RankingItem::Enable +806170ec RankingItem::Appear +806171f0 RankingItem::MakeAccessible +806171f4 RankingItem::Disappear +80617224 0x80617224 +80617280 0x80617280 +806173a8 RankingItemSelectable::GetClassName +806173b8 RankingItemSelectable::__ct +806174cc RankingItemSelectable::__dt +8061753c RankingItemSelectable::Load +80617580 RankingItemSelectable::Load_switch +80617584 RankingItemSelectable::Load_caseD_0 +80617594 RankingItemSelectable::Load_caseD_1 +806175a4 RankingItemSelectable::Load_caseD_2 +806175b4 RankingItemSelectable::Load_caseD_3 +806175c4 RankingItemSelectable::Load_caseD_4 +806175d4 RankingItemSelectable::Load_caseD_5 +806175e4 RankingItemSelectable::Load_caseD_6 +806175f4 RankingItemSelectable::Load_caseD_7 +8061799c RankingItemSelectable::InitSelf +80617a30 RankingItemSelectable::Disable +80617a4c RankingItemSelectable::Enable +80617f64 RankingItemSelectable::MakeAccessible +80617f80 RankingItemSelectable::Disappear +80617fb8 RankingItemSelectable::vf_0x50 +80617fd4 RankingItemSelectable::vf_0x54 +80617fec RankingItemSelectable::vf_0x58 +80618004 RankingItemSelectable::vf_0x5c +80618020 RankingItemSelectable::vf_0x60 +806182ec RankingItemSelectable::vf_0x64 +806182f0 RankedItemSelectable::OnSelect +80618928 RankedItemSelectable::OnSelect_switch +8061892c RankedItemSelectable::OnSelect_caseD_0 +80618934 RankedItemSelectable::OnSelect_caseD_1 +8061893c RankedItemSelectable::OnSelect_caseD_3 +80618968 RankedItemSelectable::OnSelect_caseD_5 +80618970 RankedItemSelectable::OnSelect_caseD_6 +806189ac RankedItemSelectable::OnSelect_caseD_7 +80618aec RankedItemSelectable::OnDeselect +80618c50 RankedItemSelectable::OnClick +80618e94 0x80618e94 +80618ebc 0x80618e94_switch +80618ec0 0x80618e94_caseD_0 +80618ec8 0x80618e94_caseD_1 +80618ed0 0x80618e94_caseD_2 +80618ed8 0x80618e94_caseD_3 +80618ee0 0x80618e94_caseD_4 +80618ee8 0x80618e94_caseD_5 +80618ef0 0x80618e94_caseD_6 +80618ef8 0x80618e94_caseD_7 +80618f1c RankingItemStranger::GetClassName +80618f2c RankingItemStranger::__ct +80618f88 RankingItemStranger::__dt +80618fe4 RankingItemStranger::Load +8061907c RankingItemStranger::Enable +806190ac RankingItemStranger::vf_0x50 +806190c8 RankingItemStranger::vf_0x54 +806190e0 RankingItemStranger::vf_0x58 +806190f8 RankingItemStranger::vf_0x5c +806190fc RankingItemStranger::vf_0x60 +80619100 RankingItemStranger::vf_0x64 +80619104 RankingRuler::GetClassName +80619114 RankingRuler::__ct +80619150 RankingRuler::__dt +806191a8 RankingRuler::Load +8061921c RankingRuler::InitSelf +80619228 0x80619228 +8061936c 0x8061936c +80619418 0x80619418 +80619424 RankingWindow::GetClassName +80619434 RankingWindow::__ct +80619470 RankingWindow::__dt +806194c8 0x806194c8 +80619638 RankingWindow::InitSelf +80619644 RankingWindow::OnUpdate +806196c4 0x806196c4 +806196d0 0x806196d0 +806198fc RankingWindow::GetRuntimeTypeInfo +80619908 RankingRuler::GetRuntimeTypeInfo +80619914 RankingItemStranger::GetRuntimeTypeInfo +80619920 RankingItemSelectable::GetRuntimeTypeInfo +8061992c RankingItem::GetRuntimeTypeInfo +80619938 TriggerPtmf +80619968 0x80619968 +806199bc 0x806199bc +806199fc 0x806199fc +80619a58 0x80619a58 +80619a68 0x80619a68 +80619c50 GLMeshWriterCore_ColourPlat +80619c70 0x80619c70 +80619cbc 0x80619cbc +80619f5c 0x80619f5c +80619f78 0x80619f78 +80619fe8 0x80619fe8 +8061a23c 0x8061a23c +8061a2bc 0x8061a2bc +8061a32c 0x8061a32c +8061a41c 0x8061a41c +8061a464 0x8061a464 +8061a504 0x8061a504 +8061a5a4 0x8061a5a4 +8061a5f0 0x8061a5f0 +8061a650 0x8061a650 +8061a878 0x8061a878 +8061ab1c 0x8061ab1c +8061aba0 0x8061aba0 +8061abec 0x8061abec +8061ac4c 0x8061ac4c +8061adfc 0x8061adfc +8061ae6c SectionPad::__ct +8061ae7c SectionPad::__dt +8061aebc SectionPad::Init +8061af20 SectionPad::Update +8061b338 SectionPad::ResetPadsStatusAndSet0x54 +8061b358 SectionPad::GetCurrentID +8061b378 SectionPad::GetNewID +8061b398 SectionPad::GetControllerHolder +8061b3cc SectionPad::HasAController +8061b420 SectionPad::SetDriftType +8061b440 SectionPad::IsDriftAuto +8061b470 0x8061b470 +8061b490 SectionPad::SetID +8061b4a8 0x8061b4a8 +8061b5a4 0x8061b5a4 +8061b6ec SectionPad::RequestPad +8061b7c4 SectionPad::ResetPad +8061b89c SectionPad::SetPadDisconnected +8061b8b0 SectionPad::DisconnectPad +8061b9a0 SectionPad::ResetPadsStatus +8061b9bc SectionPad::GetFirstDeactivatedPad +8061ba48 SectionPad::HasControllerChanged +8061ba80 0x8061ba80 +8061bc54 SectionPad::Has4WiimotePads +8061bcc8 SectionPad::ResetAllPads +8061bd14 SectionPad::EnablePointer +8061bdbc SectionPad::GetControllerHolderByIdx +8061be40 SectionPad::CalculateID +8061bfac SectionPad::GetType +8061bffc Pages::RecognizePad::__ct +8061c058 Pages::RecognizePad::__dt +8061c0d8 Pages::RecognizePad::OnInit +8061c1c0 Pages::RecognizePad::OnActivate +8061c288 Pages::RecognizePad::OnDeactivate +8061c2e4 Pages::RecognizePad::AfterControlUpdate +8061c384 Pages::RecognizePad::BeforeEntranceAnimations +8061c3c8 Pages::RecognizePad::AfterExitAnimations +8061c40c Pages::RecognizePad::CheckForConditions +8061c518 Pages::RecognizePad::IsVisible +8061c53c Pages::RecognizePadMenu::__ct +8061c5a4 Pages::RecognizePadMenu::__dt +8061c628 Pages::RecognizePadMenu::BeforeEntranceAnimations +8061c720 Pages::RecognizePadMenu::AfterEntranceAnimations +8061c76c Pages::RecognizePadMenu::BeforeExitAnimations +8061c780 Pages::RecognizePadMenu::AfterExitAnimations +8061c7c4 Pages::RecognizePadMenu::BeforeControlUpdate +8061c86c Pages::RecognizePadMenu::IsDVDEnabled +8061c880 0x8061c880 +8061c8e8 0x8061c8e8 +8061c96c 0x8061c96c +8061ca40 0x8061ca40 +8061caa8 0x8061caa8 +8061cb94 0x8061cb94 +8061cbe0 0x8061cbe0 +8061cbf4 0x8061cbf4 +8061cc38 0x8061cc38 +8061cd24 0x8061cd24 +8061cd40 0x8061cd40 +8061cda8 0x8061cda8 +8061ce2c 0x8061ce2c +8061cf00 0x8061cf00 +8061cfec 0x8061cfec +8061d038 0x8061d038 +8061d04c 0x8061d04c +8061d090 0x8061d090 +8061d17c 0x8061d17c +8061d198 0x8061d198 +8061d1a4 0x8061d1a4 +8061d1b0 0x8061d1b0 +8061d1bc 0x8061d1bc +8061d1c8 Pages::RecognizePadMenu::GetClassName +8061d1d4 Pages::RecognizePadMenu::GetRuntimeTypeInfo +8061d1e0 Pages::RecognizePad::GetRuntimeTypeInfo +8061d1ec 0x8061d1ec +8061d214 Pages::RegisteringFriend::__ct +8061d288 Pages::RegisteringFriend::__dt +8061d300 Pages::RegisteringFriend::OnInit +8061d3a8 Pages::RegisteringFriend::OnActivate +8061d4c8 Pages::RegisteringFriend::BeforeEntranceAnimations +8061d4cc Pages::RegisteringFriend::BeforeExitAnimations +8061d4d0 Pages::RegisteringFriend::AfterControlUpdate +8061d628 Pages::RegisteringFriend::OnResume +8061dc40 Pages::RegisteringFriend::GetNextPage +8061dc9c Pages::RegisteringFriend::IsHomeMenuWorking +8061dcb8 Pages::RegisterFriend::__ct +8061def0 Pages::RegisterFriend::__dt +8061dfb4 Pages::RegisterFriend::OnInit +8061e270 Pages::RegisterFriend::OnActivate +8061e2e8 0x8061e2e8 +8061e340 0x8061e340 +8061e348 Pages::RegisterFriend::OnDigitClick +8061e3b4 Pages::RegisterFriend::OnBackSpaceClick +8061e400 Pages::RegisterFriend::OnOkButtonClick +8061e6ec Pages::RegisterFriend::OnBackButtonClick +8061e730 Pages::RegisterFriend::OnOkButtonSelect +8061e76c Pages::RegisterFriend::OnOkButtonDeselect +8061e77c Pages::RegisterFriend::OnBackPress +8061e804 Pages::RegisterFriend::GetRuntimeTypeInfo +8061e810 Pages::xA5::GetRuntimeTypeInfo +8061e81c TriggerPtmf +8061e84c TriggerPtmf +8061e87c RegisterFriendPage_cpp_sinit_ +8061e898 0x8061e898 +8061e91c 0x8061e91c +8061e990 0x8061e990 +8061ea10 0x8061ea10 +8061eba8 Page::EndExitState +8061ebac 0x8061ebac +8061ebb0 0x8061ebb0 +8061f1cc 0x8061f1cc +8061f4e4 0x8061f1cc_switch +8061f4e8 0x8061f1cc_caseD_1 +8061f4f0 0x8061f1cc_caseD_2 +8061f4f8 0x8061f1cc_caseD_3 +8061f500 0x8061f1cc_caseD_4 +8061f508 0x8061f1cc_caseD_5 +8061f510 0x8061f1cc_caseD_6 +8061f518 0x8061f1cc_caseD_8 +8061f520 0x8061f1cc_caseD_9 +8061f528 0x8061f1cc_caseD_7 +8061f550 0x8061f1cc_caseD_0 +8061f5f0 0x8061f5f0 +8061f698 0x8061f698 +8061f6a4 TriggerPtmf +8061f6d4 0x8061f6d4 +8061f6e8 Pages::CorruptSaveMgr::__ct +8061f764 Pages::CorruptSaveMgr::__dt +8061f7cc Pages::CorruptSaveMgr::OnInit +8061f828 Pages::CorruptSaveMgr::OnActivate +8061f9b4 Pages::CorruptSaveMgr::AfterControlUpdate +8061fbec Pages::CorruptSaveMgr::OnResume +8061fd84 Pages::CorruptSaveMgr::IsHomeMenuWorking +8061fd9c Pages::CorruptSaveMgr::InitYesNoValues +8061ff40 0x8061ff40 +8062004c Pages::CorruptSaveMgr::OnReturnToWiiMenuClick +806200bc Pages::CannotSave::__ct +80620138 Pages::CannotSave::__dt +806201a0 Pages::CannotSave::OnInit +806201fc Pages::CannotSave::OnActivate +806203a0 Pages::CannotSave::OnYesNoClick +80620440 Pages::CorruptMemory::__ct +8062048c Pages::CorruptMemory::__dt +80620500 Pages::CorruptMemory::OnInit +806205c4 Pages::CorruptMemory::OnActivate +80620638 Pages::CorruptMiiData::__ct +806206c4 Pages::CorruptMiiData::__dt +80620744 Pages::CorruptMiiData::OnInit +80620848 Pages::CorruptMiiData::OnActivate +806208a0 Pages::CorruptMiiData::OnButtonClick +806208ec Pages::CorruptMiiData::IsHomeMenuWorking +806208f4 Pages::CorruptMiiData::GetRuntimeTypeInfo +80620900 Pages::CorruptMemory::IsHomeMenuWorking +80620908 Pages::CorruptMemory::GetRuntimeTypeInfo +80620914 Pages::CannotSave::__dt +8062091c Pages::CannotSave::GetRuntimeTypeInfo +80620928 Pages::CorruptSaveMgr::GetRuntimeTypeInfo +80620934 TriggerPtmf +80620964 TriggerPtmf +80620994 TriggerPtmf +806209c4 CorruptPages_cpp_sinit_ +806209e8 RKSYSRequester::__ct +80620a1c 0x80620a1c +80620a8c RKSYSRequester::__dt +80620b04 RKSYSRequester::Reset +80620b40 RKSYSRequester::Update +80620c5c RKSYSRequester::Init +80620da4 RKSYSRequester::Close +80620dd8 RKSYSRequester::HandleRequest +806211dc RKSYSRequester::HandleAllRequests +806212fc RKSYSRequester::CheckRequests +806213a4 RKSYSRequester::HasError +806213b8 0x806213b8 +806213cc RKSYSRequester::RequestEraseAllGhosts +806213e4 RKSYSRequester::RequestSaveLicense +80621410 RKSYSRequester::NotifyNewLicenseContent +80621434 RKSYSRequester::RequestSaveLicenseIfNewContent +80621484 RKSYSRequester::RequestSaveGhost +8062182c RKSYSRequester::RequestSaveGhost_0x18 +80621bd0 RKSYSRequester::RequestEraseGhost +80621c08 RKSYSRequester::RequestEraseGhostFromLicense +80621c3c RKSYSRequester::RequestGetDLdGhost +80621c78 RKSYSRequester::RequestGetGhostFromLicense +80621cb0 RKSYSRequester::RequestGetGroupGhosts +80621ce0 RKSYSRequester::RequestGetGroupGhostsFromLicense +80621d0c Section::__ct +80621d84 Section::__dt +80621e00 Section::Init +806220b0 Section::Enter +806221d8 Section::Exit +806223a0 Section::UpdateSystemPages +806224f8 Section::Update +80622764 Section::Draw +80622ae0 Section::Pause +80622b4c Section::Resume +80622c08 Section::OnTransition +80622c68 Section::CreateMoviePlayer +80622cb8 Section::CreateFriendList +80622d08 Section::CreateAndInitPage +80622d54 Section::CreateSystemPage +80622da0 Section::AddPageLayer +80622e00 Section::AddPageLayerAnimatedReturnTopLayer +80622e6c Section::IsPageTopLayer +80622ea0 Section::GetTopLayerPage +80622ec8 Section::IsHomeMenuWorking +80622f88 Section::IsDVDEnabled +80623054 Section::ScheduleDisconnection +80623068 Section::UpdateLayers +80623228 Section::AddPageLayerAnimated +80623288 Section::TopLayerPage +806232e8 Section::RemovePageLayers +80623390 Section::CreatePageById +806233b8 Section::CreatePageById_switch +806233bc Section::CreatePageById_case0 +806233d4 Section::CreatePageById_case1 +806233ec Section::CreatePageById_case2 +80623404 Section::CreatePageById_case3 +8062341c Section::CreatePageById_case4 +80623434 Section::CreatePageById_case5 +8062344c Section::CreatePageById_case6 +80623464 Section::CreatePageById_case7 +8062347c Section::CreatePageById_case8 +80623494 Section::CreatePageById_case9 +806234ac Section::CreatePageById_casea +806234c4 Section::CreatePageById_caseb +806234dc Section::CreatePageById_casec +806234f4 Section::CreatePageById_cased +8062350c Section::CreatePageById_casee +80623524 Section::CreatePageById_case10 +8062353c Section::CreatePageById_case12 +80623554 Section::CreatePageById_case13 +8062356c Section::CreatePageById_case16 +80623584 Section::CreatePageById_case17 +8062359c Section::CreatePageById_case18 +806235b4 Section::CreatePageById_case19 +806235cc Section::CreatePageById_case1a +806235e4 Section::CreatePageById_case1b +806235fc Section::CreatePageById_case1c +80623614 Section::CreatePageById_case1d +8062362c Section::CreatePageById_case1e +80623644 Section::CreatePageById_case1f +8062365c Section::CreatePageById_case20 +80623674 Section::CreatePageById_case21 +8062368c Section::CreatePageById_case22 +806236a4 Section::CreatePageById_case23 +806236bc Section::CreatePageById_case24 +806236d4 Section::CreatePageById_case25 +806236ec Section::CreatePageById_case26 +80623704 Section::CreatePageById_case27 +8062371c Section::CreatePageById_case28 +80623734 Section::CreatePageById_case29 +8062374c Section::CreatePageById_case2a +80623764 Section::CreatePageById_case2b +8062377c Section::CreatePageById_case2c +80623794 Section::CreatePageById_case2d +806237ac Section::CreatePageById_case2e +806237c4 Section::CreatePageById_case2f +806237dc Section::CreatePageById_case30 +806237f4 Section::CreatePageById_case31 +8062380c Section::CreatePageById_case32 +80623824 Section::CreatePageById_case33 +8062383c Section::CreatePageById_case34 +80623854 Section::CreatePageById_case35 +8062386c Section::CreatePageById_case36 +80623884 Section::CreatePageById_case37 +8062389c Section::CreatePageById_case38 +806238b4 Section::CreatePageById_case39 +806238cc Section::CreatePageById_case3a +806238e4 Section::CreatePageById_case3b +806238fc Section::CreatePageById_case3c +80623914 Section::CreatePageById_case3d +8062392c Section::CreatePageById_case3e +80623944 Section::CreatePageById_case3f +8062395c Section::CreatePageById_case40 +80623974 Section::CreatePageById_case41 +8062398c Section::CreatePageById_case42 +806239a4 Section::CreatePageById_case43 +806239bc Section::CreatePageById_case44 +806239d4 Section::CreatePageById_case45 +806239ec Section::CreatePageById_case46 +80623a04 Section::CreatePageById_case47 +80623a1c Section::CreatePageById_case48 +80623a34 Section::CreatePageById_case49 +80623a4c Section::CreatePageById_case4a +80623a64 Section::CreatePageById_case4b +80623a7c Section::CreatePageById_case4c +80623a94 Section::CreatePageById_case4d +80623aac Section::CreatePageById_case4e +80623ac4 Section::CreatePageById_case4f +80623adc Section::CreatePageById_case51 +80623af4 Section::CreatePageById_case52 +80623b0c Section::CreatePageById_case53 +80623b24 Section::CreatePageById_case54 +80623b3c Section::CreatePageById_case55 +80623b54 Section::CreatePageById_case56 +80623b6c Section::CreatePageById_case57 +80623b84 Section::CreatePageById_case58 +80623b9c Section::CreatePageById_case59 +80623bb4 Section::CreatePageById_case5a +80623bcc Section::CreatePageById_case5b +80623be4 Section::CreatePageById_case5c +80623bfc Section::CreatePageById_case5d +80623c14 Section::CreatePageById_case5e +80623c2c Section::CreatePageById_case5f +80623c44 Section::CreatePageById_case60 +80623c5c Section::CreatePageById_case61 +80623c74 Section::CreatePageById_case62 +80623c8c Section::CreatePageById_case63 +80623ca4 Section::CreatePageById_case64 +80623cbc Section::CreatePageById_case65 +80623cd4 Section::CreatePageById_case66 +80623cec Section::CreatePageById_case67 +80623d04 Section::CreatePageById_case68 +80623d1c Section::CreatePageById_case69 +80623d34 Section::CreatePageById_case6a +80623d4c Section::CreatePageById_case6b +80623d64 Section::CreatePageById_case6c +80623d7c Section::CreatePageById_case6d +80623d94 Section::CreatePageById_case6e +80623dac Section::CreatePageById_case6f +80623dc4 Section::CreatePageById_case70 +80623ddc Section::CreatePageById_case71 +80623df4 Section::CreatePageById_case72 +80623e0c Section::CreatePageById_case73 +80623e24 Section::CreatePageById_case74 +80623e3c Section::CreatePageById_case75 +80623e54 Section::CreatePageById_case76 +80623e6c Section::CreatePageById_case77 +80623e84 Section::CreatePageById_case78 +80623e9c Section::CreatePageById_case79 +80623eb4 Section::CreatePageById_case7a +80623ecc Section::CreatePageById_case7b +80623ee4 Section::CreatePageById_case7c +80623efc Section::CreatePageById_case7d +80623f14 Section::CreatePageById_case7e +80623f2c Section::CreatePageById_case7f +80623f44 Section::CreatePageById_case80 +80623f5c Section::CreatePageById_case81 +80623f74 Section::CreatePageById_case82 +80623f8c Section::CreatePageById_case83 +80623fa4 Section::CreatePageById_case84 +80623fbc Section::CreatePageById_case85 +80623fd4 Section::CreatePageById_case86 +80623fec Section::CreatePageById_case87 +80624004 Section::CreatePageById_case88 +8062401c Section::CreatePageById_case89 +80624034 Section::CreatePageById_case8a +8062404c Section::CreatePageById_case8b +80624064 Section::CreatePageById_case8c +8062407c Section::CreatePageById_case8d +80624094 Section::CreatePageById_case8e +806240ac Section::CreatePageById_case8f +806240c4 Section::CreatePageById_case90 +806240dc Section::CreatePageById_case91 +806240f4 Section::CreatePageById_case92 +8062410c Section::CreatePageById_case93 +80624124 Section::CreatePageById_case94 +8062413c Section::CreatePageById_case95 +80624154 Section::CreatePageById_case96 +8062416c Section::CreatePageById_case97 +80624184 Section::CreatePageById_case98 +8062419c Section::CreatePageById_case99 +806241b4 Section::CreatePageById_case9a +806241cc Section::CreatePageById_case9b +806241e4 Section::CreatePageById_case9c +806241fc Section::CreatePageById_case9d +80624214 Section::CreatePageById_case9e +8062422c Section::CreatePageById_case9f +80624244 Section::CreatePageById_casea0 +8062425c Section::CreatePageById_casea1 +80624274 Section::CreatePageById_casea2 +8062428c Section::CreatePageById_casea3 +806242a4 Section::CreatePageById_casea4 +806242bc Section::CreatePageById_casea5 +806242d4 Section::CreatePageById_casea6 +806242ec Section::CreatePageById_casea7 +80624304 Section::CreatePageById_casea8 +8062431c Section::CreatePageById_caseaa +80624334 Section::CreatePageById_caseab +8062434c Section::CreatePageById_caseac +80624364 Section::CreatePageById_casead +8062437c Section::CreatePageById_caseae +80624394 Section::CreatePageById_caseaf +806243ac Section::CreatePageById_caseb0 +806243c4 Section::CreatePageById_caseb1 +806243dc Section::CreatePageById_caseb2 +806243f4 Section::CreatePageById_caseb3 +8062440c Section::CreatePageById_caseb4 +80624424 Section::CreatePageById_caseb5 +8062443c Section::CreatePageById_caseb6 +80624454 Section::CreatePageById_caseb7 +8062446c Section::CreatePageById_caseb8 +80624484 Section::CreatePageById_caseb9 +8062449c Section::CreatePageById_caseba +806244b4 Section::CreatePageById_casebb +806244cc Section::CreatePageById_casebc +806244e4 Section::CreatePageById_casebd +806244fc Section::CreatePageById_casebe +80624514 Section::CreatePageById_casebf +8062452c Section::CreatePageById_casec0 +80624544 Section::CreatePageById_casec1 +8062455c Section::CreatePageById_casec2 +80624574 Section::CreatePageById_casec3 +8062458c Section::CreatePageById_casec5 +806245a4 Section::CreatePageById_casec6 +806245bc Section::CreatePageById_casec7 +806245d4 Section::CreatePageById_casec8 +806245ec Section::CreatePageById_casec9 +80624604 Section::CreatePageById_caseca +8062461c Section::CreatePageById_casecb +80624634 Section::CreatePageById_casecc +8062464c Section::CreatePageById_casecd +80624664 Section::CreatePageById_casece +80624690 Pages::RaceIntro::__ct +806246e4 Pages::GPHUD::__ct +80624764 Pages::RaceHUD::__dt +806247cc Pages::TTHUD::__ct +80624874 Pages::VSHUD::__ct +806248f4 Pages::VSMultiHUD::__ct +80624974 Pages::BattleHUD::__ct +806249f4 Pages::BattleMultiHUD::__ct +80624a74 Pages::TournamentHUD::__ct +80624af4 Pages::GPPause::__ct +80624b38 Pages::RaceMenu::__dt +80624ba0 Pages::VSPause::__ct +80624be4 Pages::TTPause::__ct +80624c28 Pages::BattlePause::__ct +80624c6c 0x80624c6c +80624cb0 0x80624cb0 +80624cf4 0x80624cf4 +80624d38 0x80624d38 +80624d7c Pages::GhostReplayPause::GhostReplayPausePage +80624dc0 Pages::GPEnd::__ct +80624e04 Pages::TTEnd::__ct +80624e48 Pages::VSRaceEnd::VSRaceEndPage +80624e8c Pages::BattleEnd::__ct +80624ed0 Pages::BattleFinalRaceEnd::__ct +80624f14 0x80624f14 +80624f58 0x80624f58 +80624f9c 0x80624f9c +80624fe0 0x80624fe0 +80625024 Pages::TTFriendGhostEnd::__ct +80625068 0x80625068 +806250ac Pages::CheckRankings::__ct +806250f0 Pages::QuitConfirmation::__ct +80625134 Pages::TTLeaderboard::__ct +80625198 Pages::Leaderboard::__dt +80625200 0x80625200 +8062526c Pages::GPVSLeaderboardUpdate::__ct +806252a8 Pages::GPVSLeaderboardTotal::__ct +806252e4 Pages::WWLeaderboardUpdate::__ct +80625320 Pages::TeamVSLeaderboardTotal::__ct +80625388 CtrlRaceResultTeam::__ct +806253ec CtrlRaceResultTeam::__dt +80625460 Pages::TeamVSLeaderboardBase::__dt +806254e8 Pages::BattleLeaderboardUpdate::__ct +80625550 Pages::BattleLeaderboardTotal::__ct +806255b8 0x806255b8 +806255f4 Pages::GPReplayHUD::__ct +80625674 Pages::GhostRaceHUD::__ct +806256f4 Pages::GPReplayPause::__ct +80625738 Pages::TTReplayPause::__ct +8062577c Pages::WifiVSHUD::__ct +806257fc Pages::WifiVSMultiHUD::__ct +8062587c Pages::WiFiFriendVSHUD::__ct +806258fc Pages::WiFiFriendTeamVSHUD::__ct +8062597c Pages::LiveViewHUD::__ct +806259fc Pages::LiveBattleViewHUD::__ct +80625a7c Pages::StartRace::__ct +80625d30 Pages::Menu::__dt +80625d9c Pages::DriftSelect::OnStartPress +80625da0 Pages::MainMenu::__ct +80626078 Pages::MenuBG::__ct +806261e8 Pages::TopMenuOverlay::__ct +8062635c Pages::MiiSelect::__ct +80626640 MiiName::__dt +80626698 Pages::SinglePlayer::__ct +80626950 Pages::GPClassSelect::__ct +80626c10 Pages::CharacterSelect::__ct +80627008 CtrlMenuCharacterSelect::__dt +80627060 Pages::KartSelect::__ct +80627338 CtrlMenuMachineGraph::__ct +80627374 CtrlMenuMachineGraph::__dt +806273cc Pages::DriftSelect::__ct +80627684 Pages::CupSelect::__ct +80627a40 CtrlMenuMovieHandler::__dt +80627a80 CtrlMenuMovieLayout::__dt +80627adc CtrlMenuCupSelectCup::__dt +80627b64 CtrlMenuCupSelectCourse::__dt +80627bd8 Pages::CourseSelect::__ct +80627f6c CtrlMenuCourseSelectCupSub::__ct +80627fa8 CtrlMenuCourseSelectCupSub::__dt +80628000 CtrlMenuCourseSelectCup::__dt +80628068 0x80628068 +80628074 CourseButton::__ct +806280d4 CtrlMenuMovieButton::__dt +80628130 CourseButton::__dt +80628190 0x80628190 +80628210 Pages::VSModeSelect::__ct +806284c8 Pages::VSSettings::VSSetting +806287e0 Pages::VSTeamsView::__ct +80628ab4 CtrlMenuTeamConfirmTeam::__ct +80628b10 CtrlMenuTeamConfirmTeam::__dt +80628b84 Pages::BattleModeSelect::__ct +80628e3c Pages::BattleKartSelect::__ct +8062912c CharaName::__dt +80629184 Pages::BattleSettings::__ct +8062949c Pages::BattleCupSelect::__ct +80629858 CtrlMenuBattleCupSelectCup::__dt +806298e0 CtrlMenuBattleCupSelectStage::__dt +80629954 Pages::BattleStageSelect::__ct +80629ce8 0x80629ce8 +80629d24 0x80629d24 +80629d7c CtrlMenuBattleStageSelectCup::__dt +80629df0 StageButton::__ct +80629e50 StageButton::__dt +80629eb0 CtrlMenuBattleStageSelectStage::__ct +80629f30 0x80629f30 +8062a2ec 0x8062a2ec +8062a374 0x8062a374 +8062a3e8 0x8062a3e8 +8062a77c 0x8062a77c +8062a7b8 0x8062a7b8 +8062a810 0x8062a810 +8062a884 0x8062a884 +8062a8e4 0x8062a8e4 +8062a944 0x8062a944 +8062a9c4 0x8062a9c4 +8062ac9c 0x8062ac9c +8062af88 0x8062af88 +8062b240 Pages::MultiPlayer::__ct +8062b4f4 Pages::MultiKartSelect::__ct +8062b828 MultiKartBox::__ct +8062b864 MultiKartBox::__dt +8062b8bc Pages::MultiDriftSelect::__ct +8062bb70 Pages::MultiTeamSelect::__ct +8062be24 Pages::Unlock::__ct +8062c0f4 Pages::LicenseRecords::__ct +8062c3a4 Section::CreateSectionPages +8062c3d8 Section::CreateSectionPages_switch +8062c3dc Section::CreateSectionPages_case6 +8062c3e8 Section::CreateSectionPages_case7 +8062c3f4 Section::CreateSectionPages_case8 +8062c400 Section::CreateSectionPages_case9 +8062c40c Section::CreateSectionPages_casea +8062c418 Section::CreateSectionPages_caseb +8062c424 Section::CreateSectionPages_casec +8062c430 Section::CreateSectionPages_cased +8062c43c Section::CreateSectionPages_casee +8062c448 Section::CreateSectionPages_casef +8062c454 Section::CreateSectionPages_case10 +8062c478 Section::CreateSectionPages_case11 +8062c4a8 Section::CreateSectionPages_case12 +8062c4d8 Section::CreateSectionPages_case13 +8062c4fc Section::CreateSectionPages_case14 +8062c514 Section::CreateSectionPages_case15 +8062c52c Section::CreateSectionPages_case16 +8062c538 Section::CreateSectionPages_case17 +8062c544 Section::CreateSectionPages_case18 +8062c550 Section::CreateSectionPages_case19 +8062c55c Section::CreateSectionPages_case1a +8062c568 Section::CreateSectionPages_case1b +8062c574 Section::CreateSectionPages_case1c +8062c580 Section::CreateSectionPages_case1d +8062c58c Section::CreateSectionPages_case1e +8062c5e0 Section::CreateSectionPages_case1f +8062c64c Section::CreateSectionPages_case20 +8062c694 Section::CreateSectionPages_case21 +8062c6e8 Section::CreateSectionPages_case22 +8062c73c Section::CreateSectionPages_case23 +8062c790 Section::CreateSectionPages_case24 +8062c7d8 Section::CreateSectionPages_case25 +8062c82c Section::CreateSectionPages_case26 +8062c880 Section::CreateSectionPages_case27 +8062c8d4 Section::CreateSectionPages_case28 +8062c928 Section::CreateSectionPages_case29 +8062c988 Section::CreateSectionPages_case2a +8062c9e8 Section::CreateSectionPages_case2b +8062ca48 Section::CreateSectionPages_case2c +8062ca84 Section::CreateSectionPages_case2d +8062cacc Section::CreateSectionPages_case2e +8062cafc Section::CreateSectionPages_case2f +8062cb44 Section::CreateSectionPages_case30 +8062cbbc Section::CreateSectionPages_case31 +8062cc40 Section::CreateSectionPages_case32 +8062cc7c Section::CreateSectionPages_case33 +8062ccb8 Section::CreateSectionPages_case34 +8062ccf4 Section::CreateSectionPages_case35 +8062cd0c Section::CreateSectionPages_case36 +8062cd24 Section::CreateSectionPages_case37 +8062cd3c Section::CreateSectionPages_case38 +8062cd54 Section::CreateSectionPages_case39 +8062cd60 Section::CreateSectionPages_case3a +8062cd6c Section::CreateSectionPages_case3b +8062cd84 Section::CreateSectionPages_case3c +8062cd9c Section::CreateSectionPages_case3d +8062cdd8 Section::CreateSectionPages_case3e +8062ce14 Section::CreateSectionPages_case3f +8062ce98 Section::CreateSectionPages_case40 +8062cf1c Section::CreateSectionPages_case41 +8062cfa0 Section::CreateSectionPages_case42 +8062d024 Section::CreateSectionPages_case43 +8062d0a8 Section::CreateSectionPages_case44 +8062d0c0 Section::CreateSectionPages_case45 +8062d108 Section::CreateSectionPages_case46 +8062d150 Section::CreateSectionPages_case47 +8062d1f8 Section::CreateSectionPages_case48 +8062d33c Section::CreateSectionPages_case49 +8062d480 Section::CreateSectionPages_case4a +8062d5c4 Section::CreateSectionPages_case4b +8062d648 Section::CreateSectionPages_case4c +8062d6cc Section::CreateSectionPages_case4d +8062d810 Section::CreateSectionPages_case4e +8062d87c Section::CreateSectionPages_case4f +8062d8e8 Section::CreateSectionPages_case50 +8062d954 Section::CreateSectionPages_case51 +8062d99c Section::CreateSectionPages_case52 +8062d9e4 Section::CreateSectionPages_case53 +8062da2c Section::CreateSectionPages_case54 +8062db64 Section::CreateSectionPages_case55 +8062dd08 Section::CreateSectionPages_case56 +8062deac Section::CreateSectionPages_case57 +8062e050 Section::CreateSectionPages_case58 +8062e0e0 Section::CreateSectionPages_case59 +8062e170 Section::CreateSectionPages_case5a +8062e1d0 Section::CreateSectionPages_case5b +8062e374 Section::CreateSectionPages_case5c +8062e518 Section::CreateSectionPages_case5d +8062e6bc Section::CreateSectionPages_case5e +8062e74c Section::CreateSectionPages_case5f +8062e7dc Section::CreateSectionPages_case60 +8062e86c Section::CreateSectionPages_case61 +8062e8fc Section::CreateSectionPages_case62 +8062e98c Section::CreateSectionPages_case63 +8062ea1c Section::CreateSectionPages_case64 +8062eaac Section::CreateSectionPages_case65 +8062eb3c Section::CreateSectionPages_case66 +8062ebcc Section::CreateSectionPages_case67 +8062ec5c Section::CreateSectionPages_case68 +8062ecd4 Section::CreateSectionPages_case69 +8062ed4c Section::CreateSectionPages_case6a +8062eda0 Section::CreateSectionPages_case6b +8062edf4 Section::CreateSectionPages_case6c +8062ee78 Section::CreateSectionPages_case6d +8062eefc Section::CreateSectionPages_case6e +8062ef5c Section::CreateSectionPages_case6f +8062efbc Section::CreateSectionPages_case70 +8062f028 Section::CreateSectionPages_case71 +8062f094 Section::CreateSectionPages_case72 +8062f100 Section::CreateSectionPages_case73 +8062f16c Section::CreateSectionPages_case74 +8062f1d8 Section::CreateSectionPages_case75 +8062f244 Section::CreateSectionPages_case76 +8062f2b0 Section::CreateSectionPages_case77 +8062f31c Section::CreateSectionPages_case78 +8062f334 Section::CreateSectionPages_case79 +8062f34c Section::CreateSectionPages_case7a +8062f3a0 Section::CreateSectionPages_case7b +8062f3f4 Section::CreateSectionPages_case7c +8062f448 Section::CreateSectionPages_case7d +8062f544 Section::CreateSectionPages_case7e +8062f610 Section::CreateSectionPages_case7f +8062f694 Section::CreateSectionPages_case80 +8062f778 Section::CreateSectionPages_case81 +8062f850 Section::CreateSectionPages_case82 +8062f8b0 Section::CreateSectionPages_case83 +8062fa3c Section::CreateSectionPages_case84 +8062fbd4 Section::CreateSectionPages_case85 +8062fc4c Section::CreateSectionPages_case86 +8062fcc4 Section::CreateSectionPages_case87 +8062fd48 Section::CreateSectionPages_case88 +8062fd60 Section::CreateSectionPages_case89 +8062fd78 Section::CreateSectionPages_case8a +8062fdcc Section::CreateSectionPages_case8b +8062fe08 Section::CreateSectionPages_case8c +8062fec8 Section::CreateSectionPages_case8d +8062fef8 Section::CreateSectionPages_case8e +8062ff1c Section::CreateSectionPages_case8f +8062ff88 Section::CreateSectionPages_case90 +8062ffb8 Section::CreateSectionPages_case91 +8062ffe8 Section::CreateSectionPages_case92 +80630018 Section::CreateSectionPages_case93 +80630048 Section::CreateSectionPages_case94 +80630094 Section::AddInitialLayers +806300c8 Section::AddInitialLayers_switch +806300cc Section::AddInitialLayers_caseD_6 +806300d8 Section::AddInitialLayers_caseD_7 +806300e4 Section::AddInitialLayers_caseD_8 +806300f0 Section::AddInitialLayers_caseD_9 +806300fc Section::AddInitialLayers_caseD_a +80630108 Section::AddInitialLayers_caseD_b +80630114 Section::AddInitialLayers_caseD_c +80630120 Section::AddInitialLayers_caseD_d +8063012c Section::AddInitialLayers_caseD_e +80630138 Section::AddInitialLayers_caseD_f +80630144 Section::AddInitialLayers_caseD_10 +8063015c Section::AddInitialLayers_caseD_11 +80630174 Section::AddInitialLayers_caseD_12 +8063018c Section::AddInitialLayers_caseD_13 +806301a4 Section::AddInitialLayers_caseD_14 +806301bc Section::AddInitialLayers_caseD_15 +806301d4 Section::AddInitialLayers_caseD_16 +806301e0 Section::AddInitialLayers_caseD_17 +806301ec Section::AddInitialLayers_caseD_18 +806301f8 Section::AddInitialLayers_caseD_19 +80630204 Section::AddInitialLayers_caseD_1a +80630210 Section::AddInitialLayers_caseD_1b +8063021c Section::AddInitialLayers_caseD_1c +80630228 Section::AddInitialLayers_caseD_1d +80630234 Section::AddInitialLayers_caseD_1e +8063024c Section::AddInitialLayers_caseD_1f +80630270 Section::AddInitialLayers_caseD_20 +80630288 Section::AddInitialLayers_caseD_21 +806302a0 Section::AddInitialLayers_caseD_22 +806302b8 Section::AddInitialLayers_caseD_23 +806302d0 Section::AddInitialLayers_caseD_24 +806302e8 Section::AddInitialLayers_caseD_25 +80630300 Section::AddInitialLayers_caseD_26 +80630318 Section::AddInitialLayers_caseD_27 +80630330 Section::AddInitialLayers_caseD_28 +80630348 Section::AddInitialLayers_caseD_29 +80630360 Section::AddInitialLayers_caseD_2a +80630378 Section::AddInitialLayers_caseD_2b +80630390 Section::AddInitialLayers_caseD_2c +806303a8 Section::AddInitialLayers_caseD_2d +806303c0 Section::AddInitialLayers_caseD_2e +806303d8 Section::AddInitialLayers_caseD_2f +806303fc Section::AddInitialLayers_caseD_30 +80630414 Section::AddInitialLayers_caseD_31 +80630438 Section::AddInitialLayers_caseD_32 +8063045c Section::AddInitialLayers_caseD_33 +80630480 Section::AddInitialLayers_caseD_34 +806304a4 Section::AddInitialLayers_caseD_35 +806304b0 Section::AddInitialLayers_caseD_36 +806304bc Section::AddInitialLayers_caseD_37 +806304c8 Section::AddInitialLayers_caseD_38 +806304d4 Section::AddInitialLayers_caseD_39 +806304e0 Section::AddInitialLayers_caseD_3a +806304ec Section::AddInitialLayers_caseD_3b +80630504 Section::AddInitialLayers_caseD_3c +8063051c Section::AddInitialLayers_caseD_3d +80630534 Section::AddInitialLayers_caseD_3e +8063054c Section::AddInitialLayers_caseD_3f +8063057c Section::AddInitialLayers_caseD_40 +806305ac Section::AddInitialLayers_caseD_41 +806305dc Section::AddInitialLayers_caseD_42 +8063060c Section::AddInitialLayers_caseD_43 +8063063c Section::AddInitialLayers_caseD_44 +80630654 Section::AddInitialLayers_caseD_45 +80630678 Section::AddInitialLayers_caseD_46 +8063069c Section::AddInitialLayers_caseD_47 +806306c0 Section::AddInitialLayers_caseD_48 +806306fc Section::AddInitialLayers_caseD_49 +80630738 Section::AddInitialLayers_caseD_4a +80630774 Section::AddInitialLayers_caseD_4b +806307b0 Section::AddInitialLayers_caseD_4c +806307ec Section::AddInitialLayers_caseD_4d +8063081c Section::AddInitialLayers_caseD_4e +80630858 Section::AddInitialLayers_caseD_4f +80630894 Section::AddInitialLayers_caseD_50 +806308d0 Section::AddInitialLayers_caseD_51 +80630900 Section::AddInitialLayers_caseD_52 +80630930 Section::AddInitialLayers_caseD_53 +80630960 Section::AddInitialLayers_caseD_54 +80630990 Section::AddInitialLayers_caseD_55 +806309d8 Section::AddInitialLayers_caseD_56 +80630a20 Section::AddInitialLayers_caseD_57 +80630a68 Section::AddInitialLayers_caseD_58 +80630aa4 Section::AddInitialLayers_caseD_59 +80630ae0 Section::AddInitialLayers_caseD_5a +80630af8 Section::AddInitialLayers_caseD_5b +80630b40 Section::AddInitialLayers_caseD_5c +80630b88 Section::AddInitialLayers_caseD_5d +80630bd0 Section::AddInitialLayers_caseD_5e +80630c0c Section::AddInitialLayers_caseD_5f +80630c48 Section::AddInitialLayers_caseD_60 +80630c84 Section::AddInitialLayers_caseD_61 +80630cc0 Section::AddInitialLayers_caseD_62 +80630cfc Section::AddInitialLayers_caseD_63 +80630d38 Section::AddInitialLayers_caseD_64 +80630d74 Section::AddInitialLayers_caseD_65 +80630db0 Section::AddInitialLayers_caseD_66 +80630dec Section::AddInitialLayers_caseD_67 +80630e28 Section::AddInitialLayers_caseD_68 +80630e58 Section::AddInitialLayers_caseD_69 +80630e88 Section::AddInitialLayers_caseD_6a +80630eb8 Section::AddInitialLayers_caseD_6b +80630ee8 Section::AddInitialLayers_caseD_6c +80630f18 Section::AddInitialLayers_caseD_6d +80630f48 Section::AddInitialLayers_caseD_6e +80630f78 Section::AddInitialLayers_caseD_6f +80630fa8 Section::AddInitialLayers_caseD_70 +80630fd8 Section::AddInitialLayers_caseD_71 +80631008 Section::AddInitialLayers_caseD_72 +80631038 Section::AddInitialLayers_caseD_73 +80631068 Section::AddInitialLayers_caseD_74 +80631098 Section::AddInitialLayers_caseD_75 +806310c8 Section::AddInitialLayers_caseD_76 +806310f8 Section::AddInitialLayers_caseD_77 +80631128 Section::AddInitialLayers_caseD_78 +80631140 Section::AddInitialLayers_caseD_79 +80631158 Section::AddInitialLayers_caseD_7a +80631170 Section::AddInitialLayers_caseD_7b +80631188 Section::AddInitialLayers_caseD_7c +806311a0 Section::AddInitialLayers_caseD_7d +806311e8 Section::AddInitialLayers_caseD_7e +80631224 Section::AddInitialLayers_caseD_7f +80631248 Section::AddInitialLayers_caseD_80 +80631278 Section::AddInitialLayers_caseD_81 +806312a8 Section::AddInitialLayers_caseD_82 +806312cc Section::AddInitialLayers_caseD_83 +80631314 Section::AddInitialLayers_caseD_84 +8063135c Section::AddInitialLayers_caseD_85 +80631380 Section::AddInitialLayers_caseD_86 +806313a4 Section::AddInitialLayers_caseD_87 +806313c8 Section::AddInitialLayers_caseD_88 +806313e0 Section::AddInitialLayers_caseD_89 +806313f8 Section::AddInitialLayers_caseD_8a +80631410 Section::AddInitialLayers_caseD_8b +80631428 Section::AddInitialLayers_caseD_8c +8063144c Section::AddInitialLayers_caseD_8d +80631470 Section::AddInitialLayers_caseD_8e +80631494 Section::AddInitialLayers_caseD_8f +806314b8 Section::AddInitialLayers_caseD_90 +806314e8 Section::AddInitialLayers_caseD_91 +80631518 Section::AddInitialLayers_caseD_92 +80631548 Section::AddInitialLayers_caseD_93 +80631588 Section::GetSceneId +80631734 Section::GetArchiveName +80631a58 Section::HasBackModel +80631af8 Section::HasDriverController +80631bf0 Section::GetPageName +80631c00 Section::GetSectionName +80631c10 0x80631c10 +80631c68 Section::GetSoundId +80631ef8 Section::GetGroupId +80632080 0x80632080 +806321b4 Section::GetAudioStreamIdx +806321d0 Section::GetAudioStreamIdx_switch +806321d4 Section::GetAudioStreamIdx_caseD_0 +806321dc Section::GetAudioStreamIdx_caseD_1 +806321e4 Section::GetAudioStreamIdx_caseD_2 +806321ec Section::GetAudioStreamIdx_caseD_5 +806321f4 Section::GetAudioStreamIdx_caseD_6 +806321fc Section::GetAudioStreamIdx_caseD_10 +80632204 Section::GetAudioStreamIdx_caseD_4b +8063220c Section::GetAudioStreamIdx_caseD_4c +80632214 Section::GetAudioStreamIdx_caseD_4d +8063221c Section::GetAudioStreamIdx_caseD_4e +80632224 Section::GetAudioStreamIdx_caseD_57 +8063222c Section::GetAudioStreamIdx_caseD_58 +80632234 Section::GetAudioStreamIdx_caseD_5a +8063223c Section::GetAudioStreamIdx_caseD_5b +80632244 Section::GetAudioStreamIdx_caseD_5c +8063224c Section::GetAudioStreamIdx_caseD_5d +80632254 Section::GetAudioStreamIdx_caseD_5e +8063225c Section::GetAudioStreamIdx_caseD_5f +80632264 Section::GetAudioStreamIdx_caseD_60 +8063226c Section::GetAudioStreamIdx_caseD_65 +80632274 Section::GetAudioStreamIdx_caseD_66 +8063227c Section::GetAudioStreamIdx_caseD_69 +80632284 Section::GetAudioStreamIdx_caseD_6b +8063228c Section::GetAudioStreamIdx_caseD_6e +80632294 Section::GetAudioStreamIdx_caseD_70 +8063229c Section::GetAudioStreamIdx_caseD_72 +806322a4 Section::GetAudioStreamIdx_caseD_74 +806322ac Section::GetAudioStreamIdx_caseD_75 +806322b4 Section::GetAudioStreamIdx_caseD_76 +806322bc Section::GetAudioStreamIdx_caseD_77 +806322c4 Section::GetAudioStreamIdx_caseD_78 +806322cc Section::GetAudioStreamIdx_caseD_7a +806322d4 Section::GetAudioStreamIdx_caseD_7f +806322dc Section::GetAudioStreamIdx_caseD_80 +806322e4 Section::GetAudioStreamIdx_caseD_81 +806322ec Section::GetAudioStreamIdx_caseD_84 +806322f4 Section::GetAudioStreamIdx_caseD_85 +806322fc Section::GetAudioStreamIdx_caseD_8a +80632304 Section::GetAudioStreamIdx_caseD_8b +8063230c Section::GetAudioStreamIdx_caseD_8c +80632314 Section::GetAudioStreamIdx_caseD_8d +8063231c Section::GetAudioStreamIdx_caseD_8e +80632324 Section::GetAudioStreamIdx_caseD_8f +8063232c Section::GetAudioStreamIdx_caseD_90 +80632334 Section::GetAudioStreamIdx_caseD_93 +8063233c Section::GetAudioStreamIdx_caseD_95 +80632344 Section::GetAudioStreamIdx_caseD_96 +8063234c Section::GetAudioStreamIdx_caseD_98 +80632354 Section::GetAudioStreamIdx_caseD_9a +8063235c Section::GetAudioStreamIdx_caseD_9b +80632364 Section::GetAudioStreamIdx_caseD_9c +8063236c Section::GetAudioStreamIdx_caseD_9d +80632374 Section::GetAudioStreamIdx_caseD_9f +8063237c Section::GetAudioStreamIdx_caseD_a0 +80632384 Section::GetAudioStreamIdx_caseD_a1 +8063238c Section::GetAudioStreamIdx_caseD_a2 +80632394 Section::GetAudioStreamIdx_caseD_a5 +8063239c Section::GetAudioStreamIdx_caseD_a8 +806323a4 Section::GetAudioStreamIdx_caseD_b1 +806323ac Section::GetAudioStreamIdx_caseD_b3 +806323b4 Section::GetAudioStreamIdx_caseD_b4 +806323bc Section::GetAudioStreamIdx_caseD_b5 +806323c4 Section::GetAudioStreamIdx_caseD_b6 +806323cc Section::GetAudioStreamIdx_caseD_b8 +806323d4 Section::GetAudioStreamIdx_caseD_b9 +806323dc Section::GetAudioStreamIdx_caseD_bb +806323e4 Section::GetAudioStreamIdx_caseD_c0 +806323ec Section::GetAudioStreamIdx_caseD_ca +806323f4 Section::GetAudioStreamIdx_caseD_cc +806323fc Section::GetAudioStreamIdx_caseD_cd +8063240c Pages::RaceMenu::SetNextPage +80632414 Pages::RaceMenu::GetNextPage +8063241c 0x8063241c +80632424 0x80632424 +8063242c 0x8063242c +80632438 0x80632438 +80632444 0x80632444 +8063244c 0x8063244c +806324b8 0x806324b8 +806324c4 Pages::QuitConfirmation::GetMessageBMG +806324cc Pages::QuitConfirmation::IsPausePage +806324d4 Pages::QuitConfirmation::GetButtonsBRCTRName +806324e0 Pages::QuitConfirmation::GetVariantsIdxArray +806324ec Pages::QuitConfirmation::GetButtonCount +806324f4 Pages::QuitConfirmation::__dt +80632560 Pages::QuitConfirmation::GetRuntimeTypeInfo +8063256c Pages::CheckRankings::GetMessageBMG +80632574 Pages::CheckRankings::IsPausePage +8063257c Pages::CheckRankings::GetButtonsBRCTRName +80632588 Pages::CheckRankings::GetVariantsIdxArray +80632594 Pages::CheckRankings::GetButtonCount +8063259c Pages::CheckRankings::__dt +80632608 Pages::CheckRankings::GetRuntimeTypeInfo +80632614 Pages::TTFriendGhostEnd::GetMessageBMG +8063261c Pages::TTFriendGhostEnd::IsPausePage +80632624 Pages::TTFriendGhostEnd::GetButtonsBRCTRName +80632630 Pages::TTFriendGhostEnd::GetVariantsIdxArray +8063263c Pages::TTFriendGhostEnd::GetButtonCount +80632644 Pages::TTFriendGhostEnd::__dt +806326b0 Pages::TTFriendGhostEnd::GetRuntimeTypeInfo +806326bc 0x806326bc +806326c4 0x806326c4 +806326cc 0x806326cc +806326d8 0x806326d8 +806326e4 0x806326e4 +806326ec 0x806326ec +80632758 0x80632758 +80632764 0x80632764 +8063276c 0x8063276c +80632774 0x80632774 +80632780 0x80632780 +8063278c 0x8063278c +80632794 0x80632794 +80632800 0x80632800 +8063280c 0x8063280c +80632814 0x80632814 +8063281c 0x8063281c +80632828 0x80632828 +80632834 0x80632834 +8063283c 0x8063283c +806328a8 0x806328a8 +806328b4 0x806328b4 +806328bc 0x806328bc +806328c4 0x806328c4 +806328d0 0x806328d0 +806328dc 0x806328dc +806328e4 0x806328e4 +80632950 0x80632950 +8063295c Pages::BattleFinalRaceEnd::GetMessageBMG +80632964 Pages::BattleFinalRaceEnd::IsPausePage +8063296c Pages::BattleFinalRaceEnd::GetButtonsBRCTRName +80632978 Pages::BattleFinalRaceEnd::GetVariantsIdxArray +80632984 Pages::BattleFinalRaceEnd::GetButtonCount +8063298c Pages::BattleFinalRaceEnd::__dt +806329f8 Pages::BattleFinalRaceEnd::GetRuntimeTypeInfo +80632a04 Pages::BattleEnd::GetMessageBMG +80632a0c Pages::BattleEnd::IsPausePage +80632a14 Pages::BattleEnd::GetButtonsBRCTRName +80632a20 Pages::BattleEnd::GetVariantsIdxArray +80632a2c Pages::BattleEnd::GetButtonCount +80632a34 Pages::BattleEnd::__dt +80632aa0 Pages::BattleEnd::GetRuntimeTypeInfo +80632aac Pages::VSRaceEnd::GetMessageBMG +80632ab4 Pages::VSRaceEnd::IsPausePage +80632abc Pages::VSRaceEnd::GetButtonsBRCTRName +80632af4 Pages::VSRaceEnd::GetVariantsIdxArray +80632b00 Pages::VSRaceEnd::GetButtonCount +80632b30 Pages::VSRaceEnd::__dt +80632b9c Pages::VSRaceEnd::GetRuntimeTypeInfo +80632ba8 Pages::GPEnd::GetMessageBMG +80632bb0 Pages::GPEnd::IsPausePage +80632bb8 Pages::GPEnd::GetButtonsBRCTRName +80632bf0 Pages::GPEnd::GetVariantsIdxArray +80632bfc Pages::GPEnd::GetButtonCount +80632c2c Pages::GPEnd::__dt +80632c98 Pages::GPEnd::GetRuntimeTypeInfo +80632ca4 Pages::TTEnd::GetMessageBMG +80632cac Pages::TTEnd::IsPausePage +80632cb4 Pages::TTEnd::GetButtonsBRCTRName +80632cc0 Pages::TTEnd::GetVariantsIdxArray +80632ccc Pages::TTEnd::GetButtonCount +80632cd4 Pages::TTEnd::__dt +80632d40 Pages::TTEnd::GetRuntimeTypeInfo +80632d4c 0x80632d4c +80632d54 0x80632d54 +80632d5c 0x80632d5c +80632d68 0x80632d68 +80632d74 0x80632d74 +80632d7c 0x80632d7c +80632de8 0x80632de8 +80632df4 0x80632df4 +80632dfc 0x80632dfc +80632e04 0x80632e04 +80632e10 0x80632e10 +80632e1c 0x80632e1c +80632e24 0x80632e24 +80632e90 0x80632e90 +80632e9c 0x80632e9c +80632ea4 0x80632ea4 +80632eac 0x80632eac +80632eb8 0x80632eb8 +80632ec4 0x80632ec4 +80632ecc 0x80632ecc +80632f38 0x80632f38 +80632f44 Pages::GhostReplayPause::GetMessageBMG +80632f4c Pages::GhostReplayPause::IsPausePage +80632f54 Pages::GhostReplayPause::GetButtonsBRCTRName +80632f60 Pages::GhostReplayPause::GetVariantsIdxArray +80632f6c Pages::GhostReplayPause::GetButtonCount +80632f74 Pages::GhostReplayPause::__dt +80632fe0 Pages::GhostReplayPause::GetRuntimeTypeInfo +80632fec Pages::GPReplayPause::GetMessageBMG +80632ff4 Pages::GPReplayPause::IsPausePage +80632ffc Pages::GPReplayPause::GetButtonsBRCTRName +80633034 Pages::GPReplayPause::GetVariantsIdxArray +80633040 Pages::GPReplayPause::GetButtonCount +80633070 Pages::GPReplayPause::__dt +806330dc Pages::GPReplayPause::GetRuntimeTypeInfo +806330e8 Pages::TTReplayPause::GetMessageBMG +806330f0 Pages::TTReplayPause::IsPausePage +806330f8 Pages::TTReplayPause::GetButtonsBRCTRName +80633104 Pages::TTReplayPause::GetVariantsIdxArray +80633110 Pages::TTReplayPause::GetButtonCount +80633118 Pages::TTReplayPause::__dt +80633184 Pages::TTReplayPause::GetRuntimeTypeInfo +80633190 0x80633190 +80633198 0x80633198 +806331a0 0x806331a0 +806331ac 0x806331ac +806331b8 0x806331b8 +806331c0 0x806331c0 +8063322c 0x8063322c +80633238 Pages::BattlePause::GetMessageBMG +80633240 Pages::BattlePause::IsPausePage +80633248 Pages::BattlePause::GetButtonsBRCTRName +80633270 Pages::BattlePause::GetVariantsIdxArray +8063327c Pages::BattlePause::GetButtonCount +80633284 Pages::BattlePause::__dt +806332f0 Pages::BattlePause::GetRuntimeTypeInfo +806332fc Pages::VSPause::GetMessageBMG +80633304 Pages::VSPause::IsPausePage +8063330c Pages::VSPause::GetButtonsBRCTRName +80633334 Pages::VSPause::GetVariantsIdxArray +80633340 Pages::VSPause::GetButtonCount +80633348 Pages::VSPause::__dt +806333b4 Pages::VSPause::GetRuntimeTypeInfo +806333c0 Pages::GPPause::GetMessageBMG +806333c8 Pages::GPPause::IsPausePage +806333d0 Pages::GPPause::GetButtonsBRCTRName +806333dc Pages::GPPause::GetVariantsIdxArray +806333e8 Pages::GPPause::GetButtonCount +806333f0 Pages::GPPause::__dt +8063345c Pages::GPPause::GetRuntimeTypeInfo +80633468 Pages::TTPause::GetMessageBMG +80633470 Pages::TTPause::IsPausePage +80633478 Pages::TTPause::GetButtonsBRCTRName +80633484 Pages::TTPause::GetVariantsIdxArray +80633490 Pages::TTPause::GetButtonCount +80633498 Pages::TTPause::__dt +80633504 Pages::TTPause::GetRuntimeTypeInfo +80633510 Pages::RaceHUD::GetNextPage +80633518 Pages::LiveBattleViewHUD::GetCtrlRaceNameBalloonCount +80633520 Pages::LiveBattleViewHUD::GetEnabledCtrlRaceBases +80633528 Pages::LiveBattleViewHUD::GetPausePageId +80633530 Pages::LiveBattleViewHUD::__dt +8063359c Pages::LiveBattleViewHUD::GetRuntimeTypeInfo +806335a8 Pages::LiveViewHUD::GetCtrlRaceNameBalloonCount +806335b0 Pages::LiveViewHUD::GetEnabledCtrlRaceBases +806335b8 Pages::LiveViewHUD::GetPausePageId +806335c0 Pages::LiveViewHUD::__dt +8063362c Pages::LiveViewHUD::GetRuntimeTypeInfo +80633638 Pages::WiFiFriendTeamVSHUD::GetCtrlRaceNameBalloonCount +80633640 Pages::WiFiFriendTeamVSHUD::GetEnabledCtrlRaceBases +80633648 Pages::WiFiFriendTeamVSHUD::GetPausePageId +80633650 Pages::WiFiFriendTeamVSHUD::__dt +806336bc Pages::WiFiFriendVSHUD::GetRuntimeTypeInfo +806336c8 Pages::WiFiFriendVSHUD::GetCtrlRaceNameBalloonCount +806336d0 Pages::WiFiFriendVSHUD::GetEnabledCtrlRaceBases +806336d8 Pages::WiFiFriendVSHUD::GetPausePageId +806336e0 Pages::WiFiFriendVSHUD::__dt +8063374c Pages::WiFiFriendVSHUD::GetRuntimeTypeInfo +80633758 Pages::WifiVSMultiHUD::GetCtrlRaceNameBalloonCount +80633760 Pages::WifiVSMultiHUD::GetEnabledCtrlRaceBases +80633768 Pages::WifiVSMultiHUD::GetPausePageId +80633770 Pages::WifiVSMultiHUD::__dt +806337dc Pages::WifiVSMultiHUD::GetRuntimeTypeInfo +806337e8 Pages::WifiVSHUD::GetCtrlRaceNameBalloonCount +806337f0 Pages::WifiVSHUD::GetEnabledCtrlRaceBases +806337f8 Pages::WifiVSHUD::GetPausePageId +80633800 Pages::WifiVSHUD::__dt +8063386c Pages::WifiVSHUD::GetRuntimeTypeInfo +80633878 Pages::BattleMultiHUD::GetCtrlRaceNameBalloonCount +80633880 Pages::BattleMultiHUD::GetEnabledCtrlRaceBases +80633888 Pages::BattleMultiHUD::GetPausePageId +806338c0 Pages::BattleMultiHUD::__dt +8063392c Pages::BattleMultiHUD::GetRuntimeTypeInfo +80633938 Pages::BattleHUD::GetCtrlRaceNameBalloonCount +80633940 Pages::BattleHUD::GetEnabledCtrlRaceBases +80633948 Pages::BattleHUD::GetPausePageId +80633980 Pages::BattleHUD::__dt +806339ec Pages::BattleHUD::GetRuntimeTypeInfo +806339f8 Pages::VSMultiHUD::GetCtrlRaceNameBalloonCount +80633a00 Pages::VSMultiHUD::GetEnabledCtrlRaceBases +80633a08 Pages::VSMultiHUD::GetPausePageId +80633a10 Pages::VSMultiHUD::__dt +80633a7c Pages::VSMultiHUD::GetRuntimeTypeInfo +80633a88 Pages::VSHUD::GetCtrlRaceNameBalloonCount +80633a90 Pages::VSHUD::GetEnabledCtrlRaceBases +80633a98 Pages::VSHUD::GetPausePageId +80633aa0 Pages::VSHUD::__dt +80633b0c Pages::VSHUD::GetRuntimeTypeInfo +80633b18 Pages::GPHUD::GetCtrlRaceNameBalloonCount +80633b20 Pages::GPHUD::GetEnabledCtrlRaceBases +80633b28 Pages::GPHUD::GetPausePageId +80633b30 Pages::GPHUD::__dt +80633b9c Pages::GPHUD::GetRuntimeTypeInfo +80633ba8 Pages::TTHUD::GetCtrlRaceNameBalloonCount +80633bb0 Pages::TTHUD::GetEnabledCtrlRaceBases +80633bb8 Pages::TTHUD::GetPausePageId +80633c04 Pages::TTHUD::__dt +80633c70 Pages::TTHUD::GetRuntimeTypeInfo +80633c7c Pages::GhostRaceHUD::GetCtrlRaceNameBalloonCount +80633c84 Pages::GhostRaceHUD::GetEnabledCtrlRaceBases +80633c8c Pages::GhostRaceHUD::GetPausePageId +80633ccc Pages::GhostRaceHUD::__dt +80633d38 Pages::GhostRaceHUD::GetRuntimeTypeInfo +80633d44 Pages::RaceHUD::GetCtrlRaceNameBalloonCount +80633d4c Pages::GPReplayHUD::GetEnabledCtrlRaceBases +80633d54 Pages::GPReplayHUD::GetPausePageId +80633d5c Pages::GPReplayHUD::__dt +80633dc8 Pages::GPReplayHUD::GetRuntimeTypeInfo +80633dd4 TriggerPtmf +80633e04 TriggerPtmf +80633e34 TriggerPtmf +80633e64 TriggerPtmf +80633e94 TriggerPtmf +80633ec4 TriggerPtmf +80633ef4 TriggerPtmf +80633f24 TriggerPtmf +80633f54 TriggerPtmf +80633f84 TriggerPtmf +80633fb4 TriggerPtmf +80633fe4 TriggerPtmf +80634014 TriggerPtmf +80634044 TriggerPtmf +80634074 TriggerPtmf +806340a4 TriggerPtmf +806340d4 TriggerPtmf +80634104 TriggerPtmf +80634134 TriggerPtmf +80634164 TriggerPtmf +80634194 TriggerPtmf +806341c4 TriggerPtmf +806341f4 TriggerPtmf +80634224 TriggerPtmf +80634254 TriggerPtmf +80634284 TriggerPtmf +806342b4 TriggerPtmf +806342e4 TriggerPtmf +80634314 TriggerPtmf +80634344 TriggerPtmf +80634374 TriggerPtmf +806343a4 TriggerPtmf +806343d4 TriggerPtmf +80634404 TriggerPtmf +80634434 TriggerPtmf +80634464 TriggerPtmf +80634494 TriggerPtmf +806344c4 TriggerPtmf +806344f4 TriggerPtmf +80634524 TriggerPtmf +80634554 TriggerPtmf +80634584 TriggerPtmf +806345b4 TriggerPtmf +806345e4 TriggerPtmf +80634614 TriggerPtmf +80634644 TriggerPtmf +80634674 TriggerPtmf +806346a4 TriggerPtmf +806346d4 TriggerPtmf +80634704 TriggerPtmf +80634734 TriggerPtmf +80634764 TriggerPtmf +80634794 TriggerPtmf +806347c4 TriggerPtmf +806347f4 TriggerPtmf +80634824 TriggerPtmf +80634854 TriggerPtmf +80634884 TriggerPtmf +806348b4 TriggerPtmf +806348e4 TriggerPtmf +80634914 TriggerPtmf +80634944 TriggerPtmf +80634974 TriggerPtmf +806349a4 TriggerPtmf +806349d4 TriggerPtmf +80634a04 TriggerPtmf +80634a34 TriggerPtmf +80634a64 SectionMgr::CreateSystemPages +80634b80 SectionMgr::GetSectionPriority +80634c90 SectionMgr::CreateInstance +80634cc8 SectionMgr::DestroyInstance +80634d40 SectionMgr::__ct +80634dc4 SectionMgr::__dt +80634e44 SectionMgr::Init +80634fa0 SectionMgr::GetBootSectionSceneId +80634fbc SectionMgr::LoadSection +80635080 SectionMgr::EnterSection +8063519c SectionMgr::ExitSection +806354b8 0x806354b8 +806354c0 SectionMgr::RaceUpdate +8063550c SectionMgr::Update +8063583c SectionMgr::MenuUpdate +80635890 SectionMgr::UnkUpdate +80635a2c SectionMgr::Pause +80635a34 SectionMgr::Resume +80635a3c SectionMgr::SetNextSection +80635ac8 SectionMgr::RequestSceneChange +80635b2c SectionMgr::RequestSceneReinit +80635b70 SectionMgr::HandleHomeAction +80635c0c SectionMgr::RequestGoToWiiMenu +80635c74 SectionMgr::RequestPowerOff +80635cdc SectionMgr::GetNextSectionArchiveName +80635ce4 SectionMgr::NextSectionHasBackModel +80635cec SectionMgr::NextSectionHasDriverController +80635cf4 SectionMgr::UpdateStatusBitfield +80635e08 SectionMgr::UpdateDVDStatus +80635eb8 SheetSelectControl::GetClassName +80635ec4 SheetSelectControl::__ct +8063607c SheetSelectControl::__dt +80636120 SheetSelectControl::Load +806361e4 SheetSelectControl::Func_0x38 +806361e8 SheetSelectControl::SetRightArrowHandler +806361f0 SheetSelectControl::SetLeftArrowHandler +806361f8 SheetSelectControl::SetEnabledHudSlots +80636264 SheetSelectControl::Toggle +806362e8 SheetSelectControl::Init +8063634c SheetSelectControl::Update +80636350 SheetSelectControl::HandleRightArrowSelect +806363d4 SheetSelectControl::AfterRightScroll +806363d8 SheetSelectControl::HandleLeftArrowSelect +8063645c SheetSelectControl::AfterLeftScroll +80636460 SheetSelectControl::SheetSelectButton::GetClassName +80636470 SheetSelectControl::SheetSelectButton::__ct +80636564 SheetSelectControl::SheetSelectButton::__dt +806365cc SheetSelectControl::SheetSelectButton::Load +806367f8 SheetSelectControl::SheetSelectButton::Init +806369e4 SheetSelectControl::SheetSelectButton::Update +80636c2c SheetSelectControl::SheetSelectButton::SetEnabledHudSlots +80636c48 SheetSelectControl::SheetSelectButton::Toggle +80636c6c SheetSelectControl::SheetSelectButton::GetParentControl +80636d08 SheetSelectControl::SheetSelectButton::HandleSelect +80636fc4 SheetSelectControl::SheetSelectButton::HandleDeselect +806371ac SheetSelectControl::SheetSelectButton::Select +80637344 SheetSelectControl::SheetSelectButton::SetPositionAnim +80637474 SheetSelectControl::GetArrowAnimationType +8063747c SheetSelectControlScaleFade::GetClassName +8063748c SheetSelectControlScaleFade::GetArrowAnimationType +80637494 SheetSelectControlScaleFade::GetRuntimeTypeInfo +806374a0 SheetSelectControl::GetRuntimeTypeInfo +806374ac SheetSelectControl::SheetSelectButton::GetRuntimeTypeInfo +806374b8 SheetSelectControl::SheetSelectButton::TriggerPtmf +806374e8 SheetSelect_cpp_sinit_ +80637514 Pages::ESRBNotice::__ct +80637560 Pages::ESRBNotice::__dt +806375d4 Pages::ESRBNotice::OnInit +80637678 Pages::ESRBNotice::AfterControlUpdate +806376e8 Pages::Suggest60Hz::__ct +80637734 Pages::Suggest60Hz::__dt +806377a8 Pages::Suggest60Hz::OnInit +80637850 Pages::Suggest60Hz::OnActivate +8063789c Pages::Suggest60Hz::AfterControl +80637954 Pages::Suggest60Hz::IsHomeMenuWorking +8063795c Pages::Suggest60Hz::GetRuntimeTypeInfo +80637968 Pages::ESRBNotice::IsHomeMenuWorking +80637970 Pages::ESRBNotice::GetRuntimeTypeInfo +8063797c NoticePages_cpp_sinit_ +80637998 SystemBMGHolder::__ct +806379c8 SystemBMGHolder::__dt +80637a20 SystemBMGHolder::Init +80637a8c SystemBMGHolder::GetMessage +80637ac4 TabControl::GetClassName +80637ad0 TabControl::__ct +80637b2c TabControl::__dt +80637ba4 TabControl::Load +80637d20 TabControl::OnLoadingEnd +80637d24 TabControl::SetOnClickHandler +80637d2c TabControl::SetEnabledHudSlots +80637da8 TabControl::SelectInitial +80637e48 TabControl::SelectButtonNoPtmf +80637ec0 TabControl::Init +80637f94 TabControl::Update +80637f98 TabControl::SelectButton +80638038 TabControl::OptionButton::GetClassName +80638048 TabControl::OptionButton::__ct +8063813c TabControl::OptionButton::__dt +806381a4 TabControl::OptionButton::Load +80638370 TabControl::OptionButton::Init +8063851c TabControl::OptionButton::Update +806386c8 TabControl::OptionButton::SetEnabledHudSlots +806386d0 TabControl::OptionButton::GetParentControl +806387d0 TabControl::OptionButton::SetInitialState +80638830 TabControl::OptionButton::ChangeButtonState +80638988 TabControl::OptionButton::HandleSelect +80638a7c TabControl::OptionButton::HandleDeselect +80638c64 TabControl::OptionButton::HandleClick +80638e20 TabControl::Func_0x40 +80638e24 TabControl::GetRuntimeTypeInfo +80638e30 TabControl::OptionButton::GetRuntimeTypeInfo +80638e3c TabControl::OptionButton::TriggerPtmf +80638e6c TabControl_cpp_sinit_ +80638e88 Pages::GhostSelectSupporting::__ct +80638ecc Pages::GhostSelectSupporting::__dt +80638f34 Pages::GhostSelectSupporting::OnInit +80638fa0 Pages::GhostSelectSupporting::OnActivate +80639144 Pages::GhostSelectSupporting::BeforeEntranceAnimations +80639148 Pages::GhostSelectSupporting::BeforeExitAnimations +8063914c Pages::GhostSelectSupporting::AfterControlUpdate +80639260 Pages::GhostSelectSupporting::OnResume +806393c8 Pages::GhostSelectSupporting::InitGhostList +806395ec Pages::GhostSelect::__ct +8063982c Pages::GhostSelect::__dt +80639908 Pages::GhostSelect::OnInit +80639bd0 Pages::GhostSelect::OnActivate +80639d34 Pages::GhostSelect::BeforeEntranceAnimations +80639d6c Pages::GhostSelect::OnResume +8063a1a4 Pages::GhostSelect::OnRightArrowPress +8063a2a4 Pages::GhostSelect::OnLeftArrowPress +8063a3a0 Pages::GhostSelect::OnChallengeGhostPress +8063a3c0 Pages::GhostSelect::OnWatchReplayPress +8063a3e0 Pages::GhostSelect::OnSoloTTPress +8063a400 0x8063a400 +8063a444 Pages::GhostSelect::OnBackPress +8063a45c 0x8063a45c +8063a4b8 Pages::GhostSelect::GetNextPage +8063a4c0 Pages::GhostSelect::GetRuntimeTypeInfo +8063a4cc Pages::GhostSelectSupporting::GetNextPage +8063a4d4 Pages::GhostSelectSupporting::GetRuntimeTypeInfo +8063a4e0 TriggerPtmf +8063a510 TriggerPtmf +8063a540 TriggerPtmf +8063a570 GhostSelectPage_cpp_sinit_ +8063a58c TitleImage::GetClassName +8063a598 TitleImage::__ct +8063a5d4 TitleImage::__dt +8063a62c TitleImage::Load +8063a6cc TitleImage::InitSelf +8063a704 0x8063a704 +8063a7b4 0x8063a7b4 +8063a7ec 0x8063a7ec +8063a89c 0x8063a89c +8063a8d4 TitleImage::ChangeImage +8063aa08 Pages::Title::__ct +8063aa8c CtrlMenuPressStart::__dt +8063aae4 Pages::Title::__dt +8063ab6c Pages::Title::OnInit +8063ad60 Pages::Title::OnDispose +8063ad64 Pages::Title::OnActivate +8063ade4 Pages::Title::BeforeEntranceAnimations +8063ae94 Pages::Title::AfterEntranceAnimations +8063aee0 Pages::Title::BeforeExitAnimations +8063af90 Pages::Title::AfterExitAnimations +8063afc8 Pages::Title::AfterControlUpdate +8063b124 Pages::Title::OnResume +8063b130 Pages::Title::PrepareDemo +8063b514 Pages::BlurryTitle::__ct +8063b560 Pages::BlurryTitle::__dt +8063b5d4 Pages::BlurryTitle::OnInit +8063b698 Pages::BlurryTitle::OnActivate +8063b7f0 Pages::BlurryTitle::BeforeEntranceAnimations +8063b7f4 TitleMovieControl::GetClassName +8063b804 TitleMovieControl::__ct +8063b840 TitleMovieControl::__dt +8063b898 TitleMovieControl::Load +8063b938 TitleMovieControl::InitSelf +8063b970 TitleMovieControl::OnUpdate +8063b974 0x8063b974 +8063ba24 0x8063ba24 +8063bad4 0x8063bad4 +8063bb08 Pages::TitleMovie::__ct +8063bb6c Pages::TitleMovie::__dt +8063bbe4 Pages::TitleMovie::OnInit +8063bcf8 Pages::TitleMovie::OnActivate +8063be44 Pages::TitleMovie::AfterExitAnimations +8063be88 Pages::TitleMovie::OnDeactivate +8063be9c Pages::TitleMovie::BeforeControlUpdate +8063bfcc Pages::TitleMovie::AfterControlUpdate +8063c0ac Pages::BlinkingPressA::__ct +8063c110 Pages::BlinkingPressA::__dt +8063c188 Pages::BlinkingPressA::OnInit +8063c210 Pages::BlinkingPressA::OnActivate +8063c268 Pages::BlinkingPressA::OnDeactivate +8063c27c Pages::BlinkingPressA::AfterControlUpdate +8063c368 Pages::BlinkingPressA::GetRuntimeTypeInfo +8063c374 Pages::TitleMovie::GetRuntimeTypeInfo +8063c380 TitleMovieControl::GetRuntimeTypeInfo +8063c38c Pages::BlurryTitle::GetRuntimeTypeInfo +8063c398 Pages::Title::GetNextPage +8063c3a0 Pages::Title::GetRuntimeTypeInfo +8063c3ac TitleImage::GetRuntimeTypeInfo +8063c3b8 Title_cpp_sinit_ +8063c3ec UIAnimator::__ct +8063c404 UIAnimator::__dt +8063c464 UIAnimator::SetLayout +8063c470 UIAnimator::Init +8063c4d4 UIAnimator::InitAnimationGroup +8063c53c Animation::__ct +8063c560 UIAnimator::AppendGroup +8063c650 UIAnimator::LoadNewAnimation +8063c714 UIAnimator::LoadAnimation +8063c7bc UIAnimator::Update +8063c820 UIAnimator::GetAnimationGroupById +8063c830 UIAnimator::GetAnimationGroupById +8063c840 0x8063c840 +8063c844 AnimationGroup::__ct +8063c8ac AnimationGroup::__dt +8063c91c AnimationGroup::PlayAnimationAtFrame +8063ca98 AnimationGroup::PlayAnimationAtPercent +8063cb20 AnimationGroup::PlayAnimationAtFrameAndDisable +8063cb54 AnimationGroup::PlayAnimationAtPercentAndDisable +8063cbe4 AnimationGroup::Update +8063cddc 0x8063cddc +8063ce6c UIControl::__ct +8063cf60 0x8063cf60 +8063cfa0 UIControl::__dt +8063cffc UIControl::8063cffc +8063d004 UIControl::Init +8063d044 UIControl::Update +8063d084 UIControl::Draw +8063d0b8 UIControl::SetPosition +8063d0e0 UIControl::PlayAnimationSound +8063d194 UIControl::SetPositionAnim +8063d268 UIControl::InitControlGroup +8063d278 UIControl::AddControl +8063d284 UIControl::GetAnimationDelay +8063d398 UIControl::SetParentControlGroup +8063d3a4 UIControl::SetSoundIds +8063d3b0 UIControl::GetClassName +8063d3c0 0x8063d3c0 +8063d3cc UIControl::PositionFromBasePosition +8063d550 UIControl::CalculatePosition +8063d608 UIControl::vf_0x30 +8063d648 UIControl::8063d648 +8063d74c UIControl::PlaySound +8063d764 UIControl::StopSound +8063d770 UIControl::HoldSound +8063d788 LayoutUIControl::GetClassName +8063d798 LayoutUIControl::__ct +8063d8c0 LayoutUIControl::__dt +8063d954 LayoutUIControl::LoadLayout +8063d9c0 LayoutUIControl::LoadPictureLayout +8063da00 LayoutUIControl::SetRootPane +8063dac0 LayoutUIControl::Init +8063db00 LayoutUIControl::Update +8063db84 LayoutUIControl::Draw +8063dcbc LayoutUIControl::SetTextBoxMessage +8063ddb4 LayoutUIControl::SetMessage +8063ddf4 LayoutUIControl::MessageRequester::Calc +8063deec LayoutUIControl::ResetTextBoxMsg +8063dfc8 LayoutUIControl::ResetMsg +8063e000 LayoutUIControl::MessageResetter::Calc +8063e0f0 LayoutUIControl::SetPicturePane +8063e328 LayoutUIControl::PicturePaneExists +8063e3dc LayoutUIControl::SetMiiPane +8063e54c LayoutUIControl::SetPaneVisibility +8063e58c LayoutUIControl::CropMovie(Rect) +8063e5c4 LayoutUIControl::CropMovie(4floats) +8063e61c LayoutUIControl::vf_0x30 +8063e664 LayoutUIControlScaleFade::GetClassName +8063e674 LayoutUIControlScaleFade::__ct +8063e7a8 LayoutUIControlScaleFade::__dt +8063e840 LayoutUIControlScaleFade::SetPositionAnim +8063e924 LayoutUIControlScaleFade::GetRuntimeTypeInfo +8063e930 LayoutUIControlcpp_sinit_ +8063e954 UpDownControl::GetClassName +8063e960 UpDownControl::__ct +8063eb5c UpDownControl::__dt +8063ebdc UpDownControl::Load +8063f01c UpDownControl::OnLoadingEnd +8063f020 UpDownControl::SetOnChangeHandler +8063f028 UpDownControl::SetOnClickHandler +8063f030 UpDownControl::SetOnElementSelectHandler +8063f038 UpDownControl::SetOnDeselectHandler +8063f040 0x8063f040 +8063f0ac 0x8063f0ac +8063f0c0 UpDownControl::SelectInitial(count+initial) +8063f0e0 UpDownControl::SelectInitial(initial) +8063f0f8 0x8063f0f8 +8063f160 UpDownControl::SetPlayerBitfield +8063f16c UpDownControl::Init +8063f2a8 UpDownControl::Update +8063f694 UpDownControl::HandleSelect +8063f7c4 UpDownControl::HandleDeselect +8063f8c8 UpDownControl::HandleButtonSelect +8063f8e0 UpDownControl::HandleButtonDeselect +8063f8f8 UpDownControl::HandleClick +8063fb64 UpDownControl::HandleRightPress +8063fc90 UpDownControl::HandleLeftPress +8063fdb0 UpDownControl::UpDownButton::GetClassName +8063fdc0 UpDownControl::UpDownButton::__ct +8063fdfc UpDownControl::UpDownButton::__dt +8063fe54 UpDownControl::UpDownButton::Load +8063ff18 UpDownControl::UpDownButton::Init +8063ffb0 UpDownControl::UpDownButton::Update +8064009c UpDownControl::UpDownButton::GetParentControl +8064019c UpDownControl::UpDownButton::ToggleSelect +80640360 0x80640360 +806403c4 0x806403c4 +80640508 0x80640508 +806405b0 0x806405b0 +806405b8 0x806405b8 +806405c0 TextUpDownValueControl::GetClassName +806405d0 TextUpDownValueControl::__ct +8064063c TextUpDownValueControl::__dt +806406b0 TextUpDownValueControl::Load +806407d4 TextUpDownValueControl::SetOnTextChangeHandler +806407dc TextUpDownValueControl::Init +806408fc TextUpDownValueControl::Update +806409a0 0x806409a0 +80640aa0 TextUpDownValueControl::InitText +80640b68 TextUpDownValueControl::OnSelect +80640cdc TextUpDownValueControl::SetMessage +80640d0c TextUpDownValueControl::SetMessageAnimated +80640dfc TextUpDownValueControl::OnClick +80640e68 TextUpDownValueControl::IsOKStop +80640f00 TextUpDownValueControl::Get3rdAnimStartFrame +80640f74 TextUpDownValueControl::SetBorderColours +80641028 TextUpDownValueControl::SetColor_BaseColours +806410d0 TextUpDownValueControl::TextControl::GetClassName +806410e0 TextUpDownValueControl::TextControl::__ct +8064111c TextUpDownValueControl::TextControl::__dt +80641174 TextUpDownValueControl::TextControl::Load +806411e8 TextUpDownValueControl::TextControl::InitSelf +80641298 0x80641298 +806412f8 TextUpDownValueControl::TextControl::ToggleState +8064145c UpDownControlOld::1 +8064146c UpDownControlOld::2 +80641624 UpDownControlOld::3 +8064168c UpDownControlOld::4 +806419d4 UpDownControlOld::5 +806419dc UpDownControlOld::6 +80641ac0 UpDownControlOld::7 +80641c8c UpDownControlOld::8 +80641ca0 0x80641ca0 +80641ca8 UpDownControlOld::9 +80641d88 UpDownControlOld::10 +80641d8c UpDownControlOld::11 +80641f8c UpDownControlOld::12 +80641f90 UpDownControlOld::13 +80642070 UpDownControlOld::14 +806422b8 UpDownControlOld::15 +80642370 UpDownControlOld::16 +80642424 UpDownControlOld::17 +80642574 UpDownControlOld::18 +8064262c UpDownControlOld::19 +80642630 UpDownControlOld::20 +806426e4 UpDownControlOld::21 +806426e8 TextUpDownControlOld::1 +806426f8 TextUpDownControlOld::2 +806428bc TextUpDownControlOld::3 +80642928 TextUpDownControlOld::4 +8064295c 0x8064295c +80642964 0x80642964 +8064296c 0x8064296c +80642970 0x80642970 +8064297c 0x8064297c +80642988 TextUpDownValueControl::GetTextControl +8064298c TextUpDownValueControl::GetRuntimeTypeInfo +80642998 TextUpDownValueControl::TextControl::GetRuntimeTypeInfo +806429a4 UpDownControl::Func_0x40 +806429a8 UpDownControl::GetRuntimeTypeInfo +806429b4 UpDownControl::UpDownButton::GetRuntimeTypeInfo +806429c0 TriggerPtmf +806429f0 TriggerPtmf +80642a20 UpDownControl_cpp_sinit_ +80642a50 TextUpDownValueControl::SetColor_BaseColours(UpDownDisplayedText) +80642a58 TextUpDownValueControl::SetBorderColours(UpDownDisplayedText) +80642a60 TextUpDownValueControl::Get3rdAnimStartFrame(UpDownDisplayedText) +80642a68 TextUpDownValueControl::Animate(UpDownDisplayedText) +80642a70 TextUpDownValueControl::OnClick(UpDownDisplayedText) +80642a78 TextUpDownValueControl::SetMessageAnimated(UpDownDisplayedText) +80642a80 TextUpDownValueControl::SetMessage(UpDownDisplayedText) +80642a88 TextUpDownValueControl::OnSelect(UpDownDisplayedText) +80642a90 TextUpDownValueControl::InitText(UpDownDisplayedText) +80642a98 TextUpDownValueControl::GetTextControl(UpDownDisplayedText) +80642aa0 VoteControl::GetClassName +80642aac VoteControl::__ct +80642ae8 VoteControl::__dt +80642b40 VoteControl::Load +80642bec VoteControl::InitSelf +80642ca0 VoteControl::OnUpdate +80642ca4 VoteControl::Fill +80642e14 VoteControl::AnimateSelect +80642e68 VoteControl::AnimateDeselect +80642ea0 VoteControl::AnimateChose +80642efc VoteControl::AnimateFadeOut +80642f58 VoteControl::IsAnimationShow +80642f90 Pages::Vote::__ct +806430bc Pages::Vote::__dt +80643168 Pages::Vote::OnInit +80643208 Pages::Vote::OnInit_switch +8064320c Pages::Vote::OnInit_case58 +80643218 Pages::Vote::OnInit_case59 +80643220 Pages::Vote::OnInit_case5a +806433c8 Pages::Vote::OnDispose +806433cc Pages::Vote::BeforeEntranceAnimations +806437e8 Pages::Vote::BeforeControlUpdate +80643d24 Pages::Vote::BeforeControlUpdate_switch +80643d28 Pages::Vote::BeforeControlUpdate_case58 +80643d30 Pages::Vote::BeforeControlUpdate_case59 +80643d38 Pages::Vote::BeforeControlUpdate_case5e +80643d40 Pages::Vote::BeforeControlUpdate_case5f +80643d48 Pages::Vote::BeforeControlUpdate_case60 +80643d50 Pages::Vote::BeforeControlUpdate_case61 +80643d58 Pages::Vote::BeforeControlUpdate_case62 +80643d60 Pages::Vote::BeforeControlUpdate_case63 +80643d68 Pages::Vote::BeforeControlUpdate_case64 +80643d70 Pages::Vote::BeforeControlUpdate_case65 +80643d78 Pages::Vote::BeforeControlUpdate_case66 +80643d80 Pages::Vote::BeforeControlUpdate_case67 +80643d84 Pages::Vote::BeforeControlUpdate_caseDefault +80643e98 Pages::Vote::BeforeExitAnimations +80643f18 Pages::Vote::GetNextPage +80643f48 Pages::Vote::SetVotedCourseId +80643f78 Pages::Vote::SetCourseButtonId_switch +80643f7c Pages::Vote::SetCourseButtonId_case58 +80643f84 Pages::Vote::SetCourseButtonId_case59 +80643f88 Pages::Vote::SetCourseButtonId_case5a +80643f8c Pages::Vote::FillVoteControl +806441e8 Pages::Vote::StartRoulette +80644430 Pages::Vote::FillVotesCounter +806445a8 Pages::Vote::OnMessageBoxClick +8064464c Pages::Vote::GetRuntimeTypeInfo +80644658 VoteControl::GetRuntimeTypeInfo +80644664 TriggerPtmf +80644694 VotePage_cpp_sinit_ +806446b8 0x806446b8 +806446fc 0x806446fc +80644764 0x80644764 +806447c0 0x806447c0 +806448b8 Page::EndExitState +806448bc 0x806448bc +806448c0 0x806448c0 +806449e8 0x806449e8 +806449f8 0x806449f8 +80644b50 0x80644b50 +80644bf0 0x80644bf0 +80644d58 0x80644d58 +80644e40 0x80644e40 +80645078 0x80645078 +80645110 0x80645110 +80645178 0x80645178 +80645348 0x80645348 +80645354 0x80645354 +8064535c 0x8064535c +80645368 0x80645368 +80645398 0x80645398 +806453c8 0x806453c8 +806453e4 0x806453e4 +806453f0 0x806453f0 +8064542c 0x8064542c +80645484 0x80645484 +80645524 0x80645524 +8064555c 0x8064555c +80645594 0x80645594 +806455cc WifiAwardResultItem::GetClassName +806455dc WifiAwardResultItem::__ct +80645618 WifiAwardResultItem::__dt +80645670 WifiAwardResultItem::Load +806457e8 WifiAwardResultItem::InitSelf +80645820 0x80645820 +80645bc8 Pages::WiFiVSResults::__ct +80645c8c Pages::WiFiVSResults::__dt +80645d28 Pages::WiFiVSResults::OnInit +8064605c Pages::WiFiVSResults::OnActivate +80646148 Pages::WiFiVSResults::BeforeEntranceAnimations +8064615c Pages::WiFiVSResults::AfterEntranceAnimations +806461b4 Pages::WiFiVSResults::BeforeExitAnimations +806461c8 Pages::WiFiVSResults::AfterControlUpdate +80646200 Pages::WiFiVSResults::OnClick +8064620c 0x8064620c +80646278 0x80646278 +806463a0 0x806463a0 +806465e0 0x806465e0 +80646750 Pages::WiFiVSResults::GetNextPage +80646758 Pages::WiFiVSResults::GetRuntimeTypeInfo +80646764 WifiAwardResultItem::GetRuntimeTypeInfo +80646770 0x80646770 +8064677c TriggerPtmf +806467ac WiFiVSResults_cpp_sinit_ +806467d4 Pages::WFCConnect::__ct +80646850 Pages::WFCConnect::__dt +806468b8 Pages::WFCConnect::OnInit +80646924 Pages::WFCConnect::OnActivate +80646dd4 Pages::WFCConnect::BeforeEntranceAnimations +80646dd8 Pages::WFCConnect::AfterEntranceAnimations +80646df8 Pages::WFCConnect::BeforeExitAnimations +80646dfc Pages::WFCConnect::AfterExitAnimations +80646e00 Pages::WFCConnect::OnDeactivate +80646eb0 Pages::WFCConnect::AfterControlUpdate +806474e8 Pages::WFCConnect::OnResume +806478c4 Pages::WFCConnect::GetNextPage +80647998 Pages::WFCConnect::IsHomeMenuWorking +806479dc Pages::WFCConnect::SetText +80647cec Pages::WFCConnect::GetTitleBMGId +80647db8 Pages::WFCConnect::OnDisconnect +80647ecc Pages::WFCDataStatement::__ct +80647fe4 Pages::WFCDataStatement::__dt +80648084 Pages::WFCDataStatement::OnInit +80648220 Pages::WFCDataStatement::OnActivate +806483c8 Pages::WFCDataStatement::SetType +80648404 Pages::WFCDataStatement::OnButtonClick +80648480 Pages::WFCDataStatement::OnBackPress +806484a8 Pages::WFCAllowData::__ct +80648610 Pages::WFCAllowData::__dt +806486bc Pages::WFCAllowData::OnInit +806488b4 Pages::WFCAllowData::OnActivate +80648a24 Pages::WFCAllowData::OnResume +80648afc Pages::WFCAllowData::OnButtonClick +80648c9c Pages::WFCAllowData::OnBackButtonClick +80648cb4 Pages::WFCAllowData::OnBackPress +80648de8 Pages::WFCDisconnect::__ct +80648e64 Pages::WFCDisconnect::__dt +80648ecc Pages::WFCDisconnect::OnInit +80648f28 Pages::WFCDisconnect::OnActivate +80649124 Pages::WFCDisconnect::BeforeEntranceAnimations +80649128 Pages::WFCDisconnect::BeforeExitAnimations +8064912c Pages::WFCDisconnect::OnDeactivate +806491dc Pages::WFCDisconnect::OnResume +806491ec Pages::WFCDisconnect::OnYesNoYesClick +806492cc Pages::WFCDisconnect::GetRuntimeTypeInfo +806492d8 Pages::WFCAllowData::GetRuntimeTypeInfo +806492e4 Pages::WFCDataStatement::GetRuntimeTypeInfo +806492f0 Pages::WFCConnect::GetRuntimeTypeInfo +806492fc TriggerPtmf +8064932c TriggerPtmf +8064935c TriggerPtmf +8064938c TriggerPtmf +806493bc TriggerPtmf +806493ec TriggerPtmf +8064941c TriggerPtmf +8064944c WFCConnectPage_cpp_sinit_ +80649470 Pages::DisconnectMgr::__ct +806494bc Pages::DisconnectMgr::__dt +80649524 Pages::DisconnectMgr::OnInit +80649590 Pages::DisconnectMgr::OnUpdate +80649988 Pages::DisconnectError::__ct +80649a14 Pages::DisconnectError::__dt +80649a94 Pages::DisconnectError::OnInit +80649b90 Pages::DisconnectError::OnActivate +80649d18 Pages::DisconnectError::AfterControlUpdate +80649d60 Pages::DisconnectError::BeforeEntranceAnimations +80649d6c Pages::DisconnectError::IsHomeMenuWorking +80649da4 Pages::DisconnectError::GetErrorBMGId +80649fd8 Pages::DisconnectError::OnClick +8064a04c Pages::DisconnectError::GetRuntimeTypeInfo +8064a058 Pages::DisconnectMgr::GetRuntimeTypeInfo +8064a064 TriggerPtmf +8064a094 DisconnectPage_cpp_sinit_ +8064a0b0 Pages::LiveViewWaiting::__ct +8064a10c Pages::LiveViewWaiting::__dt +8064a198 Pages::LiveViewWaiting::OnInit +8064a298 Pages::LiveViewWaiting::AfterControlUpdate +8064a2d8 Pages::LiveViewWaiting::GetNextPage +8064a324 Pages::LiveViewWaiting::BeforeExitAnimations +8064a370 Pages::LiveViewWaiting::BeforeEntranceAnimations +8064a3c8 Pages::LiveViewWaiting::AfterExitAnimations +8064a3d4 Pages::LiveViewWaiting::GetRuntimeTypeInfo +8064a3e0 LiveViewWaitingPage_cpp_sinit_ +8064a3f4 Pages::VR::__ct +8064a4e0 Pages::VR::__dt +8064a58c Pages::VR::OnInit +8064a670 Pages::VR::OnInit_switch +8064a674 Pages::VR::OnInit_case58 +8064a688 Pages::VR::OnInit_case59 +8064a698 Pages::VR::OnInit_case5a +8064a84c Pages::VR::OnActivate +8064a908 Pages::VR::OnActivate_switch +8064a90c Pages::VR::OnActivate_case58 +8064a914 Pages::VR::OnActivate_case59 +8064a918 Pages::VR::OnActivate_case5a +8064aac4 Pages::VR::AfterControlUpdate +8064ab08 Pages::VR::FillVRControl +8064ae6c Pages::VR::OnButtonClick +8064aea8 Pages::VR::GetRuntimeTypeInfo +8064aeb4 TriggerPtmf +8064aee4 VRPage_cpp_sinit_ +8064aef8 Pages::Approve2PWFC::__ct +8064af74 Pages::Approve2PWFC::__dt +8064afdc Pages::Approve2PWFC::OnInit +8064b048 Pages::Approve2PWFC::OnActivate +8064b15c Pages::Approve2PWFC::BeforeEntranceAnimations +8064b160 Pages::Approve2PWFC::BeforeExitAnimations +8064b164 Pages::Approve2PWFC::OnResume +8064b224 Pages::Approve2PWFC::GetNextPage +8064b23c Pages::Approve2PWFC::OnApprove +8064b2b4 FriendsStatusButton::GetClassName +8064b2c0 FriendsStatusButton::__ct +8064b2fc FriendsStatusButton::__dt +8064b354 FriendsStatusButton::Load +8064b444 FriendsStatusButton::InitSelf +8064b450 FriendsStatusButton::OnUpdate +8064b4c0 FriendsStatusButton::OnSelect +8064b4c4 FriendsStatusButton::OnDeselect +8064b4c8 FriendsStatusButton::UpdateFriendStatus +8064b640 Pages::WFCMainMenu::__ct +8064b850 Pages::WFCMainMenu::__dt +8064b910 Pages::WFCMainMenu::OnInit +8064bc90 Pages::WFCMainMenu::OnActivate +8064bce8 Pages::WFCMainMenu::OnWorldwideButtonClick +8064bdc4 Pages::WFCMainMenu::OnRegionalButtonClick +8064bea0 Pages::WFCMainMenu::OnFriendsButtonClick +8064bee4 Pages::WFCMainMenu::OnBackButtonClick +8064bf4c Pages::WFCMainMenu::OnButtonSelect +8064bfb8 Pages::WFCMainMenu::OnBackPress +8064bfd8 Pages::WFCModeSelect::__ct +8064c170 Pages::WFCModeSelect::__dt +8064c220 Pages::WFCModeSelect::OnInit +8064c4f8 Pages::WFCModeSelect::OnActivate +8064c610 Pages::WFCModeSelect::OnModeButtonClick +8064c718 Pages::WFCModeSelect::OnBackButtonClick +8064c75c Pages::WFCModeSelect::OnModeButtonSelect +8064c7b0 Pages::WFCModeSelect::OnBackPress +8064c7c8 Pages::WFCFriendsMenu::__ct +8064c9d8 Pages::WFCFriendsMenu::__dt +8064ca98 Pages::WFCFriendsMenu::OnInit +8064cf18 Pages::WFCFriendsMenu::OnActivate +8064cff8 Pages::WFCFriendsMenu::OnDeactivate +8064d0a0 Pages::WFCFriendsMenu::OnRosterButtonClick +8064d1e8 Pages::WFCFriendsMenu::OnRegisterButtonClick +8064d330 Pages::WFCFriendsMenu::OnCreateRoomClick +8064d4f4 Pages::WFCFriendsMenu::OnBackButtonClick +8064d5a0 Pages::WFCFriendsMenu::OnButtonSelect +8064d60c Pages::WFCFriendsMenu::OnBackPress +8064d660 Pages::WFCFriendsMenu::GetNextPage +8064d668 Pages::WFCFriendsMenu::GetRuntimeTypeInfo +8064d674 Pages::WFCModeSelect::GetNextPage +8064d67c Pages::WFCModeSelect::GetRuntimeTypeInfo +8064d688 Pages::WFCMainMenu::GetNextPage +8064d690 Pages::WFCMainMenu::GetRuntimeTypeInfo +8064d69c FriendsStatusButton::GetRuntimeTypeInfo +8064d6a8 Pages::Approve2PWFC::GetRuntimeTypeInfo +8064d6b4 TriggerPtmf +8064d6e4 TriggerPtmf +8064d714 TriggerPtmf +8064d744 TriggerPtmf +8064d774 TriggerPtmf +8064d7a4 TriggerPtmf +8064d7d4 TriggerPtmf +8064d804 WFCMainMenuPage__cpp__sinit +8064d834 Pages::WWRaceEndCountDown::__ct +8064d888 Pages::WWRaceEndCountDown::__dt +8064d908 Pages::WWRaceEndCountDown::OnInit +8064d994 Pages::WWRaceEndCountDown::OnActivate +8064db00 Pages::WWRaceEndCountDown::BeforeControlUpdate +8064db4c FriendStatusButtonRaceAfter::GetClassName +8064db5c 0x8064db5c +8064db68 FriendStatusButtonRaceAfter::__ct +8064dba4 FriendStatusButtonRaceAfter::__dt +8064dbfc FriendStatusButtonRaceAfter::8064dbfc +8064dd38 FriendStatusButtonRaceAfter::OnInit +8064dd3c FriendStatusButtonRaceAfter::OnUpdate +8064dd9c FriendStatusButtonRaceAfter::UpdateStatus +8064dee8 Pages::WWRaceEnd::__ct +8064e014 Pages::WWRaceEnd::__dt +8064e0b0 Pages::WWRaceEnd::OnInit +8064e3d8 Pages::WWRaceEnd::OnActivate +8064e4dc Pages::WWRaceEnd::BeforeEntranceAnimations +8064e4f0 Pages::WWRaceEnd::AfterControlUpdate +8064e690 Pages::WWRaceEnd::OnContinueRaceClick +8064e80c Pages::WWRaceEnd::OnFriendButtonClick +8064e8ec Pages::WWRaceEnd::OnQuitClick +8064e9d0 Pages::WWRaceEndQuit::__ct +8064ea94 Pages::WWRaceEndQuit::__dt +8064eb20 Pages::WWRaceEndQuit::OnInit +8064ecb4 Pages::WWRaceEndQuit::OnDispose +8064ed34 Pages::WWRaceEndQuit::OnActivate +8064edf8 Pages::WWRaceEndQuit::OnDeactivate +8064ee04 Pages::WWRaceEndQuit::AfterControlUpdate +8064efa4 Pages::WWRaceEndQuit::OnYesButtonClick +8064f124 Pages::WWRaceEndQuit::OnNoButtonClick +8064f168 Pages::WWRaceEndWait::__ct +8064f1b4 Pages::WWRaceEndWait::__dt +8064f228 Pages::WWRaceEndWait::OnInit +8064f26c Pages::WWRaceEndWait::OnInit_switch +8064f270 Pages::WWRaceEndWait::OnInit_case_68 +8064f28c Pages::WWRaceEndWait::OnInit_case_69 +8064f320 Pages::WWRaceEndWait::OnActivate +8064f374 Pages::WWRaceEndWait::AfterControlUpdate +8064f424 Pages::WWRaceEndWait::AfterControlUpdate_switch +8064f428 Pages::WWRaceEndWait::AfterControlUpdate_case_68 +8064f430 Pages::WWRaceEndWait::AfterControlUpdate_case_69 +8064f438 Pages::WWRaceEndWait::AfterControlUpdate_case_6c +8064f440 Pages::WWRaceEndWait::AfterControlUpdate_case_6d +8064f5b8 Pages::WWRaceEndWait::AfterControlUpdate_switch2D +8064f5bc Pages::WWRaceEndWait::AfterControlUpdate_case2_70 +8064f5c4 Pages::WWRaceEndWait::AfterControlUpdate_case2_71 +8064f5cc Pages::WWRaceEndWait::AfterControlUpdate_case2_72 +8064f5d4 Pages::WWRaceEndWait::AfterControlUpdate_case2_73 +8064f5dc Pages::WWRaceEndWait::AfterControlUpdate_case2_74 +8064f5e4 Pages::WWRaceEndWait::AfterControlUpdate_case2_75 +8064f5ec Pages::WWRaceEndWait::AfterControlUpdate_case2_76 +8064f5f4 Pages::WWRaceEndWait::AfterControlUpdate_case2_77 +8064f65c 0x8064f65c +8064f7ec 0x8064f7ec +8064f8e0 0x8064f65c_switch +8064f8e4 0x8064f65c_caseD_68 +8064f93c 0x8064f65c_caseD_6c +8064f988 0x8064f65c_caseD_6a +8064f9d8 Pages::DisableHomeButton::__ct +8064fa1c Pages::DisableHomeButton::__dt +8064fa84 Pages::DisableHomeButton::OnInit +8064fae0 Pages::DisableHomeButton::IsHomeMenuWorking +8064fae8 Pages::DisableHomeButton::GetRuntimeTypeInfo +8064faf4 Pages::RaceEndWait::GetRuntimeTypeInfo +8064fb00 Pages::WWRaceEndQuit::GetNextPage +8064fb08 Pages::WWRaceEndQuit::GetRuntimeTypeInfo +8064fb14 Pages::WWRaceEnd::GetNextPage +8064fb1c Pages::WWRaceEnd::GetRuntimeTypeInfo +8064fb28 FriendStatusButtonRaceAfter::GetRuntimeTypeInfo +8064fb34 Pages::WWRaceEndCountDown::GetNextPage +8064fb3c Pages::WWRaceEndCountDown::GetRuntimeTypeInfo +8064fb48 TriggerPtmf +8064fb78 TriggerPtmf +8064fba8 WWRaceSupportingPages_cpp_sinit_ +8064fbdc Pages::SELECTStageMgr::__ct +8064fc70 Pages::SELECTStageMgr::__dt +8064fcfc Pages::SELECTStageMgr::OnInit +8064fd54 Pages::SELECTStageMgr::OnInit_switch +8064fd58 Pages::SELECTStageMgr::OnInit_case58 +8064fd64 Pages::SELECTStageMgr::OnInit_case60 +8064fd70 Pages::SELECTStageMgr::OnInit_case59 +8064fd7c Pages::SELECTStageMgr::OnInit_case62 +8064fd84 Pages::SELECTStageMgr::OnInit_case5a +8064fe30 Pages::SELECTStageMgr::OnDispose +8064fe34 Pages::SELECTStageMgr::OnActivate +8064ff08 Pages::SELECTStageMgr::OnActivate_Pages::SELECTStageMgr::OnActivate_switch +8064ff0c Pages::SELECTStageMgr::OnActivate_case58 +8064ff2c Pages::SELECTStageMgr::OnActivate_case59 +8064ff48 Pages::SELECTStageMgr::OnActivate_case5a +80650018 Pages::SELECTStageMgr::OnActivate_switch2 +8065001c Pages::SELECTStageMgr::OnActivate_2case58 +806501d8 Pages::SELECTStageMgr::OnActivate_2case59 +806501f8 Pages::SELECTStageMgr::OnActivate_2case5a +80650214 Pages::SELECTStageMgr::OnDeactivate +80650218 Pages::SELECTStageMgr::BeforeControlUpdate +80650274 Pages::SELECTStageMgr::AfterControlUpdate +806507a0 Pages::SELECTStageMgr::OnResume +806509ac Pages::SELECTStageMgr::OnResume_Pages::SELECTStageMgr::OnResume_switch +806509b0 Pages::SELECTStageMgr::OnResume_case58 +806509b8 Pages::SELECTStageMgr::OnResume_case59 +806509bc Pages::SELECTStageMgr::OnResume_case5a +80650af4 Pages::SELECTStageMgr::IsPublicBattle +80650b24 Pages::SELECTStageMgr::IsPublicBattle_switch +80650b28 Pages::SELECTStageMgr::IsPublicBattle_case58 +80650b30 Pages::SELECTStageMgr::IsPublicBattle_case59 +80650b38 Pages::SELECTStageMgr::IsPublicBattle_case5a +80650b40 Pages::SELECTStageMgr::GetEngineClass +80650b90 Pages::SELECTStageMgr::GetBattleType +80650be0 Pages::SELECTStageMgr::GetInstructionBmgId +80650c10 Pages::SELECTStageMgr::GetInstructionBmgId_switch +80650c14 Pages::SELECTStageMgr::GetInstructionBmgId_case58 +80650c84 Pages::SELECTStageMgr::GetInstructionBmgId_case59 +80650ce4 Pages::SELECTStageMgr::GetInstructionBmgId_case60 +80650d54 Pages::SELECTStageMgr::GetInstructionBmgId_case61 +80650dc4 Pages::SELECTStageMgr::GetInstructionBmgId_case62 +80650dcc Pages::SELECTStageMgr::GetInstructionBmgId_case63 +80650dd4 Pages::SELECTStageMgr::GetInstructionBmgId_case5a +80650ddc Pages::SELECTStageMgr::GetPlayerId +80650e24 Pages::SELECTStageMgr::PrepareRace +80650ee4 Pages::SELECTStageMgr::PrepareRace_switch +80650ee8 Pages::SELECTStageMgr::PrepareRace_case58 +80650f08 Pages::SELECTStageMgr::PrepareRace_case60 +80650f28 Pages::SELECTStageMgr::PrepareRace_case61 +80650f48 Pages::SELECTStageMgr::PrepareRace_case59 +80650f68 Pages::SELECTStageMgr::PrepareRace_case62 +80650f84 Pages::SELECTStageMgr::PrepareRace_case5a +80650fb4 Pages::SELECTStageMgr::PrepareRace_case5a_switch +80650fb8 Pages::SELECTStageMgr::PrepareRace_2case58 +8065107c Pages::SELECTStageMgr::PrepareRace_2case59 +806510f4 Pages::SELECTStageMgr::PrepareRace_2case62 +80651120 Pages::SELECTStageMgr::PrepareRace_2case63 +8065114c Pages::SELECTStageMgr::PrepareRace_2case5a +806515cc Pages::SELECTStageMgr::UpdateOnlineParams +806517b0 Pages::SELECTStageMgr::OnDisconnect +80651854 Pages::SELECTStageMgr::SetModeTypes +80651898 Pages::SELECTStageMgr::SetModeTypes_switch +8065189c Pages::SELECTStageMgr::SetModeTypes_case58 +806518b0 Pages::SELECTStageMgr::SetModeTypes_case59 +806518c4 Pages::SELECTStageMgr::SetModeTypes_case60 +80651900 Pages::SELECTStageMgr::SetModeTypes_case62 +8065190c Pages::SELECTStageMgr::SetModeTypes_case63 +80651914 Pages::SELECTStageMgr::SetModeTypes_loop +8065193c 0x8065193c +80651984 0x80651984 +80651a84 0x80651a84 +80651a90 0x80651a90 +80651ad0 0x80651ad0 +80651b1c 0x80651b1c +80651ba8 Pages::SELECTStageMgr::IsHomeMenuWorking +80651bb0 Pages::SELECTStageMgr::GetRuntimeTypeInfo +80651bbc Pages::SELECTStageMgr::TriggerPtmf +80651bec CountDownTimerPage_cpp_sinit_ +80651c00 WipeControl::GetClassName +80651c0c WipeControl::__ct +80651c48 WipeControl::__dt +80651ca0 WipeControl::Load +80651d54 WipeControl::InitSelf +80651d8c WipeControl::OnUpdate +80651e80 Pages::Wipe::__ct +80651eec Pages::Wipe::__dt +80651f78 Pages::Wipe::OnInit +806522c4 Pages::Wipe::GetRuntimeTypeInfo +806522d0 WipeControl::GetRuntimeTypeInfo +806522dc WipePagecpp_sinit_ +80652300 Pages::YesNo::__ct +806523a4 Pages::YesNo::__dt +80652430 Pages::YesNo::OnInit +806524c4 Pages::YesNo::OnActivate +80652564 Pages::YesNo::AfterControlUpdate +806525a0 Pages::YesNo::Reset +806525fc Pages::YesNo::SetMessageBoxMsg +80652604 Pages::YesNo::PrepareButton +80652678 Pages::YesNo::OnButtonClick +80652700 Pages::YesNo::SelectInitialButton +80652774 Pages::YesNoPopUp::__ct +8065284c Pages::YesNoPopUp::__dt +806528fc Pages::YesNoPopUp::OnInit +80652b48 Pages::YesNoPopUp::BeforeEntranceAnimations +80652b8c Pages::YesNoPopUp::AfterExitAnimations +80652bd0 Pages::YesNoPopUp::Container::GetClassName +80652be0 Pages::YesNoPopUp::Container::__ct +80652c1c Pages::YesNoPopUp::Container::__dt +80652c74 Pages::YesNoPopUp::Container::Load +80652ce8 Pages::YesNoPopUp::Reset +80652d44 Pages::YesNoPopUp::Container::SetPositionAnim +80652d7c Pages::YesNoFull::__ct +80652e4c Pages::YesNoFull::__dt +80652ef0 Pages::YesNoFull::OnInit +806530d0 Pages::YesNoFull::Reset +8065313c Pages::YesNoFull::SetTitleMsg +80653144 Pages::YesNoFull::BeforeEntranceAnimations +80653160 Pages::YesNoFull::AfterExitAnimations +8065317c Pages::YesNoFull::GetRuntimeTypeInfo +80653188 Pages::YesNoPopUp::GetRuntimeTypeInfo +80653194 Pages::YesNoPopUp::Container::GetRuntimeTypeInfo +806531a0 Pages::YesNo::GetRuntimeTypeInfo +806531ac Pages::YesNo::TriggerPtmf +806531dc YesNoPagecpp_sinit_ +80653208 RKNet::PacketMgr::CreateInstance +80653284 RKNet::PacketMgr::DestroyInstance +806532d8 RKNet::PacketMgr::Reset +80653728 RKNet::PacketMgr::Update +80653960 RKNet::PacketMgr::ExportRacedata +80653abc RKNet::PacketMgr::GetRacedata +80653c08 RKNet::PacketMgr::ExportRH2 +80653cb8 RKNet::PacketMgr::GetRH2 +80653d14 RKNet::PacketMgr::ClearSentRacedata +80653d78 RKNet::PacketMgr::ClearSentRH2 +80653ddc RKNet::PacketMgr::ProcessPlayerDC +80653df4 RKNet::PacketMgr::__ct +80653e4c 0x80653e4c +80653e8c 0x80653e8c +80653ecc RKNet::PacketMgr::__dt +80653f24 RKNet::PacketMgr::UpdateAsSpectator +80654038 RKNet::PacketMgr::EveryoneIsConnected +80654150 RKNet::PacketMgr::UpdateAsRacer +806544a8 RKNet::PacketMgr::GetPlayerRH1Timer +80654568 RKNet::PacketMgr::GetPlayerLagFrames +806545dc RKNet::PacketMgr::GetMaxRH1Timer +8065471c RKNet::PacketMgr::SetITEMItem +80654734 RKNet::PacketMgr::IsITEMItemNew +80654740 RKNet::PacketMgr::GetITEMStoredItem +8065474c RKNet::PacketMgr::AddEVENTEntry +80654758 RKNet::PacketMgr::HasFreeEVENTEntries +80654810 RKNet::PacketMgr::GetEVENTFreeDataSpace +80654820 RKNet::PacketMgr::IsPlayerConnected +806548a8 RKNet::PacketMgr::IsPlayerLocal +80654918 RKNet::PacketMgr::GetHudSlotId +80654984 RKNet::PacketMgr::UpdateRaceinfoConnectionStatus +80654a34 0x80654a34 +80654b00 RKNet::PacketMgr::ProcessLagFrames +80654d08 RKNet::PacketMgr::CreateRH1AndExportRH1andUSERandRacedata +80655164 RKNet::PacketMgr::CreateAndExportRH1andUSER +806554a0 RKNet::PacketMgr::ProcessReceived +8065574c RKNet::PacketMgr::UpdateAidsBitfield +80655838 0x80655838 +8065584c 0x8065584c +80655858 0x80655858 +806559ec 0x806559ec +806559f0 0x806559f0 +80655b14 0x80655b14 +80655b1c 0x80655b1c +80655b24 RKNet::Controller::CreateInstance +80655bac RKNet::Controller::DestroyInstance +80655c10 RKNet::Controller::Init +806561a8 RKNet::Controller::ScheduleShutdown +806561b4 RKNet::Controller::StartSearchWW +80656270 RKNet::Controller::StartSearchRegion +8065632c RKNet::Controller::StartSearchBTWW +806563e8 RKNet::Controller::StartSearchBTRegional +806564a4 RKNet::Controller::JoinFriend +806565d4 0x806565d4 +80656704 RKNet::Controller::JoinRoom +806567d8 RKNet::Controller::StartRoom +80656898 RKNet::Controller::ResetRH1andROOM +806568f0 0x806568f0 +80656908 0x80656908 +80656920 0x80656920 +806569b4 RKNet::Controller::GetErrorParams +80656a30 RKNet::Controller::ResetErrorParams +80656a8c RKNet::Controller::GetSubTickDiff +80656aec RKNet::Controller::UpdateServersAsyncAlt +80656b44 RKNet::Controller::UpdateServersAsync +80656b6c RKNet::Controller::UpdateFriendsStatus +80656ca0 RKNet::Controller::SetFriendsAddedBack +80656d6c RKNet::Controller::SetFriendDeleted +80656d9c RKNet::Controller::CanExchangeFriendData +80656e44 RKNet::Controller::IsIdle +80656e70 0x80656e70 +80656f00 RKNet::Controller::IsConnectedToAnyone +80656f40 RKNet::Controller::ClearSplitSendRACEPointers +80656ff8 RKNet::Controller::SetConnectionStateToFailed +80657004 RKNet::Controller::__ct +806573dc RKNet::FriendManager::__dt +8065741c RKNet::Controller::__dt +80657500 RKNet::Controller::MainNetworkLoop +80657504 RKNet::Controller::MainLoop +80657a70 RKNet::Controller::SendPacketsAndUpdateUSER +80657ab0 RKNet::Controller::CombineSplitSendPackets +80657e30 RKNet::Controller::TrySendNextRACEPacket +80657fe4 RKNet::Controller::SendAidNextRACEPacket +806580c4 RKNet::Controller::GetSplitSendPacketsTotalSize +80658140 RKNet::Controller::SetConnectionState +80658148 RKNet::Controller::GetConnectionState +806581cc RKNet::Controller::HandleError +8065832c RKNet::Controller::Alloc +806583b4 RKNet::Controller::Free +80658418 RKNet::Controller::SOAlloc +80658498 RKNet::Controller::SOFree +80658500 RKNet::Controller::DWCAlloc +8065858c RKNet::Controller::DWCFree +806585f4 RKNet::Controller::UserRecvCallback +80658614 RKNet::Controller::LoginCallback +80658718 RKNet::Controller::MatchedSCCallback +80658768 RKNet::Controller::ConnectionAttemptCallback +80658824 RKNet::Controller::ConnectionClosedCallback +806588d8 RKNet::Controller::UpdateServerCallback +806588fc RKNet::Controller::UpdateServerCallbackAlt +80658918 RKNet::Controller::FriendStatusChangeCallback +8065891c RKNet::Controller::BuddyFriendCallback +80658928 RKNet::Controller::SetDWCRecvBuffers +80658990 RKNet::Controller::ProcessPlayerDisconnect +80658b9c RKNet::Controller::ConnectToWFC +80658d3c RKNet::Controller::InitSubs +80658de0 RKNet::Controller::UpdateSubsAndVR +80658f80 RKNet::Controller::ResetFriendArray +80659044 RKNet::Controller::StartMatching +80659170 RKNet::Controller::ConnectToAnybodyAsync +806595b8 RKNet::Controller::SetupGameServer +8065961c 0x8065961c +80659680 RKNet::Controller::ConnectToGameServerFromGroupId +806598c4 0x806598c4 +80659a3c RKNet::Controller::CalcRACEPacketCRC32 +80659a84 RKNet::Controller::ProcessRACEPacket +80659bc0 RKNet::Controller::UpdateAidsBelongingToPlayerIds +80659d20 RKNet::Controller::ResetAidsBelongingToPlayerIds +80659d58 RKNet::Controller::GetLocalPlayerId +80659fa4 RKNet::Controller::GetFriendsSearchType +8065a0b4 RKNet::Controller::UpdateStatusDatas +8065a24c RKNet::PacketHolder::__ct +8065a2ac RKNet::PacketHolder::__dt +8065a30c RKNet::PacketHolder::Clear +8065a34c RKNet::PacketHolder::Copy +8065a38c RKNet::PacketHolder::Append +8065a3dc RKNet::SplitRACEPointers::__ct +8065a474 RKNet::SplitRACEPointers::__dt +8065a4f8 RKNet::RatingRangeMaker::Init +8065a5e0 RKNet::RatingRangeMaker::GetLowerRange +8065a64c RKNet::RatingRangeMaker::GetUpperRange +8065a6b4 0x8065a6b4 +8065a768 RKNet::RatingRangeMaker::GetRatingGroup +8065a8d4 RKNet::GPReverseBuddiesListCallback +8065a974 RKNet::SetGPError +8065aa88 RKNet::ROOMHandler::CreateInstance +8065ab4c RKNet::ROOMHandler::DestroyInstance +8065ab8c RKNet::ROOMHandler::Init +8065ac94 RKNet::ROOMHandler::Reset +8065ad18 RKNet::ROOMHandler::Update +8065ae70 RKNet::ROOMHandler::SetAllToSendPackets +8065af64 RKNet::ROOMHandler::GetRecvPacket +8065af74 RKNet::ROOMHandler::SetToSendPacket +8065af84 RKNet::ROOMHandler::__ct +8065b008 RKNet::ROOMHandler::__dt +8065b048 RKNet::ROOMHandler::ExportROOMandUSER +8065b194 RKNet::ROOMHandler::ImportNewPackets +8065b260 RKNet::ROOMHandler::ResetAllPackets +8065b2e0 RKNet::ROOMHandler::InitPacket +8065b2ec RKNet::ROOMHandler::ClearToSendPacket +8065b300 RKNet::ROOMHandler::ClearRecvPacket +8065b314 RKNet::ROOMHandler::SetToSendPacket2 +8065b324 RKNet::ROOMHandler::SetRecvPacket +8065b334 RKNet::ROOMHandler::GetToSendPacketPtr +8065b344 RKNet::ROOMHandler::GetRecvPacketPtr +8065b354 0x8065b354 +8065b3b8 0x8065b3b8 +8065b40c 0x8065b40c +8065b450 0x8065b450 +8065b490 0x8065b490 +8065b4e8 RKNet::EVENTHandler::CreateInstance +8065b600 RKNet::EVENTHandler::DestroyInstance +8065b640 RKNet::EVENTHandler::__ct +8065b72c 0x8065b72c +8065b730 RKNet::EVENTHandler::Update +8065b780 RKNet::EVENTHandler::AddEntry +8065b998 RKNet::EVENTHandler::Reset +8065ba8c RKNet::EVENTHandler::__dt +8065bacc RKNet::EVENTHandler::FreeExpiredSendEntries +8065bb94 RKNet::EVENTHandler::ExportSendEntries +8065bce4 RKNet::EVENTHandler::ImportReceivedEntries +8065be40 RKNet::EVENTHandler::ProcessReceivedEntries +8065c04c RKNet::EVENTHandler::ResetEntry +8065c078 RKNet::EVENTHandler::GetEventType +8065c0b0 RKNet::EVENTHandler::SetEntryItemAndActionFromType +8065c0c4 RKNet::EVENTHandler::ProcessEntry +8065c0ec RKNet::ITEMHandler::CreateInstance +8065c368 RKNet::ITEMHandler::DestroyInstance +8065c3a8 RKNet::ITEMHandler::__ct +8065c5e4 0x8065c5e4 +8065c5e8 RKNet::ITEMHandler::Update +8065c6c0 RKNet::ITEMHandler::SetItem +8065c74c RKNet::ITEMHandler::RemoveItem +8065c7e0 RKNet::ITEMHandler::OnRouletteEnd +8065c8cc 0x8065c8cc +8065ca64 0x8065ca64 +8065cc2c 0x8065cc2c +8065cdf4 RKNet::ITEMHandler::GetModeRelatedInt +8065cec0 0x8065cec0 +8065cfc4 RKNet::ITEMHandler::SetItemAndMode +8065d020 0x8065d020 +8065d0e0 RKNet::ITEMHandler::SetDraggedItemAndMode +8065d13c 0x8065d13c +8065d21c RKNet::ITEMHandler::GetStoredItem +8065d2b0 RKNet::ITEMHandler::GetDraggedItem +8065d364 RKNet::ITEMHandler::GetStoredItemMode +8065d3f8 RKNet::ITEMHandler::GetDraggedItemMode +8065d4ac RKNet::ITEMHandler::GetTimer +8065d55c RKNet::ITEMHandler::IsPlayerConnected +8065d568 RKNet::ITEMHandler::IsItemNew +8065d608 RKNet::ITEMHandler::IsDraggingItem +8065d6e4 RKNet::ITEMHandler::HasStartedDraggingItem +8065d9f0 0x8065d9f0 +8065dc2c RKNet::ITEMHandler::__dt +8065dc6c RKNet::ITEMHandler::ExportSendPackets +8065dd3c RKNet::ITEMHandler::ImportNewPackets +8065e0a0 RKNet::ITEMHandler::UpdateItemStatusAndSums +8065e644 RKNet::ITEMHandler::AcknowledgeAid +8065e8a8 RKNet::ITEMHandler::UpdateLocalPlayersItems +8065e938 RKNet::ITEMHandler::UpdateStoredItem +8065ed74 RKNet::ITEMHandler::UpdateDraggedItem +8065f068 RKNet::ITEMHandler::UpdateLocalPlayers +8065f6dc RKNet::ItemHandler::GetStoredItemCount +8065f9cc 0x8065f9cc +8065fa0c 0x8065fa0c +8065fa4c 0x8065fa4c +8065fb6c 0x8065fb6c +8065fe4c 0x8065fe4c +8065fe8c RKNet::SELECTHandler::CreateInstance +8065ff60 RKNet::SELECTHandler::DestroyInstance +8065ffa4 RKNet::SELECTHandler::InitPackets +806602f8 0x806602f8 +806602fc RKNet::SELECTHandler::Update +80660434 RKNet::SELECTHandler::GetWinningVoter +80660450 RKNet::SELECTHandler::GetWinningCourse +80660470 RKNet::SELECTHandler::GetOwnSelectId +8066048c RKNet::SELECTHandler::GetEngineClass +806604a8 RKNet::SELECTHandler::GetBattleType +806604cc RKNet::SELECTHandler::GetOwnPlayerIdToAid +806604d4 RKNet::SELECTHandler::GetCharacterId +80660524 RKNet::SELECTHandler::GetKartId +80660574 RKNet::SELECTHandler::GetCourseVote +806605c4 RKNet::SELECTHandler::GetPlayerData +8066060c RKNet::SELECTHandler::GetStarRank +80660654 RKNet::SELECTHandler::GetTeam +8066068c RKNet::SELECTHandler::HasUnprocessedPackets +80660710 RKNet::SELECTHandler::IsPrepared +80660750 RKNet::SELECTHandler::SetPlayerData +8066076c RKNet::SELECTHandler::__ct +806607f4 RKNet::SELECTHandler::__dt +806608b4 RKNet::SELECTHandler::ResetSendPacket +80660a00 RKNet::SELECTHandler::ResetReceivedPackets +80660af4 RKNet::SELECTHandler::ResetPacket +80660bd0 RKNet::SELECTHandler::ResetPlayerData +80660c3c RKNet::SELECTHandler::HandleReceivedPacketTimes +80660d2c RKNet::SELECTHandler::IsBattle +80660d40 RKNet::SELECTHandler::IsTrackDecided +80660d5c RKNet::SELECTHandler::EveryoneHasAccurateRaceSettings +80660dcc RKNet::SELECTHandler::EveryoneHasVoted +80660e3c RKNet::SELECTHandler::EveryoneHasAccurateAidPidMap +80660eb0 RKNet::SELECTHandler::ClearRH1ReceivedBuffers +80660f1c RKNet::SELECTHandler::PrepareAndExportSELECTandUSER +80661078 RKNet::SELECTHandler::ImportNewPackets +80661288 RKNet::SELECTHandler::PrepareAndExportPacket +806614b0 RKNet::SELECTHandler::ProcessNewPacketVoting +80661934 RKNet::SELECTHandler::DecideBattleTypeAndTeams +80661a14 RKNet::SELECTHandler::GenerateOwnSELECTId +80661a5c RKNet::SELECTHandler::DecideEngineClass +80661ce8 RKNet::SELECTHandler::DecideTrack +80662034 RKNet::SELECTHandler::AllocatePlayerIdsToAids +806621dc RKNet::SELECTHandler::HasMatchingPlayerIdToAid +80662290 RKNet::SELECTHandler::DecidePublicTeams +80662588 RKNet::SELECTHandler::DecidePrivateTeams +80662778 RKNet::USERHandler::CreateInstance +806627f8 RKNet::USERHandler::DestroyInstance +8066284c RKNet::USERHandler::Prepare +80662944 RKNet::USERHandler::Reset +806629c0 RKNet::USERHandler::Update +80662abc RKNet::USERHandler::CopySendToPacketHolder +80662b00 RKNet::USERHandler::ProcessPlayerDisconnect +80662bec 0x80662bec +80662c58 0x80662c58 +80662c68 0x80662c68 +80662c88 0x80662c88 +80662d20 0x80662d20 +80662d80 RKNet::USERHandler::SetSendVR +80662dc4 RKNet::USERHandler::SetSendBR +80662e08 RKNet::USERHandler::__ct +80662e64 RKNet::USERHandler::__dt +80662ebc RKNet::USERHandler::ImportNewPackets +80662fb8 0x80662fb8 +8066300c RKNet::USERHandler::CreateSendPacket +80663194 RKNet::FriendMgr::CreateInstance +806632bc RKNet::FriendMgr::DestroyInstance +806632fc RKNet::FriendMgr::ReadDataFromRKSYS +80663444 RKNet::FriendMgr::Update +806635a8 RKNet::FriendMgr::CreateAccUserData +806635b0 RKNet::FriendMgr::CheckAccUserData +806635e0 RKNet::FriendMgr::IsAccUserDataDirt +80663610 RKNet::FriendMgr::ClearAccUserDataDirtyFlag +80663618 RKNet::FriendMgr::CreateFriendCode +80663644 RKNet::FriendMgr::SetCodesForUnusedIdxs +8066374c RKNet::FriendMgr::IsAvailable +8066375c RKNet::FriendMgr::IsBuddyFriend +80663794 RKNet::FriendMgr::IsValidFriend +806637c4 RKNet::FriendMgr::CheckFriendCode +806637fc RKNet::FriendMgr::GetFriendIdx +80663874 RKNet::FriendMgr::GetFriendCodeLicenseIdx +80663918 RKNet::FriendMgr::GetLicenseFriendCode +80663940 RKNet::FriendMgr::GetFirstFreeFriendIdx +806639a0 RKNet::FriendMgr::__ct +80663aa4 RKNet::RH1Handler::CreateInstance +80663c38 RKNet::RH1Handler::DestroyInstance +80663c7c RKNet::RH1Handler::Activate +80663c88 RKNet::RH1Handler::Reset +80663dd4 RKNet::RH1Handler::Update +8066410c RKNet::RH1Handler::Reset +806641b0 RKNet::RH1Handler::HasValidCourse +806643c0 RKNet::RH1Handler::GetKart +806643d8 RKNet::RH1Handler::GetCharacter +806643f0 0x806643f0 +8066454c RKNet::RH1Handler::GetStarRank +80664560 RKNet::RH1Handler::GetValidCourse +806646c8 0x806646c8 +806647d0 RKNet::RH1Handler::GetPlayerCount +80664944 RKNet::RH1Handler::GetSELECTId +80664a3c RKNet::RH1Handler::GetEngineClass +80664b34 RKNet::RH1Handler::GetAidsBelongingToPlayer +80664c2c 0x80664c2c +80664d9c RKNet::RH1Handler::__dt +80664f00 0x80664f00 +80664ff8 0x80664ff8 +8066506c 0x8066506c +806651b4 RKNet::RH1Handler::ImportNewPackets +80665334 0x80665334 +806653c8 RKNet::RH1Handler::ExportDefaultPacket +80665480 RKNet::RH1Handler::ExportDefaultPacket2 +80665538 0x80665538 +80665660 0x80665660 +80665700 0x80665700 +8066588c 0x8066588c +8066591c 0x8066591c +8066596c 0x8066596c +80665998 0x80665998 +806659b0 0x806659b0 +80665a2c 0x80665a2c +80665a4c 0x80665a4c +80665a90 0x80665a90 +80665af8 0x80665af8 +80665b6c 0x80665b6c +80665bf4 Page::EndExitState +80665bf8 0x80665bf8 +80665bfc 0x80665bfc +80665c24 0x80665c24 +80665c3c 0x80665c3c +80665fec 0x80665fec +806660e0 0x806660e0 +8066617c 0x8066617c +80666288 0x80666288 +80666408 0x80666408 +806665cc 0x806665cc +806665e4 0x806665e4 +8066664c 0x8066664c +80666694 0x80666694 +806666a0 0x806666a0 +806666ac 0x806666ac +806666b8 0x806666b8 +806666e8 0x806666e8 +80666718 0x80666718 +80666748 0x80666748 +80666768 0x80666768 +80666880 0x80666880 +80666920 0x80666920 +80666aac 0x80666aac +80666b00 0x80666b00 +80666b18 0x80666b18 +80666b94 0x80666b94 +80666bb4 0x80666bb4 +80666bc4 0x80666bc4 +80666c20 0x80666c20 +80666c94 0x80666c94 +80666de8 0x80666de8 +80666dec 0x80666dec +80666ee4 0x80666ee4 +80666f7c 0x80666f7c +80666fec 0x80666fec +8066704c 0x8066704c +806670e4 0x806670e4 +8066711c 0x8066711c +80667150 0x80667150 +80667234 0x80667234 +80667294 0x80667294 +806672a4 0x806672a4 +806674b4 0x806674b4 +80667598 0x80667598 +80667870 0x80667870 +80667a48 0x80667a48 +80667a4c 0x80667a4c +80667a50 0x80667a50 +80667bb4 0x80667bb4 +80667cac 0x80667cac +80667db8 0x80667db8 +80667ec0 0x80667ec0 +80667efc 0x80667efc +80667f0c 0x80667f0c +80667f24 0x80667f24 +80667f70 0x80667f70 +80667fe4 0x80667fe4 +80668078 0x80668078 +80668080 0x80668080 +806681b4 Page::EndExitState +806681b8 0x806681b8 +806681bc 0x806681bc +806684f4 0x806681bc_switch +806684f8 0x806681bc_switch_806684f4::CaseD_1 +80668500 0x806681bc_switch_806684f4::CaseD_2 +80668508 0x806681bc_switch_806684f4::CaseD_3 +80668510 0x806681bc_switch_806684f4::CaseD_4 +80668518 0x806681bc_switch_806684f4::CaseD_5 +80668520 0x806681bc_switch_806684f4::CaseD_6 +80668528 0x806681bc_switch_806684f4::CaseD_8 +80668530 0x806681bc_switch_806684f4::CaseD_7 +80668558 0x806681bc_caseD_0 +806685d8 0x806685d8 +806686c0 0x806686c0 +806686c8 0x806686c8 +806686d4 0x806686d4 +806686e0 0x806686e0 +806686ec 0x806686ec +806686f8 0x806686f8 +80668728 0x80668728 +80668758 0x80668758 +80668788 0x80668788 +806687b8 0x806687b8 +806687e8 0x806687e8 +80668814 ServerMgr::__dt +8066895c 0x8066895c +806689b8 ServerMgr::DestroyInstance +80668ab4 ServerMgr::CreateInstance +80668b18 ServerMgr::__ct +80668cb0 0x80668cb0 +80668cd4 0x80668cd4 +80668d14 0x80668d14 +80668d7c 0x80668d7c +80668e80 0x80668e80 +80668f4c 0x80668f4c +80668f60 0x80668f60 +80668fa4 ServerMgr::GetServerState +80668fac ServerMgr::GetState +80668fb4 0x80668fb4 +80668fd0 0x80668fd0 +80668fdc ServerMgr::GetError +80668fe4 ServerMgr::CountFriendsToInviteViaMsgBoard +806690f8 0x806690f8 +80669100 0x80669100 +80669108 0x80669108 +80669264 ServerMgr::Update +806692a4 ServerMgr::Update_switch +806692a8 ServerMgr::Update_caseD_1 +806692cc ServerMgr::Update_caseD_8 +806692d4 ServerMgr::Update_caseD_16 +806692dc ServerMgr::Update_caseD_9 +806693f4 ServerMgr::Update_caseD_3 +80669720 ServerMgr::Update_caseD_0 +80669734 0x80669734 +80669858 0x80669858 +80669c74 0x80669c74 +80669cb4 0x80669cb4 +80669cf4 BundleSortProc +80669d04 0x80669d04 +80669d0c 0x80669d0c +80669de8 0x80669d0c_switch +80669dec 0x80669d0c_caseD_0 +80669e5c 0x80669d0c_caseD_1 +8066a050 0x80669d0c_caseD_2 +8066a118 0x80669d0c_caseD_3 +8066a1d8 0x80669d0c_caseD_4 +8066a3a4 0x80669d0c_caseD_5 +8066a440 0x80669d0c_caseD_6 +8066a7bc 0x80669d0c_caseD_7 +8066aa74 0x80669d0c_caseD_8 +8066aad8 0x80669d0c_caseD_9 +8066ad28 0x80669d0c_caseD_a +8066ad9c 0x8066ad9c +8066afac 0x8066afac +8066b2d0 0x8066b2d0 +8066b418 0x8066b418 +8066b4bc ServerMgr::GetRKG +8066b5f0 0x8066b5f0 +8066b84c 0x8066b84c +8066b8b8 0x8066b8b8 +8066bcc0 0x8066bcc0 +8066bdb4 0x8066bdb4 +8066bddc 0x8066bddc +8066c06c 0x8066c06c +8066c17c ServerMgr::CheckNWC24Available +8066c188 0x8066c188 +8066c2e8 0x8066c2e8 +8066c390 0x8066c390 +8066c420 0x8066c420 +8066c4bc 0x8066c4bc +8066c528 ServerMgr::HasCourseNewFriendRecord +8066c618 0x8066c618 +8066c66c 0x8066c66c +8066c7f8 0x8066c7f8 +8066c874 0x8066c874 +8066c8d8 ServerMgr::GetCompetitionInfo +8066ca04 0x8066ca04 +8066ca54 0x8066ca54 +8066ca64 0x8066ca64 +8066cb30 ServerMgr::GetSpecialGhostInfo +8066cb4c 0x8066cb4c +8066cbcc 0x8066cbcc +8066d35c 0x8066d35c +8066d668 0x8066d668 +8066d734 0x8066d734 +8066d7f4 0x8066d7f4 +8066dad0 0x8066dad0 +8066dbbc 0x8066dbbc +8066dd48 0x8066dd48 +8066dd94 0x8066dd94 +8066e048 0x8066e048 +8066e160 0x8066e160 +8066e35c 0x8066e35c +8066e4b0 0x8066e4b0 +8066e654 0x8066e654 +8066e844 0x8066e844 +8066e910 0x8066e910 +8066ea80 0x8066ea80 +8066eb5c 0x8066eb5c +8066ec2c 0x8066ec2c +8066eeb4 0x8066eeb4 +8066ef64 0x8066ef64 +8066f0cc 0x8066f0cc +8066f16c 0x8066f16c +8066f2c4 0x8066f2c4 +8066f4ac 0x8066f4ac +8066f590 0x8066f590 +8066f8bc 0x8066f8bc +8066fa48 0x8066fa48 +8066fbc8 0x8066fbc8 +8066fe0c 0x8066fe0c +8066ffa4 0x8066ffa4 +80670248 0x80670248 +806706ac 0x806706ac +80670c08 0x80670c08 +80671000 0x80671000 +8067120c 0x8067120c +80671314 0x80671314 +80671390 0x80671390 +80671440 0x80671440 +80671588 0x80671588 +8067160c 0x8067160c +80671770 0x80671770 +80671848 0x80671848 +80671b4c 0x80671b4c +80671c7c ServerMgr::PublishMsgBoardAsync +80671df8 ServerMgr::PublishMsgBoard +806720d0 ServerMgr::InitCHJObj +80672324 0x80672324 +8067233c 0x8067233c +80672350 ServerMgr::RequestMsgBoardPublish +806724a0 0x806724a0 +80672620 0x80672620 +8067268c 0x8067268c +80672840 0x80672840 +80672a00 0x80672a00 +80672b3c 0x80672b3c +80672cc8 0x80672cc8 +80672e14 0x80672e14 +806730e4 0x806730e4 +80673170 0x80673170 +80673218 0x80673218 +806732c8 0x806732c8 +80673568 0x80673568 +806738d0 0x806738d0 +80673a40 0x80673a40 +80673a9c 0x80673a9c +80673bec 0x80673bec +80673d3c 0x80673d3c +80673e8c 0x80673e8c +80674000 0x80674000 +80674154 0x80674154 +806742d4 0x806742d4 +80674518 0x80674518 +80674658 0x80674658 +80674794 0x80674794 +8067482c 0x8067482c +8067495c 0x8067495c +80674a38 0x80674a38 +80674ae4 0x80674ae4 +80674b60 0x80674b60 +80674bcc 0x80674bcc +80674c38 0x80674c38 +80674c88 0x80674c88 +80674dd0 0x80674dd0 +80674f08 0x80674f08 +8067507c 0x8067507c +806750f0 0x806750f0 +8067512c ServerMgr::CreateRKGFromRKG +80675230 ServerMgr::CheckGhostDataValidity +80675304 0x80675304 +80675384 0x80675384 +8067538c 0x8067538c +80675444 0x80675444 +80675454 0x80675454 +8067552c 0x8067552c +80675624 0x80675624 +8067568c 0x8067568c +806756a4 0x806756a4 +806756c8 0x806756c8 +806756d0 0x806756d0 +80675724 0x80675724 +80675848 0x80675848 +80675ad4 0x80675ad4 +80675b80 0x80675b80 +80675e48 0x80675e48 +80675f6c 0x80675f6c +806760d0 0x806760d0 +806760dc 0x806760dc +80676130 0x80676130 +80676184 0x80676184 +806761c8 0x806761c8 +806761d0 0x806761d0 +80676220 0x80676220 +80676228 0x80676228 +80676248 0x80676248 +80676288 0x80676288 +806762d4 0x806762d4 +806763e4 0x806763e4 +8067643c 0x8067643c +80676548 0x80676548 +806766b4 0x806766b4 +80676860 0x80676860 +8067686c 0x8067686c +80676904 0x80676904 +80676970 0x80676970 +8067697c 0x8067697c +80676990 0x80676990 +806771cc 0x806771cc +806771f8 0x806771f8 +8067721c 0x8067721c +80677228 0x80677228 +80677230 0x80677230 +80677238 0x80677238 +80677240 0x80677240 +80677380 0x80677380 +806773c8 0x806773c8 +80677444 0x80677444 +806775e4 0x806775e4 +806776b0 0x806776b0 +80677758 0x80677758 +80677880 0x80677880 +8067790c 0x8067790c +80677990 0x80677990 +80677a8c 0x80677a8c +80677b64 0x80677b64 +80677bf4 0x80677bf4 +80677ce8 0x80677ce8 +80677d80 0x80677d80 +80677e64 0x80677e64 +80677f64 0x80677f64 +80677fb0 0x80677fb0 +8067805c 0x8067805c +806781dc 0x806781dc +80678298 0x80678298 +80678318 0x80678318 +80678384 0x80678384 +806783e8 0x806783e8 +806783fc 0x806783fc +806784a4 0x806784a4 +8067858c 0x8067858c +80678628 0x80678628 +80678640 0x80678640 +8067866c 0x8067866c +80678678 0x80678678 +80678680 0x80678680 +80678740 0x80678740 +8067884c 0x8067884c +80678850 0x80678850 +806789b4 0x806789b4 +80678a28 0x80678a28 +80678aac 0x80678aac +80678b78 0x80678b78 +80678e94 0x80678e94 +80678f4c 0x80678f4c +80678fcc 0x80678fcc +80679024 0x80679024 +8067906c 0x8067906c +80679078 0x80679078 +80679080 0x80679080 +80679088 0x80679088 +8067909c 0x8067909c +8067927c 0x8067927c +8067937c thunk_0x801de348 +80679380 0x80679380 +80679390 0x80679390 +80679568 0x80679568 +80679720 0x80679720 +80679aa8 0x80679aa8 +80679c6c 0x80679c6c +80679ef4 0x80679ef4 +8067a0b8 0x8067a0b8 +8067a29c 0x8067a29c +8067a480 0x8067a480 +8067a58c 0x8067a58c +8067a7b0 0x8067a7b0 +8067a8bc 0x8067a8bc +8067ac00 0x8067ac00 +8067ac44 0x8067ac44 +8067acac 0x8067acac +8067ad34 0x8067ad34 +8067ad3c 0x8067ad3c +8067ae40 Page::EndExitState +8067ae44 0x8067ae44 +8067ae48 0x8067ae48 +8067b12c 0x8067ae48_switch +8067b130 0x8067ae48_switch_8067b12c::CaseD_1 +8067b138 0x8067ae48_switch_8067b12c::CaseD_2 +8067b140 0x8067ae48_switch_8067b12c::CaseD_3 +8067b148 0x8067ae48_switch_8067b12c::CaseD_4 +8067b150 0x8067ae48_switch_8067b12c::CaseD_5 +8067b158 0x8067ae48_switch_8067b12c::CaseD_6 +8067b160 0x8067ae48_switch_8067b12c::CaseD_8 +8067b168 0x8067ae48_switch_8067b12c::CaseD_7 +8067b190 0x8067ae48_caseD_0 +8067b210 0x8067b210 +8067b2f8 Effects::Mgr::GetRuntimeTypeInfo +8067b304 EffectsMgrcpp_sinit_ +8067b318 Effects::MGWhiteFog::__ct +8067b388 Effects::MGWhiteFog::__dt +8067b3c8 Effects::MGWhiteFog::Update +8067b448 Effects::MGWhiteFog::UpdateEffect +8067b4b4 Effects::MGWhiteFog::Kill +8067b4c8 Effects::Mgr::CreateInstance +8067b580 Effects::Mgr::Init +8067b8f0 Effects::Mgr::Reset +8067baf4 Effects::Sub9d8::__dt +8067bb34 Effects::Mgr::LoadRaceEffects +8067c4f4 Effects::Mgr::CreatePlayerEffects +8067c580 Effects::Mgr::CreateEffects::Items +8067c5c0 Effects::Mgr::LoadObjectEffect +8067c9e4 0x8067c9e4 +8067ca4c Effects::Mgr::InitRace +8067cb88 Effects::Mgr::Update +8067cc24 0x8067cc24 +8067cca0 Effects::Mgr::CalcPlayerEffects +8067ce44 Effects::Mgr::UpdatePlayersVisibility +8067cf74 Effects::Mgr::Draw +8067d6f4 0x8067d6f4 +8067d90c 0x8067d90c +8067da54 Object::GetClipInfo +8067da5c Effects::Mgr::UpdatePlayerFromWheelCollision +8067da78 Effects::Mgr::UpdatePlayerFromKartCollision +8067daa8 Effects::Mgr::GetMenuEffectByIdx +8067dab0 Effects::Mgr::CalcEffect1 +8067dad0 Effects::Mgr::DrawEffect1 +8067daf4 Effects::Mgr::DrawSelfItem +8067dca4 0x8067dca4 +8067dcc0 Effects::Mgr::IsEveryoneLookingForwards +8067dd1c Effects::Mgr::IsMTOrPlayer0xF4 +8067dd58 0x8067dd58 +8067de14 Effects::Sub9d8::Update +8067e0bc 0x8067e0bc +8067e230 0x8067e230 +8067e270 dGeomEnable +8067e280 0x8067e280 +8067e384 ObjectCollidable::__dt +8067e3c4 Object::__dt +8067e404 Objects::FlamePoleEff::OnStart +8067e410 Objects::FlamePoleEff::Update +8067e5c8 0x8067e5c8 +8067e670 0x8067e670 +8067e6ec 0x8067e6ec +8067e6f4 Objects::FlamePole::__ct +8067eaec Vec3::EulerFromMatrix +8067eafc ObjectExternKCL::__dt +8067eb3c ObjectKCL::__dt +8067eb7c 0x8067eb7c +8067ebe0 Objects::FlamePole::__dt +8067ec94 Objects::FlamePole::OnStart +8067ef70 Objects::FlamePole::OnUpdate +8067f240 Objects::FlamePole::UpdateModelMatrix +8067f2dc 0x8067f2dc +8067f2f0 0x8067f2f0 +8067f2f4 Objects::FlamePole::CalcOtherEntityDistance +8067f484 0x8067f484 +8067f538 0x8067f538 +8067f544 0x8067f544 +8067f5ec 0x8067f5ec +8067f604 0x8067f604 +8067f650 0x8067f650 +8067f6ac 0x8067f6ac +8067f6b0 0x8067f6b0 +8067f6b4 0x8067f6b4 +8067f6b8 0x8067f6b8 +8067f7c8 0x8067f7c8 +8067f864 0x8067f864 +8067f868 0x8067f868 +8067f8e4 0x8067f8e4 +8067fa8c 0x8067fa8c +8067fbb8 Objects::FlamePole::GetTransformationMatrix +8067fc50 0x8067fc50 +8067fdf4 0x8067fdf4 +8067fe1c Objects::FlamePole::UpdateOtherEntityPosWithRot +8067fe88 Objects::FlamePole::IsCollidingNoTriangleCheckImpl +80680218 0x80680218 +806805a8 ObjectEffect::__dt +80680600 Object::DoCalcShadowMtx +80680608 Object::vf_0xa8 +80680610 Object::IsLodDisabled +80680618 Object::GetEntity +80680620 Object::vf_0x84 +80680628 Object::GetShadowListIdx +80680630 Object::UpdateShadowScale +8068065c Object::UpdateModelScale +806806cc Object::GetShadowResName +806806d4 Object::GetLowResourceName +806806dc Object::GetSubFileName +80680730 Object::GetBRRESName +80680784 Object::GetName +806807d8 do_nothing_7d8 +806807dc do_nothing_7dc +806807e0 ObjectKCL::GetScnObjDrawOptionsIdx +806807e8 ObjectExternKCL::UpdateKCL +806807f0 ObjectExternKCL::IsCollidingNoTerrainInfo +806808ec ObjectExternKCL::HasCurrentCollision +806808f4 ObjectExternKCL::vf_0x13c +806808fc ObjectExternKCL::IsCollidingAddEntryNoTerrainInfo +806809f8 ObjectExternKCL::HasCollision +80680a00 ObjectExternKCL::IsDriveable +80680a08 ObjectExternKCL::vf_0xfc +80680b04 ObjectExternKCL::IsCollidingAddEntry +80680b14 ObjectExternKCL::IsCollidingNoTerrainInfo(no radius) +80680bcc ObjectExternKCL::IsCollidingAddEntryNoTerrainInfo(no radius) +80680c84 ObjectExternKCL::vf_0xec +80680d3c ObjectExternKCL::IsCollidingAddEntry(no radius) +80680df4 ObjectExternKCL::IsCollidingNoTerrainInfoNoTriangleCheck +80680ef0 ObjectExternKCL::IsCollidingAddEntryNoTerrainInfoNoTriangleCheck +80680fec ObjectExternKCL::IsColliding +806810e8 ObjectExternKCL::IsCollidingAddEntryNoTriangleCheck +806810f8 ObjectExternKCL::vf_0xc0 +806811b0 ObjectExternKCL::vf_0xc4 +80681268 ObjectExternKCL::vf_0xc8 +80681320 ObjectExternKCL::vf_0xcc +806813d8 ObjectExternKCL::UpdateOtherEntityPos +8068142c ObjectExternKCL::SetObjInfoCalcBit +8068143c ObjectExternKCL::GetPeriod +80681448 ObjectExternKCL::GetPosition +80681450 ObjectExternKCL::UpdateDiffPosVector +8068147c ObjectExternKCL::vf_0xb4 +80681490 ObjectExternKCL::UpdateCollision +806814a4 Objects::FlamePole::vf__0x11c +806814b0 Objects::FlamePole::vf_0x108 +806814bc Objects::FlamePole::AreItemsAllowed +806814c4 Objects::FlamePole::GetCollisionDiameter +80681508 Objects::FlamePole::LoadAnimations +80681588 0x80681588 +80681590 Objects::FlamePole::GetPropertiesBitfield +80681598 Object::GetPosition +806815a0 ObjectCollidable::GetCollisionDiameter +806816b8 ObjectCollidable::GetSolidityCenter +806816d8 ObjectCollidable::InitEntity +80681734 ObjectCollidable::IsSolid +8068173c ObjectCollidable::GetCollisionTranslation +80681748 ObjectCollidable::CheckCollision +8068179c ObjectCollidable::OnCollision +806817a4 ObjectCollidable::OnItemCollision +806817ac ObjectCollidable::GetScnObjDrawOptionsIdx +806817b4 Objects::FlamePoleEff::LoadAnimations +8068180c Objects::FlamePoleEff::vf_0xec +80681814 Objects::FlamePoleEff::GetBRRESName +80681820 Objects::FlamePoleEff::GetPropertiesBitfield +80681828 Objects::FlamePoleEff::__dt +80681868 Objects::FlamePole::__dt(thunk) +80681870 0x80681870 +80681950 0x80681950 +80681e24 0x80681e24 +80681e98 0x80681e98 +80682160 0x80682160 +8068273c 0x8068273c +806828e8 ObjectKCL::vf_0x111c +806828f0 ObjectKCL::vf_0x1118 +806828f4 ObjectKCL::UpdateOtherEntityPos +806828f8 ObjectKCL::SetObjInfoCalcBit +806828fc ObjectKCL::CalcOtherEntityDistance +80682900 ObjectKCL::vf_0x108 +8068290c ObjectKCL::AreItemsAllowed +80682914 ObjectKCL::UpdateKCL +80682918 ObjectKCL::GetCollisionDiameter +80682924 ObjectKCL::InitCollision +80682928 0x80682928 +8068292c 0x8068292c +80682930 0x80682930 +80682934 0x80682934 +80682938 0x80682938 +80682940 0x80682940 +80682948 0x80682948 +80682950 0x80682950 +80682958 0x80682958 +8068295c 0x8068295c +80682960 0x80682960 +80682964 0x80682964 +80682968 0x80682968 +80682970 0x80682970 +80682978 0x80682978 +80682980 0x80682980 +80682988 0x80682988 +8068298c 0x8068298c +80682990 0x80682990 +80682994 0x80682994 +80682998 0x80682998 +806829a0 0x806829a0 +806829e0 0x806829e0 +806837bc 0x806837bc +80684598 0x80684598 +80685374 0x80685374 +80686150 0x80686150 +80686570 0x80686570 +80686988 0x80686988 +80686c8c 0x80686c8c +80686f84 0x80686f84 +8068717c 0x8068717c +806871e0 0x806871e0 +806872a0 0x806872a0 +806873bc 0x806873bc +80687768 0x80687768 +8068776c 0x8068776c +80687770 0x80687770 +80687774 0x80687774 +806877c0 0x806877c0 +80687800 0x80687800 +806879a4 0x806879a4 +806879cc 0x806879cc +80687a2c 0x80687a2c +80687cc0 0x80687cc0 +80687d58 ObjectExternKCL::UpdateOtherEntityPosWithRot +80687d5c ObjectExternKCL::vf_0x108 +80687d68 ObjectExternKCL::AreItemsAllowed +80687d70 ObjectExternKCL::GetCollisionDiameter +80687db0 ObjectExternKCL::GetYScale +80687db8 0x80687db8 +80687dc0 0x80687dc0 +80687e0c 0x80687e0c +80687e14 0x80687e14 +80687e1c 0x80687e1c +80687e24 obj_gnd_sphere::__ct +80688004 0x80688004 +806880a8 0x806880a8 +8068829c 0x8068829c +806882a0 0x806882a0 +806884ac 0x806884ac +806884ec 0x806884ec +806887b4 0x806887b4 +80688810 0x80688810 +80688bf8 0x80688bf8 +80688c9c 0x80688c9c +80688dac 0x80688dac +80688e48 0x80688e48 +80688e60 0x80688e60 +80688eb0 0x80688eb0 +80688f00 0x80688f00 +80688f50 0x80688f50 +80688fa0 0x80688fa0 +80688ff0 0x80688ff0 +80689040 0x80689040 +80689090 0x80689090 +806890e0 0x806890e0 +80689130 0x80689130 +80689300 0x80689300 +80689a78 0x80689a78 +80689b84 0x80689b84 +80689b88 0x80689b88 +80689b8c 0x80689b8c +80689b90 0x80689b90 +80689b94 0x80689b94 +80689b9c 0x80689b9c +80689ba4 0x80689ba4 +80689bac 0x80689bac +80689bb4 0x80689bb4 +80689bb8 0x80689bb8 +80689bbc 0x80689bbc +80689bc0 0x80689bc0 +80689bc4 0x80689bc4 +80689bcc 0x80689bcc +80689bd4 0x80689bd4 +80689bdc 0x80689bdc +80689be4 0x80689be4 +80689be8 0x80689be8 +80689bec 0x80689bec +80689bf0 0x80689bf0 +80689bf4 0x80689bf4 +80689bfc 0x80689bfc +80689d74 0x80689d74 +80689d8c 0x80689d8c +80689ddc 0x80689ddc +80689e2c 0x80689e2c +80689e7c 0x80689e7c +80689ecc 0x80689ecc +80689f1c 0x80689f1c +80689f6c 0x80689f6c +80689fbc 0x80689fbc +8068a00c 0x8068a00c +8068a05c 0x8068a05c +8068a23c 0x8068a23c +8068a3b4 0x8068a3b4 +8068a3cc 0x8068a3cc +8068a41c 0x8068a41c +8068a46c 0x8068a46c +8068a4bc 0x8068a4bc +8068a50c 0x8068a50c +8068a55c 0x8068a55c +8068a5ac 0x8068a5ac +8068a5fc 0x8068a5fc +8068a64c 0x8068a64c +8068a69c 0x8068a69c +8068a888 0x8068a888 +8068a9fc 0x8068a9fc +8068aa14 0x8068aa14 +8068aa64 0x8068aa64 +8068aab4 0x8068aab4 +8068ab04 0x8068ab04 +8068ab54 0x8068ab54 +8068aba4 0x8068aba4 +8068abf4 0x8068abf4 +8068ac44 0x8068ac44 +8068ac94 0x8068ac94 +8068ace4 0x8068ace4 +8068afe8 0x8068afe8 +8068b15c 0x8068b15c +8068b174 0x8068b174 +8068b1c4 0x8068b1c4 +8068b214 0x8068b214 +8068b264 0x8068b264 +8068b2b4 0x8068b2b4 +8068b304 0x8068b304 +8068b354 0x8068b354 +8068b3a4 0x8068b3a4 +8068b3f4 0x8068b3f4 +8068b444 0x8068b444 +8068b750 0x8068b750 +8068b7c4 0x8068b7c4 +8068b97c 0x8068b97c +8068b984 obj_gnd_wave1::__ct +8068bb68 0x8068bb68 +8068bc0c 0x8068bc0c +8068bdc8 0x8068bdc8 +8068bdcc obj_gnd_wave2::__ct +8068bfe4 0x8068bfe4 +8068c088 0x8068c088 +8068c230 0x8068c230 +8068c234 0x8068c234 +8068c464 0x8068c464 +8068c508 0x8068c508 +8068c9d4 0x8068c9d4 +8068c9d8 0x8068c9d8 +8068cbb8 0x8068cbb8 +8068cc5c 0x8068cc5c +8068ced8 0x8068ced8 +8068cedc Effects::Items::__ct +8068d234 Effects::Items::__dt +8068d274 Effects::Items::Init +8068d280 0x8068d280 +8068d284 Effects::Items::CreateKouraTailGEffect +8068d2d8 Effects::Items::CreateKouraTailREffect +8068d32c Effects::Items::CreateKouraTailBEffect +8068d380 0x8068d380 +8068d3a8 0x8068d3a8 +8068d3d0 0x8068d3d0 +8068d3f0 Effects::Items::FadeKouraTailGEffect +8068d41c Effects::Items::FadeKouraTailREffect +8068d448 Effects::Items::FadeKouraTailBEffect +8068d474 Effects::Items::KillKouraTailGEffect +8068d4a0 Effects::Items::KillKouraTailREffect +8068d4cc Effects::Items::KillKouraTailBEffect +8068d4f8 Effects::Items::CreateKouraBrkGEffect +8068d570 Effects::Items::CreateKouraBrkREffect +8068d5e8 Effects::Items::CreateItemVanEffect +8068d608 Effects::Items::CreateBombHeiEffect +8068d748 Effects::Items::CreateHaneKouraEffect +8068d784 Effects::Items::CreateBombHeiSparkEffect +8068d81c Effects::Items:KillBombHeiEffect +8068d848 Effects::Items::CreateBananaEffect +8068d954 0x8068d954 +8068d9e8 Effects::Items::CreateKaminariGumoPreEffect +8068dad8 Effects::Items::KillKaminariGumoPreEffect +8068daf4 Effects::Items::CreateKaminariGumoVanEffect +8068db8c Effects::Items::UpdateKaminariGumoVanEffect +8068dd08 Effects::Items::CreateKouraTailEffect +8068df94 0x8068df94 +8068df98 Effects::Player::__ct +8068e830 Effects::Player::GetRacedata +8068e83c Effects::Player::GetCourseId +8068e844 Effects::Player::Color::__ct +8068e848 Effects::Player::Color::__dt +8068e888 Effects::Player::__dt +8068e9a8 Effects::Player::Init +8068ea58 Effects::Player::Reset +8068f044 Effects::Player::LoadEffects +8069077c Effects::Player::LoadEffects_switch +80690780 Effects::Player::LoadEffects_case0 +8069084c Effects::Player::LoadEffects_case1 +80690918 Effects::Player::LoadEffects_case2 +806909e4 Effects::Player::LoadEffects_case3 +80690ab0 Effects::Player::LoadEffects_case4 +80690b7c Effects::Player::LoadEffects_case5 +80690c48 Effects::Player::LoadEffects_case6 +80690d14 Effects::Player::LoadEffects_case7 +80690de0 Effects::Player::LoadEffects_case8 +80690eac Effects::Player::LoadEffects_case9 +80690f78 Effects::Player::LoadEffects_casea +80691044 Effects::Player::LoadEffects_caseb +80693d58 Effects::Player::Toggle +80693d84 Effects::Player::Update +80693e94 Effects::Player::UpdateValues +806946bc 0x806946bc +80694d0c Effects::Player::UpdateItemEffect +80695604 0x80695604 +8069566c 0x8069566c +806958b8 0x806958b8 +806958f4 Effects::Player::KillEffects +80695ba8 Effects::Player::UpdateNonItemEffects +80696cf0 0x80696cf0 +80696f1c 0x80696f1c +80696fbc 0x80696fbc +8069703c 0x8069703c +80697138 0x80697138 +80697234 0x80697234 +806972f4 0x806972f4 +806973b4 0x806973b4 +806974c4 0x806974c4 +806975d4 Effects::Player::CreateAndUpdateEffectsByIdx +80697788 Effects::Player::FollowFadeEffectsByIdx +8069797c Effects::Player::CreateEffectsByIdx +806979f0 Effects::Player::UpdateEffectsByIdx +80697b2c Effects::Player::KillEffectsByIdx +80697be0 Effects::Player::UpdateCollisionEffects +80697c30 0x80697c30 +80697d40 Effects::Player::UpdateCollisionEffects_caseD_0 +80697d50 Effects::Player::UpdateCollisionEffects_caseD_1 +80697d60 Effects::Player::UpdateCollisionEffects_caseD_2 +80697d70 Effects::Player::UpdateCollisionEffects_caseD_3 +80697d80 Effects::Player::UpdateCollisionEffects_caseD_4 +80697d90 Effects::Player::UpdateCollisionEffects_caseD_5 +80697da0 Effects::Player::UpdateCollisionEffects_caseD_6 +80697db0 Effects::Player::UpdateCollisionEffects_caseD_7 +80697ed8 Effects::Player::UpdateCollisionEffects_case2D_0 +80697ee8 Effects::Player::UpdateCollisionEffects_case2D_1 +80697ef8 Effects::Player::UpdateCollisionEffects_case2D_2 +80697f08 Effects::Player::UpdateCollisionEffects_case2D_3 +80697f18 Effects::Player::UpdateCollisionEffects_case2D_4 +80697f28 Effects::Player::UpdateCollisionEffects_case2D_5 +80697f38 Effects::Player::UpdateCollisionEffects_case2D_6 +80697f48 Effects::Player::UpdateCollisionEffects_case2D_7 +80697ffc Effects::Player::UpdateVehicleEffects +80698028 Effects::Player::UpdateKartEffects +80698f70 Effects::Player::UpdateBikeEffects +80699cb0 Effects::Player::UpdateJumpSmokeEffects +8069a738 0x8069a738 +8069a914 0x8069a914 +8069abf4 0x8069abf4 +8069ad40 0x8069ad40 +8069b114 0x8069b114 +8069b394 Effects::Player::UpdateStartUpEffects +8069b748 Effects::Player::UpdateSlipStreamEffects +8069baf8 Effects::Player::UpdateGasSmokeEffects +8069c040 0x8069c040 +8069c1bc 0x8069c1bc +8069c2ec 0x8069c2ec +8069c648 0x8069c648 +8069c790 0x8069c790 +8069c7f0 Effects::Player::UpdateWaterEffects +8069cf18 0x8069cf18 +8069d0bc 0x8069d0bc +8069d120 Effects::Player::UpdateInBillEffects +8069db3c 0x8069db3c +8069dba4 Effects::Player::UpdateThunderEffects +8069e304 0x8069e304 +8069e514 0x8069e514 +8069e8ac Effects::Player::Create228Effect +8069eb5c Effects::Player::Create25CEffect +8069f3e4 0x8069f3e4 +8069f41c 0x8069f41c +8069f690 0x8069f690 +8069f918 0x8069f918 +8069fcf0 Effects::Player::UpdateInkEffect +806a02e4 0x806a02e4 +806a0370 0x806a0370 +806a0428 0x806a0428 +806a0b4c 0x806a0b4c +806a0fdc Effects::Player::UpdateFromWheelCollision +806a115c Effects::Player::UpdateFromWheelCollision_switch +806a1160 Effects::Player::UpdateFromWheelCollision_caseD_0 +806a117c Effects::Player::UpdateFromWheelCollision_caseD_1 +806a1198 Effects::Player::UpdateFromWheelCollision_caseD_4 +806a11b4 Effects::Player::UpdateFromWheelCollision_caseD_5 +806a11d0 Effects::Player::UpdateFromWheelCollision_caseD_3 +806a11e8 Effects::Player::UpdateFromWheelCollision_caseD_2 +806a11fc Effects::Player::UpdateFromWheelCollision_caseD_6 +806a1210 Effects::Player::UpdateFromWheelCollision_caseD_7 +806a1660 Effects::Player::UpdateFromKartCollision +806a1808 0x806a1808 +806a1820 0x806a1820 +806a1938 0x806a1938 +806a19d8 Effects::Player::CreateAndUpdateEffectsByIdxVelocity +806a1b68 Effects::Player::FollowFadeEffectsByIdxVelocity +806a1cc8 0x806a1cc8 +806a1d1c 0x806a1d1c +806a1d70 0x806a1d70 +806a1e4c 0x806a1e4c +806a1eb0 0x806a1eb0 +806a1fd8 0x806a1fd8 +806a20b4 0x806a20b4 +806a2118 0x806a2118 +806a21f4 0x806a21f4 +806a2258 0x806a2258 +806a2348 0x806a2348 +806a2424 0x806a2424 +806a2460 0x806a2460 +806a2488 0x806a2488 +806a2578 0x806a2578 +806a25ec 0x806a25ec +806a26a0 0x806a26a0 +806a2780 0x806a2780 +806a27e4 0x806a27e4 +806a290c 0x806a290c +806a29ec 0x806a29ec +806a2a50 0x806a2a50 +806a2b78 0x806a2b78 +806a2c14 0x806a2c14 +806a2c30 0x806a2c30 +806a2ce4 0x806a2ce4 +806a2e04 0x806a2e04 +806a2e8c 0x806a2e8c +806a2f20 0x806a2f20 +806a3138 0x806a3138 +806a3184 0x806a3184 +806a326c 0x806a326c +806a33b4 0x806a33b4 +806a34a4 0x806a34a4 +806a35cc 0x806a35cc +806a3618 0x806a3618 +806a36c8 0x806a36c8 +806a37b8 0x806a37b8 +806a381c 0x806a381c +806a3898 0x806a3898 +806a38e4 0x806a38e4 +806a3948 0x806a3948 +806a3ac4 0x806a3ac4 +806a3d48 0x806a3d48 +806a3f90 0x806a3f90 +806a4020 0x806a4020 +806a40b0 0x806a40b0 +806a414c 0x806a414c +806a4248 0x806a4248 +806a4318 0x806a4318 +806a43b4 0x806a43b4 +806a44b0 0x806a44b0 +806a4580 0x806a4580 +806a461c 0x806a461c +806a4718 0x806a4718 +806a47e8 0x806a47e8 +806a4870 0x806a4870 +806a4920 0x806a4920 +806a49c8 0x806a49c8 +806a4a78 0x806a4a78 +806a4b20 0x806a4b20 +806a4b94 0x806a4b94 +806a4c38 0x806a4c38 +806a4ce8 0x806a4ce8 +806a4dac 0x806a4dac +806a4e5c 0x806a4e5c +806a4f20 0x806a4f20 +806a4fd0 0x806a4fd0 +806a5094 0x806a5094 +806a5144 0x806a5144 +806a5208 0x806a5208 +806a52d8 0x806a52d8 +806a537c 0x806a537c +806a54d0 0x806a54d0 +806a5660 0x806a5660 +806a597c 0x806a597c +806a5a00 0x806a5a00 +806a5d8c 0x806a5d8c +806a5db0 0x806a5db0 +806a5e34 0x806a5e34 +806a61b0 0x806a61b0 +806a6234 0x806a6234 +806a62a4 Vec3::DivideByScalar +806a62d8 0x806a62d8 +806a6334 0x806a6334 +806a6428 0x806a6428 +806a65ec Effects::Player::CreateRKHitEffect +806a66a4 Effects::Player::OnBushCollision +806a67e4 0x806a67e4 +806a67f8 Effects::Player::CreateSpinEffect +806a6a2c 0x806a6a2c +806a6ad8 0x806a6ad8 +806a6c34 0x806a6c34 +806a6ce0 0x806a6ce0 +806a6e3c 0x806a6e3c +806a6ee8 0x806a6ee8 +806a7044 0x806a7044 +806a70f0 0x806a70f0 +806a724c 0x806a724c +806a72c0 0x806a72c0 +806a7364 0x806a7364 +806a73d8 0x806a73d8 +806a747c 0x806a747c +806a7528 0x806a7528 +806a75cc 0x806a75cc +806a7640 0x806a7640 +806a76e4 0x806a76e4 +806a77b4 0x806a77b4 +806a7858 0x806a7858 +806a79f4 0x806a79f4 +806a7ae4 0x806a7ae4 +806a7b68 0x806a7b68 +806a7c08 0x806a7c08 +806a7c8c 0x806a7c8c +806a7d30 0x806a7d30 +806a7dd4 0x806a7dd4 +806a7ea4 0x806a7ea4 +806a7f08 0x806a7f08 +806a7f80 0x806a7f80 +806a7fa0 0x806a7fa0 +806a804c 0x806a804c +806a80f0 0x806a80f0 +806a8154 0x806a8154 +806a81c4 0x806a81c4 +806a8268 Effects::Player::CreateKartBrakeBackwardsEffects +806a837c Effects::Player::KillKartBrakeBackwardsEffects +806a849c 0x806a849c +806a84e4 0x806a84e4 +806a8588 Effects::Player::CreateBikeBrakeBackwardsEffects +806a867c Effects::Player::KillBikeBrakeBackwardsEffects +806a877c 0x806a877c +806a881c 0x806a881c +806a88f8 0x806a88f8 +806a8998 0x806a8998 +806a8a18 0x806a8a18 +806a8ad4 Effects::Player::CreateStarEffect +806a8cb0 Effects::Player::UpdateStarEffect +806a8f2c Effects::Player::CreateRKKiller +806a9070 Effects::Player::UpdateRKKiller +806a92a4 0x806a92a4 +806a9328 0x806a9328 +806a93a8 0x806a93a8 +806a942c 0x806a942c +806a9550 0x806a9550 +806a95f0 0x806a95f0 +806a96cc 0x806a96cc +806a98cc 0x806a98cc +806a9b48 0x806a9b48 +806a9d48 0x806a9d48 +806a9fc4 0x806a9fc4 +806aa1c8 0x806aa1c8 +806aa274 0x806aa274 +806aa33c 0x806aa33c +806aa4cc 0x806aa4cc +806aa508 0x806aa508 +806aa530 Effects::Player::UpdatePowEffect +806aa784 Effects::Player::UpdatePow1Effect +806aa988 0x806aa988 +806aaa2c 0x806aaa2c +806aaba8 0x806aaba8 +806aabf8 0x806aabf8 +806aace4 0x806aace4 +806aae00 0x806aae00 +806aaf04 Effects::Player::UpdateNiseBoxBiriEffect +806ab0d4 0x806ab0d4 +806ab1d4 0x806ab1d4 +806ab298 0x806ab298 +806ab33c 0x806ab33c +806ab468 0x806ab468 +806ab50c 0x806ab50c +806ab60c 0x806ab60c +806ab6b0 0x806ab6b0 +806ab8a0 Effects::Player::TriggerPochaEffect +806abaf4 0x806abaf4 +806abb1c 0x806abb1c +806abb5c Effects::Player::CreatePochaEffect +806abd3c 0x806abd3c +806abf10 0x806abf10 +806abfa8 0x806abfa8 +806ac0c0 0x806ac0c0 +806ac140 0x806ac140 +806ac210 0x806ac210 +806ac2b8 0x806ac2b8 +806ac400 0x806ac400 +806ac618 0x806ac618 +806ac69c 0x806ac69c +806ac73c 0x806ac73c +806ac810 0x806ac810 +806ac924 0x806ac924 +806acb04 0x806acb04 +806acb1c 0x806acb1c +806acfbc 0x806acfbc +806ad088 0x806ad088 +806ad518 0x806ad518 +806ad5e4 0x806ad5e4 +806ada74 0x806ada74 +806adb40 0x806adb40 +806adccc 0x806adccc +806adce0 0x806adce0 +806ade58 0x806ade58 +806ade6c 0x806ade6c +806adee0 0x806adee0 +806adef4 Effects::Player::CreateSandEffect +806ae36c Effects::Player::Color::__ct(args) +806ae380 Effects::Player::Color::operator= +806ae3a4 0x806ae3a4 +806ae58c Effects::Player::CreateGrassEffect +806ae8ec 0x806ae8ec +806aeac8 Effects::Player::CreateSnowDirtEffect +806aedbc 0x806aedbc +806aef88 0x806aef88 +806af038 Effects::Player::CreateSnowRoadEffect +806af4d0 0x806af4d0 +806af768 Effects::Player::CreateWaterEffect +806af9ec 0x806af9ec +806afaf0 0x806afaf0 +806afc04 Effects::Player::CreateMudEffect +806aff10 0x806aff10 +806b0098 Effects::Player::CreateFlowerEffect +806b0334 0x806b0334 +806b04bc Effects::Player::CreateStoneEffect +806b0754 0x806b0754 +806b08d8 0x806b08d8 +806b0bb4 0x806b0bb4 +806b0e04 0x806b0e04 +806b11b0 0x806b11b0 +806b171c Effects::Player::SetOffroadColors +806b25ac 0x806b25ac +806b271c 0x806b271c +806b2764 0x806b2764 +806b2880 0x806b2880 +806b28c8 0x806b28c8 +806b294c 0x806b294c +806b2964 0x806b2964 +806b2c60 0x806b2c60 +806b2dac 0x806b2dac +806b32fc 0x806b32fc +806b33e0 0x806b33e0 +806b345c 0x806b345c +806b3698 Effects::Player::SetGessoEffectsVisibility +806b37cc Effects::Player::UpdateGessoEffectsVisibility +806b38a8 0x806b38a8 +806b3900 Vector3_rotate +806b39e0 0x806b39e0 +806b3ac4 0x806b3ac4 +806b3b40 0x806b3b40 +806b3ca4 Vec3::CalcYAxisRotMtx +806b3e1c 0x806b3e1c +806b3f70 0x806b3f70 +806b4108 0x806b4108 +806b41e0 0x806b41e0 +806b42c8 0x806b42c8 +806b4474 0x806b4474 +806b45b8 0x806b45b8 +806b46a4 0x806b46a4 +806b46f8 0x806b46f8 +806b475c 0x806b475c +806b5380 0x806b5380 +806b5530 0x806b5530 +806b55c4 0x806b55c4 +806b59a8 -bx^2+ax +806b5a0c CreateParabola +806b5a30 0x806b5a30 +806b5a34 0x806b5a34 +806b5a38 0x806b5a38 +806b5a3c 0x806b5a3c +806b5a40 0x806b5a40 +806b5ac8 Effects::Mgr::CreateOneTimeEffect(pos) +806b5b28 Effects::Mgr::CreateOneTimeEffect(mtx) +806b5b88 Effects::Mgr::CreateOneTimeEffect(pos, scale) +806b5c04 Effects::Mgr::CreateOneTimeEffect(mtx, scale) +806b5c80 0x806b5c80 +806b5c84 Objects::Basabasa::__ct +806b5e1c 0x806b5e1c +806b5e80 Objects::Basabasa::OnStart +806b6028 Objects::Basabasa::vf_0x10 +806b602c Objects::Basabasa::Update +806b6100 0x806b6100 +806b6288 0x806b6288 +806b652c 0x806b652c +806b6874 Objects::Basabasa::OnCollision +806b6a18 Objects::Basabasa::OnItemCollision +806b6eac Objects::Basabasa::UpdateShadow +806b70d0 Objects::BasabasaMgr::__ct +806b72f4 Objects::BasabasaMgr::__dt +806b7334 Objects::BasabasaMgr::OnStart +806b74c0 Objects::BasabasaMgr::vf_0x10 +806b74c4 Objects::BasabasaMgr::Update +806b7618 Objects::BasabasaMgr::LoadClipInfo +806b761c Objects::BasabasaMgr::LoadCollision +806b7620 Objects::BasabasaMgr::LoadRoute +806b7624 Objects::BasabasaMgr::LoadModels +806b7628 Objects::BasabasaMgr::GetPropertiesBitfield +806b7630 Objects::Basabasa::__dt +806b769c 0x806b769c +806b76a4 Objects::Basabasa::LoadAnimations +806b76e8 Objects::Basabasa::GetName +806b76f4 Objects::Basabasa::GetSubFileName +806b7700 Objects::Basabasa::GetPropertiesBitfield +806b7708 Objects::Basabasa::__dt(thunk) +806b7710 Objects::CarA::__ct +806b7868 0x806b7868 +806b78cc Objects::CarA::__dt +806b7944 Objects::CarA::LoadModels +806b7b44 Objects::CarA::LoadCollision +806b7bc4 Objects::CarA::UpdateCollision +806b7ce0 Objects::CarA::OnStart +806b7e60 Objects::CarA::OnCollision +806b802c Objects::CarA::OnItemCollision +806b82cc Objects::CarA::Update +806b84fc 0x806b84fc +806b8588 0x806b8588 +806b85ac 0x806b85ac +806b86f0 Objects::CarA::UpdateRoute +806b8838 0x806b8838 +806b8844 0x806b8844 +806b8914 0x806b8914 +806b89a8 0x806b89a8 +806b8ccc 0x806b8ccc +806b8d3c 0x806b8d3c +806b8e54 0x806b8e54 +806b8edc 0x806b8edc +806b8ee4 Objects::CarA::LoadAnimations +806b8f44 Objects::CarA::GetPropertiesBitfield +806b8f4c MiiCreationParams::__dt +806b8f8c Objects::CarA::__dt(thunk) +806b8f94 0x806b8f94 +806b9074 0x806b9074 +806b90b4 0x806b90b4 +806b90e4 0x806b90e4 +806b9164 0x806b9164 +806b926c 0x806b926c +806b9274 0x806b9274 +806b93cc 0x806b93cc +806b9428 0x806b9428 +806b94a0 0x806b94a0 +806b9514 0x806b9514 +806b9594 0x806b9594 +806b9674 0x806b9674 +806b96a0 0x806b96a0 +806b9b28 0x806b9b28 +806b9b8c 0x806b9b8c +806b9bf8 0x806b9bf8 +806b9e60 0x806b9e60 +806ba144 0x806ba144 +806ba378 0x806ba378 +806ba6d8 0x806ba6d8 +806ba7fc 0x806ba7fc +806baad4 0x806baad4 +806babec 0x806babec +806bb144 0x806bb144 +806bb394 0x806bb394 +806bb398 0x806bb398 +806bb39c 0x806bb39c +806bb5f0 0x806bb5f0 +806bb838 0x806bb838 +806bb83c 0x806bb83c +806bb840 0x806bb840 +806bba7c 0x806bba7c +806bbae8 0x806bbae8 +806bbb80 0x806bbb80 +806bbc9c 0x806bbc9c +806bbddc 0x806bbddc +806bbde4 0x806bbde4 +806bbe34 0x806bbe34 +806bbe3c 0x806bbe3c +806bbe40 0x806bbe40 +806bbe44 0x806bbe44 +806bbe4c 0x806bbe4c +806bbe58 0x806bbe58 +806bbe64 0x806bbe64 +806bbe6c 0x806bbe6c +806bbeac 0x806bbeac +806bbeb0 0x806bbeb0 +806bbeb8 0x806bbeb8 +806bbec0 Objects::CowBase::__ct +806bbf24 Objects::CowBase::__dt +806bbf64 0x806bbf64 +806bc124 0x806bc124 +806bc2ac Objects::CowBase::OnCollision +806bc3f0 Objects::CowBase::OnItemCollision +806bc5c0 0x806bc5c0 +806bc6d8 0x806bc6d8 +806bc87c Objects::CowBase::vf_0xf0 +806bc9d4 0x806bc9d4 +806bcb28 0x806bcb28 +806bcba4 0x806bcba4 +806bccc0 0x806bccc0 +806bcdc4 0x806bcdc4 +806bce88 0x806bce88 +806bcf4c 0x806bcf4c +806bcfd4 0x806bcfd4 +806bd080 Objects::CowLeader::__ct +806bd194 0x806bd194 +806bd1f8 Objects::CowLeader::__dt +806bd264 Objects::CowLeader::OnStart +806bd388 Objects::CowLeader::Update +806bd6b0 0x806bd6b0 +806bd738 0x806bd738 +806bd7d8 0x806bd7d8 +806bd84c 0x806bd84c +806bd88c 0x806bd88c +806bda1c 0x806bda1c +806bda70 0x806bda70 +806bdcd8 Objects::CowLeader::vf_0xf0 +806bdd44 0x806bdd44 +806bdd48 Objects::CowVassal::__ct +806bdf90 0x806bdf90 +806bdff4 Objects::CowVassal::__dt +806be060 Objects::CowVassal::OnStart +806be1a8 Objects::CowVassal::Update +806be4e8 0x806be4e8 +806be580 0x806be580 +806be62c 0x806be62c +806be794 0x806be794 +806be930 0x806be930 +806be9cc 0x806be9cc +806beb54 Objects::CowGroup::__ct +806befec Objects::CowGroup::__dt +806bf02c Objects::CowGroup::OnStart +806bf064 Objects::CowGroup::Update +806bf114 0x806bf114 +806bf208 0x806bf208 +806bf228 0x806bf228 +806bf344 Objects::CowGroup::LoadClipInfo +806bf348 Objects::CowGroup::LoadCollision +806bf34c Objects::CowGroup::LoadRoute +806bf350 Objects::CowGroup::LoadModels +806bf354 Objects::CowGroup::GetPropertiesBitfield +806bf35c Objects::CowBase::vf_0xec +806bf364 Objects::CowBase::LoadAnimations +806bf420 Objects::CowVassal::LoadRoute +806bf424 Objects::CowVassal::GetPropertiesBitfield +806bf42c Objects::CowLeader::GetPropertiesBitfield +806bf434 Object::GetGetPropertiesBitfield +806bf448 Object::Update +806bf4b8 Objects::CowLeader::__dt(thunk) +806bf4c0 Objects::CowVassal::__dt(thunk) +806bf4c8 0x806bf4c8 +806bf538 0x806bf538 +806bf578 0x806bf578 +806bf5e4 0x806bf5e4 +806bf604 0x806bf604 +806bf764 0x806bf764 +806bf768 0x806bf768 +806bf770 0x806bf770 +806bf7b0 0x806bf7b0 +806bf7b8 0x806bf7b8 +806bf7fc 0x806bf7fc +806bf880 0x806bf880 +806bf888 0x806bf888 +806bf918 0x806bf918 +806bf958 0x806bf958 +806bf9f0 0x806bf9f0 +806bf9f4 0x806bf9f4 +806bfad4 0x806bfad4 +806bfb14 Objects::Cannon::__ct +806bfc48 Objects::Cannon::LoadAnimations +806bfd90 Objects::Cannon::__dt +806bfddc Objects::Cannon::OnStart +806bfed0 Objects::Cannon::Update +806c040c Objects::Cannon::OnActivation +806c04f4 Objects::Cannon::vf_0xec +806c04fc 0x806c04fc +806c0504 Objects::Cannon::GetScnObjDrawOptionsIdx +806c050c Objects::EffectSpawner::__ct +806c0560 Objects::EffectSpawner::OnStart +806c0570 Objects::EffectSpawner::OnCollision +806c06f0 Objects::EffectSpawner::Update +806c07e4 Objects::EffectSpawner::__dt +806c0824 Objects::EffectSpawner::OnItemCollision +806c0850 Objects::EffectSpawner::OnWallCollision +806c0854 Objects::EffectSpawner::GetPropertiesBitfield +806c085c Objects::LeafEffect::__ct +806c099c Objects::LeafEffect::OnCollision +806c0b54 Objects::LeafEffect::Update +806c0cd8 Objects::LeafEffect::__dt +806c0d18 Objects::FireSnake_ball::__ct +806c0d88 Objects::FireSnake_ball::OnStart +806c0e00 Objects::FireSnake_ball::vf_0xec +806c0e20 Objects::FireSnake_ball::ToggleVisible +806c0e24 Objects::FireSnake_ball::Update +806c0e28 0x806c0e28 +806c0ec8 0x806c0ec8 +806c0f1c 0x806c0f1c +806c0f30 Objects::FireSnake::__ct +806c1284 Objects::MapObjProjectile::__dt +806c12c4 Objects::FireSnake_ball::SetScale +806c12e0 0x806c12e0 +806c1344 Objects::FireSnake::__dt +806c13b0 Objects::FireSnake::OnStart +806c14e4 Objects::FireSnake::Update +806c1818 Objects::FireSnake::OnCollision +806c18a8 Objects::FireSnake::OnItemCollision +806c1930 Objects::FireSnake::OnCycleEnd +806c19e4 Objects::FireSnake::OnCycleEnd2 +806c19e8 Objects::FireSnake::OnCycleStart +806c1a88 Objects::FireSnake::OnCycleStart2 +806c1dcc Objects::FireSnake::OnInitialGroundCollision +806c1e90 Objects::FireSnake::vf_0x114 +806c2000 Objects::FireSnake::vf_0x118 +806c2138 Objects::FireSnake::vf_0x11c +806c21c8 0x806c21c8 +806c2250 Objects::FireSnake::vf_0x120 +806c2254 Objects::FireSnake::vf_0x124 +806c23b8 Objects::FireSnake::vf_0x128 +806c23c4 Objects::FireSnake::vf_0x12c +806c23c8 Objects::FireSnake::vf_0xec +806c2530 0x806c2530 +806c2904 Objects::FireSnake_ball::GetEntity +806c290c Objects::FireSnake_ball::UpdateDrawInfo +806c2968 Objects::FireSnake_ball::vf_0x8c +806c296c Objects::FireSnake_ball::vf_0xf8 +806c29fc Objects::FireSnake::RequestShoot +806c2a08 Objects::FireSnake::vf_0x130 +806c2a10 Objects::FireSnake::LoadAnimations +806c2a5c Objects::FireSnake::GetPropertiesBitfield +806c2a64 Objects::FireSnake_eye::LoadCollision +806c2a68 Objects::FireSnake_eye::LoadRoute +806c2a6c Objects::FireSnake_eye::GetName +806c2a78 Objects::FireSnake_eye::GetSubFileName +806c2a84 Objects::FireSnake_eye::GetPropertiesBitfield +806c2a8c Objects::FireSnake_eye::__dt +806c2acc Objects::FireSnake_ball::__dt +806c2b0c Objects::FireSnake_ball::vf_0xf0 +806c2b14 Objects::FireSnake_ball::LoadAnimations +806c2b60 Objects::FireSnake_ball::GetPropertiesBitfield +806c2b68 Objects::FireSnake::__dt(thunk) +806c2b70 Objects::FireSnake_v::__ct +806c2cbc Objects::FireSnake_v::OnStart +806c2d54 Objects::FireSnake_v::Update +806c2da4 0x806c2da4 +806c30f0 Objects::FireSnake_v::OnCycleEnd +806c30f4 Objects::FireSnake_v::OnCycleEnd2 +806c30f8 Objects::FireSnake_v::OnCycleStart +806c31f0 Objects::FireSnake_v::OnCycleStart2 +806c33b4 Objects::FireSnake_v::OnInitialGroundCollision +806c33c4 Objects::FireSnake_v::vf_0x114 +806c3548 Objects::FireSnake_v::__dt +806c35a0 0x806c35a0 +806c35a8 0x806c35a8 +806c369c 0x806c369c +806c36dc 0x806c36dc +806c375c 0x806c375c +806c37f0 0x806c37f0 +806c3864 0x806c3864 +806c3868 0x806c3868 +806c393c 0x806c393c +806c3944 0x806c3944 +806c3988 0x806c3988 +806c39c8 0x806c39c8 +806c3a3c 0x806c3a3c +806c3a40 0x806c3a40 +806c3a44 0x806c3a44 +806c3a4c 0x806c3a4c +806c3a9c 0x806c3a9c +806c3aa4 obj_FlamePole_v::__ct +806c3e2c 0x806c3e2c +806c3e90 0x806c3e90 +806c3fcc obj_FlamePole_v_update +806c40e4 0x806c40e4 +806c4130 0x806c4130 +806c4178 0x806c4178 +806c41f0 obj_FlamePole_v_raiseUpdate +806c4280 obj_FlamePole_v_beforeShake +806c42a0 obj_FlamePole_v_calcShake +806c43bc obj_FlamePole_v_dropUpdate +806c43e8 0x806c43e8 +806c4478 0x806c4478 +806c44c8 0x806c44c8 +806c44e4 0x806c44e4 +806c47b4 0x806c47b4 +806c4820 0x806c4820 +806c4878 0x806c4878 +806c4880 0x806c4880 +806c488c 0x806c488c +806c4898 0x806c4898 +806c48a0 0x806c48a0 +806c48a8 0x806c48a8 +806c4c34 0x806c4c34 +806c4c38 0x806c4c38 +806c4e94 0x806c4e94 +806c4ed4 ManagedObjects::RegisterObject +806c4ef0 Objects::HwanwanItembox::__ct +806c4f74 Objects::Itembox::__dt +806c4fb4 0x806c4fb4 +806c4ff4 Objects::HwanwanItembox::OnStart +806c5070 Objects::HwanwanItembox::Update +806c5254 Objects::HwanwanItembox::UpdateShadow +806c5354 Objects::Hwanwan::__ct +806c56dc Objects::Hwanwan::__dt +806c571c 0x806c571c +806c5ac4 Objects::Hwanwan::Update +806c5bcc Objects::Hwanwan::vf_0xec +806c5de0 0x806c5de0 +806c6148 0x806c6148 +806c619c 0x806c619c +806c6214 0x806c6214 +806c63a8 RouteController::SetPercent +806c63b0 0x806c63b0 +806c66d8 0x806c66d8 +806c66f4 0x806c66f4 +806c677c 0x806c677c +806c692c 0x806c692c +806c69a8 0x806c69a8 +806c69ac Objects::Hwanwan::LoadClipInfo +806c69b0 Objects::Hwanwan::LoadCollision +806c69b4 Objects::Hwanwan::LoadModels +806c69b8 Objects::Hwanwan::GetPropertiesBitfield +806c69c0 Objects::Itembox::IsActive +806c69d4 Objects::Itembox::IsSolid +806c69dc Objects::Itembox::OnNormalCollision +806c69e0 Objects::Itembox::UpdateModelScale +806c69e4 Objects::Itembox::UpdateModelMatrix +806c69e8 Objects::Itembox::LoadShadow +806c69fc Objects::Itembox::GetPropertiesBitfield +806c6a04 Objects::HwanwanItembox::GetBRRESName +806c6a10 Objects::HwanwanItembox::GetSubFileName +806c6a1c Objects::HwanwanItembox::GetName +806c6a28 Objects::HwanwanItembox::GetID +806c6a40 Objects::HwanwanItembox::__dt +806c6a80 Objects::HwanwanItembox::LoadRoute +806c6a84 Objects::Itembox::UpdateShadow +806c6a88 0x806c6a88 +806c6a8c Objects::OnStart +806c6ad8 Objects::Hwanwan_Chn::ToggleVisible +806c6adc Objects::Hwanwan_Chn::__dt +806c6b1c Objects::Hwanwan_Chn::LoadCollision +806c6b20 Objects::Hwanwan_Chn::LoadRoute +806c6b24 Objects::Hwanwan_Chn::GetName +806c6b30 Objects::Hwanwan_Chn::GetSubFileName +806c6b3c Objects::Hwanwan_Chn::GetPropertiesBitfield +806c6b44 Objects::HanachanPart::LoadModels +806c6b7c Objects::HanachanPart::OnStart +806c6c08 Objects::HanachanPart::vf_0x100 +806c6c10 Objects::HanachanPart::Update +806c6c94 Objects::HanachanPart::OnCollision +806c6e40 Objects::HanachanPart::OnItemCollision +806c7064 0x806c7064 +806c7188 0x806c7188 +806c723c 0x806c723c +806c72b8 0x806c72b8 +806c7674 0x806c7674 +806c7798 0x806c7798 +806c77b8 0x806c77b8 +806c78cc 0x806c78cc +806c78f4 0x806c78f4 +806c7a14 0x806c7a14 +806c7b14 0x806c7b14 +806c7c6c nw4r::g3d::ICalcWorldCallback::__dt +806c7cac Objects::BossHanachanHead::CalcWorldCB::ExecCallbackB +806c7d68 0x806c7d68 +806c7d74 Objects::BossHanachanHead::__ct +806c7e68 Objects::HanachanPart::__dt +806c7ea8 0x806c7ea8 +806c818c Objects::BossHanachanHead::LoadCollision +806c8204 Objects::BossHanachanHead::OnStart +806c82ac Objects::BossHanachanHead::Update +806c8450 Objects::BossHanachanHead::UpdateCollision +806c85d4 0x806c85d4 +806c8724 0x806c8724 +806c87bc 0x806c87bc +806c87cc Objects::HanachanBody::LoadAnimations +806c8878 Objects::HanachanBody::OnStart +806c8908 0x806c8908 +806c8a5c Objects::Hanachan::__ct +806c9534 0x806c9534 +806c9598 Objects::Hanachan::__dt +806c9630 Objects::Hanachan::OnStart +806c9860 Objects::Hanachan::Update +806c9bb8 0x806c9bb8 +806c9bc0 0x806c9bc0 +806c9d38 0x806c9d38 +806c9ec8 0x806c9ec8 +806c9f90 0x806c9f90 +806c9f98 0x806c9f98 +806c9fb0 0x806c9fb0 +806ca248 Objects::Hanachan::vf_0xC(parent) +806ca24c 0x806ca24c +806ca27c 0x806ca27c +806ca2f0 0x806ca2f0 +806ca554 0x806ca554 +806ca55c 0x806ca55c +806ca5e4 0x806ca5e4 +806ca6cc 0x806ca6cc +806ca72c 0x806ca72c +806ca8e0 0x806ca8e0 +806ca9ac 0x806ca9ac +806caa8c 0x806caa8c +806caad0 0x806caad0 +806cab5c 0x806cab5c +806cacd0 0x806cacd0 +806caed4 0x806caed4 +806caf9c 0x806caf9c +806cafb8 0x806cafb8 +806cb0a8 0x806cb0a8 +806cb1e8 0x806cb1e8 +806cb32c 0x806cb32c +806cb3fc 0x806cb3fc +806cb588 0x806cb588 +806cb590 0x806cb590 +806cb764 0x806cb764 +806cb950 0x806cb950 +806cb9e0 0x806cb9e0 +806cbbc4 0x806cbbc4 +806cbe2c 0x806cbe2c +806cbe48 0x806cbe48 +806cbea8 0x806cbea8 +806cbf18 0x806cbf18 +806cbf88 0x806cbf88 +806cbff8 0x806cbff8 +806cc14c 0x806cc14c +806cc24c 0x806cc24c +806cc43c 0x806cc43c +806cc48c 0x806cc48c +806cc6c0 0x806cc6c0 +806cc8e8 0x806cc8eA +806cc9e4 nw4r::g3d::ICalcWorldCallback::ExecCallbackC +806cc9e8 nw4r::g3d::ICalcWorldCallback::ExecCallbackA +806cc9ec Objects::Hanachan::UpdateModel +806cc9f0 Objects::Hanachan::LoadClipInfo +806cc9f4 Objects::Hanachan::LoadCollision +806cc9f8 Objects::Hanachan::LoadModels +806cc9fc Objects::Hanachan::GetPropertiesBitfield +806cca04 Objects::HanachanPart::vf_0x104 +806cca0c Objects::HanachanPart::vf_0x84 +806cca14 Objects::HanachanPart::UpdateModelMatrix +806ccacc Objects::HanachanPart::LoadRoute +806ccad0 Objects::HanachanPart::GetPropertiesBitfield +806ccad8 Objects::HanachanBody::__dt +806ccb18 Objects::HanachanBody::vf_0xfc +806ccb20 Objects::HanachanBody::vf_0xf8 +806ccb28 Objects::HanachanBody::vf_0x108 +806ccb30 0x806ccb30 +806ccb4c 0x806ccb4c +806ccb6c 0x806ccb6c +806ccb74 Objects::HanachanBody::GetShadowResName +806ccb80 Objects::HanachanBody::GetSubFileName +806ccb88 Objects::HanachanPart::GetSubFileName +806ccb94 Objects::BossHanachanHead::__dt +806ccbd4 Objects::BossHanachanHead::vf_0xfc +806ccbf0 Objects::BossHanachanHead::vf_0xf8 +806ccbf8 Objects::BossHanachanHead::vf_0xf4 +806ccc44 Objects::BossHanachanHead::vf_0xf0 +806ccca4 Objects::BossHanachanHead::vf_0xec +806ccd24 Objects::BossHanachanHead::UpdateShadow +806ccd98 Objects::BossHanachanHead::vf_0x108 +806ccda0 Objects::BossHanachanHead::LoadAnimations +806cce28 Objects::BossHanachanHead::GetBRRESName +806cce34 nw4r::g3d::ICalcWorldCallback::ExecCallbackB +806cce38 Objects::BossHanachanHead::CalcWorldCB::__dt +806cce78 Objects::BossHanachanHead::CalcWorldCB::ExecCallbackB(thunk) +806cce80 Objects::BossHanachanHead::CalcWorldCB::__dt(thunk) +806cce88 Objects::Hanachan::__dt(thunk) +806cce90 0x806cce90 +806cd044 0x806cd044 +806cd328 0x806cd328 +806cd458 0x806cd458 +806cd4c0 0x806cd4c0 +806cd6e0 0x806cd6e0 +806cd868 0x806cd868 +806cd994 0x806cd994 +806cdcc0 0x806cdcc0 +806cdcc4 0x806cdcc4 +806cdd20 0x806cdd20 +806cdddc 0x806cdddc +806cdde0 0x806cdde0 +806cdde4 0x806cdde4 +806cdec8 0x806cdec8 +806ce03c 0x806ce03c +806ce168 0x806ce168 +806ce354 0x806ce354 +806ce39c 0x806ce39c +806ce3d0 0x806ce3d0 +806ce430 0x806ce430 +806ce548 0x806ce548 +806ce7b0 0x806ce7b0 +806ce808 0x806ce808 +806ce810 0x806ce810 +806ce818 0x806ce818 +806ce820 0x806ce820 +806ce828 Objects::Heyho::__ct +806ceac0 0x806ceac0 +806ceb24 Objects::Heyho::__dt +806ceb90 Objects::Heyho::OnStart +806cedf4 Objects::Heyho::vf_0x10 +806cedf8 Objects::Heyho::Update +806cf024 Objects::Heyho::OnCollision +806cf198 Objects::Heyho::OnItemCollision +806cf3a4 Objects::Heyho::UpdateShadow +806cf4cc 0x806cf4cc +806cf4d0 obj_heyho_move_update +806cf714 obj_heyho_beforeJump +806cf72c obj_heyho_jump_update +806cfac0 0x806cfac0 +806cfb38 0x806cfb38 +806cfd48 0x806cfd48 +806cfdb0 0x806cfdb0 +806cffb0 0x806cffb0 +806d0028 0x806d0028 +806d00b0 0x806d00b0 +806d013c Objects::Heyho::LoadAnimations +806d01cc Objects::Heyho::vf_0xc +806d01d4 Objects::Heyho::UpdateCollision +806d02b4 Objects::Heyho::GetPropertiesBitfield +806d02bc 0x806d02bc +806d02c4 Objects::HeyHoBallGBA::__ct +806d058c 0x806d058c +806d05f0 Objects::HeyHoBallGBA::OnStart +806d0780 Objects::HeyHoBallGBA::Update +806d0880 Objects::HeyHoBallGBA::OnCollision +806d0978 Objects::HeyHoBallGBA::OnItemCollision +806d0a00 0x806d0a00 +806d0a14 0x806d0a14 +806d0a3c 0x806d0a3c +806d0ad8 0x806d0ad8 +806d0c0c 0x806d0c0c +806d0cd8 0x806d0cd8 +806d0d84 0x806d0d84 +806d0f24 0x806d0f24 +806d10a4 Objects::HeyHoBallGBA::vf_0xec +806d11c8 0x806d11c8 +806d1308 0x806d1308 +806d1384 0x806d1384 +806d14d8 0x806d14d8 +806d1570 0x806d1570 +806d15a8 0x806d15a8 +806d1820 Objects::HeyHoBallGBA::__dt +806d188c Objects::HeyHoBallGBA::RequestShoot +806d1898 0x806d1898 +806d18a0 Objects::HeyHoBallGBA::LoadAnimations +806d18ec Objects::HeyHoBallGBA::GetPropertiesBitfield +806d18f4 0x806d18f4 +806d18fc Objects::HeyHoShipGBA::__ct +806d1998 Objects::MapObjShooter::__dt +806d19d8 Objects::HeyHoShipGBA::OnStart +806d1b9c Objects::HeyHoShipGBA::Update +806d1ca4 Objects::HeyHoShipGBA::GetShootFromRoutePointID +806d1cc4 0x806d1cc4 +806d1d10 0x806d1d10 +806d1dd4 0x806d1dd4 +806d203c 0x806d203c +806d20c4 0x806d20c4 +806d2320 Objects::HeyHoShipGBA::__dt +806d2360 Objects::HeyHoShipGBA::GetPropertiesBitfield +806d2368 Objects::HeyHoShipManager::__ct +806d2514 Objects::HeyHoShipManager::__dt +806d2590 Objects::HeyHoShipManager::OnStart +806d2868 Objects::HeyHoShipManager::Update +806d28ec 0x806d28ec +806d28f0 Objects::MapObjSniper::LoadClipInfo +806d28f4 Objects::MapObjSniper::LoadCollision +806d28f8 Objects::MapObjSniper::LoadRoute +806d28fc Objects::MapObjSniper::LoadModels +806d2900 Objects::MapObjSniper::GetPropertiesBitfield +806d2908 Objects::HighwayManager::__ct +806d2fe8 Objects::HighwayManager::__dt +806d30bc 0x806d30bc +806d332c Objects::HighwayManager::OnStart +806d3448 0x806d3448 +806d345c Objects::HighwayManager::Update +806d394c 0x806d394c +806d3c9c 0x806d3c9c +806d3e5c 0x806d3e5c +806d401c 0x806d401c +806d435c 0x806d435c +806d469c 0x806d469c +806d4930 0x806d4930 +806d4bc4 0x806d4bc4 +806d4d18 0x806d4d18 +806d4e6c 0x806d4e6c +806d4eb4 0x806d4eb4 +806d4efc 0x806d4efc +806d4f44 0x806d4f44 +806d4f8c 0x806d4f8c +806d4fd4 0x806d4fd4 +806d501c 0x806d501c +806d5064 0x806d5064 +806d50ac 0x806d50ac +806d51c8 0x806d51c8 +806d52e0 0x806d52e0 +806d54bc 0x806d54bc +806d55a0 0x806d55a0 +806d55ec 0x806d55ec +806d5700 0x806d5700 +806d58c4 0x806d58c4 +806d58cc Objects::K_truck_light::LoadAnimations +806d595c Objects::K_truck_light::LoadCollision +806d5960 Objects::K_truck_light::LoadRoute +806d5964 Objects::K_truck_light::GetSubFileName +806d5970 Objects::K_truck_light::GetBRRESName +806d597c Objects::K_truck_light::GetPropertiesBitfield +806d5984 Objects::K_truck_light::GetID +806d598c Objects::K_truck_light::__dt +806d59cc 0x806d59cc +806d59d4 Objects::K_car_light::LoadAnimations +806d5a64 Objects::K_car_light::LoadCollision +806d5a68 Objects::K_car_light::LoadRoute +806d5a6c Objects::K_car_light::GetSubFileName +806d5a78 Objects::K_car_light::GetBRRESName +806d5a84 Objects::K_car_light::GetPropertiesBitfield +806d5a8c Objects::K_car_light::GetID +806d5a94 Objects::K_car_light::__dt +806d5ad4 0x806d5ad4 +806d5adc Objects::K_truck_tire::LoadAnimations +806d5b28 Objects::K_truck_tire::LoadCollision +806d5b2c Objects::K_truck_tire::LoadRoute +806d5b30 Objects::K_truck_tire::GetSubFileName +806d5b3c Objects::K_truck_tire::GetBRRESName +806d5b48 Objects::K_truck_tire::GetPropertiesBitfield +806d5b50 Objects::K_truck_tire::GetID +806d5b58 Objects::K_truck_tire::__dt +806d5b98 0x806d5b98 +806d5ba0 Objects::K_car_tire::LoadAnimations +806d5bec Objects::K_car_tire::LoadCollision +806d5bf0 Objects::K_car_tire::LoadRoute +806d5bf4 Objects::K_car_tire::GetSubFileName +806d5c00 Objects::K_car_tire::GetBRRESName +806d5c0c Objects::K_car_tire::GetPropertiesBitfield +806d5c14 Objects::K_car_tire::GetID +806d5c1c Objects::K_car_tire::__dt +806d5c5c Objects::HighwayManager::LoadClipInfo +806d5c60 Objects::HighwayManager::LoadCollision +806d5c64 Objects::HighwayManager::LoadRoute +806d5c68 Objects::HighwayManager::LoadModels +806d5c6c Objects::HighwayManager::GetPropertiesBitfield +806d5c74 0x806d5c74 +806d5d4c 0x806d5d4c +806d5e98 0x806d5e98 +806d5ed8 0x806d5ed8 +806d5edc 0x806d5edc +806d5ee4 Objects::HighwayCar::__ct +806d68b0 Objects::HighwayCar::GetBRRESName +806d68b8 0x806d68b8 +806d691c Objects::HighwayCar::__dt +806d6988 Objects::HighwayCar::LoadModels +806d69c0 Objects::HighwayCar::GetCollisionDiameter +806d6b14 Objects::HighwayCar::OnStart +806d6ecc Objects::HighwayCar::Update +806d7328 Objects::HighwayCar::OnCollision +806d7780 Objects::HighwayCar::OnItemCollision +806d7af8 Objects::HighwayCar::LoadCollision +806d7bf8 Objects::HighwayCar::UpdateCollision +806d7cf8 Objects::HighwayCar::GetSolidityCenter +806d7d64 0x806d7d64 +806d7d68 0x806d7d68 +806d7d6c 0x806d7d6c +806d7d70 0x806d7d70 +806d7e08 0x806d7e08 +806d7e0c 0x806d7e0c +806d7ea4 0x806d7ea4 +806d7f58 0x806d7f58 +806d827c 0x806d827c +806d8b5c 0x806d8b5c +806d8de8 0x806d8de8 +806d9000 0x806d9000 +806d93a8 0x806d93a8 +806d9430 0x806d9430 +806d9504 0x806d9504 +806d95a0 0x806d95a0 +806d96e0 0x806d96e0 +806d9838 0x806d9838 +806d9914 0x806d9914 +806d998c 0x806d998c +806d9a04 0x806d9a04 +806d9a3c 0x806d9a3c +806d9b7c 0x806d9b7c +806d9c70 0x806d9c70 +806d9d2c 0x806d9d2c +806d9d9c 0x806d9d9c +806d9f60 0x806d9f60 +806d9fcc 0x806d9fcc +806da4b8 0x806da4b8 +806da58c 0x806da58c +806da660 Objects::HighwayCar::ProcessCollision +806da728 Objects::HighwayCar::UpdateShadow +806da79c Objects::HighwayCar::GetShadowResName +806da7a4 Objects::HighwayCar::GetSubFileName +806da7ac Objects::HighwayCar::GetPropertiesBitfield +806da7b4 0x806da7b4 +806da7bc Objects::Kbomneji::LoadAnimations +806da800 Objects::Kbomneji::LoadCollision +806da804 Objects::Kbomneji::LoadRoute +806da808 Objects::Kbomneji::GetSubFileName +806da814 Objects::Kbomneji::GetPropertiesBitfield +806da81c Objects::Kbomneji::__dt +806da85c 0x806da85c +806da864 Objects::K_bomb_tire00::LoadAnimations +806da8b0 Objects::K_bomb_tire00::LoadCollision +806da8b4 Objects::K_bomb_tire00::LoadRoute +806da8b8 Objects::K_bomb_tire00::GetSubFileName +806da8c4 Objects::K_bomb_tire00::GetPropertiesBitfield +806da8cc Objects::K_bomb_tire00::__dt +806da90c Objects::HighwayCar::__dt(thunk) +806da914 Objects::KoopaFigure64::__ct +806daa44 Objects::KoopaFigure64::OnStart +806dab5c Objects::KoopaFigure64::Update +806daf10 Objects::KoopaFigure64::UpdateForCPURace +806dafb8 Objects::KoopaFigure64::UpdateCollision +806db114 Objects::KoopaFigure64::__dt +806db154 Objects::KoopaFigure64::LoadCollision +806db168 Objects::KoopaFigure64::OnCollision +806db184 Objects::Kuribo::__ct +806db33c 0x806db33c +806db3a0 Objects::Kuribo::__dt +806db40c Objects::Kuribo::OnStart +806db5ac Objects::Kuribo::vf_0x10 +806db5b0 Objects::Kuribo::Update +806db764 Objects::Kuribo::OnCollision +806dba98 Objects::Kuribo::OnItemCollision +806dc21c Objects::Kuribo::OnRouteEnd +806dc220 Objects::Kuribo::UpdateRouteEnd +806dc3f4 Objects::Kuribo::OnCycleBegin +806dc3f8 Objects::Kuribo::UpdateCycle +806dc460 0x806dc460 +806dc5e0 0x806dc5e0 +806dc77c 0x806dc77c +806dc790 0x806dc790 +806dc978 0x806dc978 +806dc9d8 0x806dc9d8 +806dcb58 0x806dcb58 +806dcc9c 0x806dcc9c +806dcd48 0x806dcd48 +806dcdd0 0x806dcdd0 +806dcddc 0x806dcddc +806dcfd8 0x806dcfd8 +806dd038 0x806dd038 +806dd128 0x806dd128 +806dd270 Objects::Kuribo::vf_0xC(ObjectCycleManager) +806dd278 Objects::Kuribo::LoadAnimations +806dd2c8 0x806dd2c8 +806dd2d0 Objects::Kuribo::__dt(thunk) +806dd2d8 Objects::Balloon::__ct +806dd440 Objects::Balloon::__dt +806dd480 Objects::Balloon::OnStart +806dd5dc Objects::Balloon::vf_0x10 +806dd5e0 Objects::Balloon::Update +806dd7d0 0x806dd7d0 +806dd858 Objects::Balloon::UpdateShadow +806dd924 Objects::Balloon::GetShadowListIdx +806dd92c Objects::Balloon::GetPropertiesBitfield +806dd934 0x806dd934 +806dd998 0x806dd998 +806dd9d8 0x806dd9d8 +806dda54 0x806dda54 +806dda80 0x806dda80 +806dda84 Objects::MapObjSniper::__ct +806ddaf4 Objects::MapObjSniper::__dt +806ddb34 Objects::MapObjSniper::OnStart +806ddc44 Objects::MapObjSniper::Update +806ddcb0 0x806ddcb0 +806ddd20 0x806ddd20 +806ddda0 0x806ddda0 +806dddbc 0x806dddbc +806dddc0 0x806dddc0 +806dddc8 0x806dddc8 +806dddcc 0x806dddcc +806dddd0 0x806dddd0 +806dddd4 0x806dddd4 +806dddd8 Objects::SunDS::__ct +806ddf04 0x806ddf04 +806ddf68 Objects::SunDS::__dt +806ddfd4 Objects::SunDS::OnStart +806de03c Objects::SunDS::Update +806de1c8 0x806de1c8 +806de1e4 0x806de1e4 +806de408 0x806de408 +806de454 0x806de454 +806de458 0x806de458 +806de4e4 0x806de4e4 +806de510 0x806de510 +806de568 0x806de568 +806de598 Objects::SunDS::GetShootFromRoutePointID +806de5c8 0x806de5c8 +806de5d0 Objects::SunDS::LoadAnimations +806de614 Objects::SunDS::GetPropertiesBitfield +806de61c Objects::SunDS::__dt(thunk) +806de624 Objects::SunDSManager::__ct +806de780 Objects::SunDSManager::__dt +806de7fc 0x806de7fc +806de914 0x806de914 +806de9b0 0x806de9b0 +806dea1c 0x806dea1c +806deb40 Objects::TruckChimSmk::__ct +806ded34 Objects::TruckChimSmk::__dt +806ded74 Objects::TruckChimSmk::OnStart +806def0c Objects::TruckChimSmk::Update +806df3f4 Objects::TruckChimSmk::OnCollision +806df558 Objects::TruckChimSmk::OnItemCollision +806df584 Objects::TruckChimSmk::UpdateCollision +806df67c Objects::TruckChimSmk::vf_0x10 +806df680 0x806df680 +806df770 Objects::TruckChimSmk::OnWallCollision +806df774 Objects::TruckChimSmk::LoadAnimations +806df7c0 0x806df7c0 +806df7c8 Objects::TruckChimSmk::GetPropertiesBitfield +806df7d0 Objects::TruckChimSmkW::__ct +806df94c Objects::TruckChimSmkW::LoadModels +806df960 Objects::TruckChimSmkW::LoadAnimations +806df9b8 Objects::TruckChimSmkW::LoadCollision +806df9cc Objects::TruckChimSmkW::GetSolidityCenter +806dfa64 Objects::TruckChimSmkW::OnStart +806dfb18 Objects::TruckChimSmkW::Update +806dfccc Objects::TruckChimSmkW::__dt +806dfd0c Objects::TruckChimSmkW::GetScnObjDrawOptionsIdx +806dfd14 0x806dfd14 +806dfd94 0x806dfd94 +806dfe34 0x806dfe34 +806dfe84 0x806dfe84 +806dfe9c 0x806dfe9c +806e0050 0x806e0050 +806e0090 0x806e0090 +806e00f4 0x806e00f4 +806e0160 0x806e0160 +806e01c0 0x806e01c0 +806e03e4 0x806e03e4 +806e03e8 0x806e03e8 +806e0650 0x806e0650 +806e0834 0x806e0834 +806e0b30 0x806e0b30 +806e0d48 0x806e0d48 +806e0df0 0x806e0df0 +806e1248 0x806e1248 +806e12d0 0x806e12d0 +806e1370 0x806e1370 +806e17f4 0x806e17f4 +806e1894 0x806e1894 +806e1898 0x806e1898 +806e1940 0x806e1940 +806e1dd0 0x806e1dd0 +806e1e34 0x806e1e34 +806e1f20 0x806e1f20 +806e1f90 0x806e1f90 +806e2060 0x806e2060 +806e206c ObjTruckWagon_construct +806e21ec 0x806e21ec +806e222c 0x806e222c +806e239c 0x806e239c +806e23a0 0x806e23a0 +806e23a4 0x806e23a4 +806e248c 0x806e248c +806e24dc 0x806e24dc +806e24e0 0x806e24e0 +806e24e4 0x806e24e4 +806e24e8 0x806e24e8 +806e24ec 0x806e24ec +806e24f4 0x806e24f4 +806e24fc 0x806e24fc +806e256c 0x806e256c +806e2574 0x806e2574 +806e2604 obj_draw_shadow_init +806e2608 0x806e2608 +806e260c 0x806e260c +806e2618 0x806e2618 +806e2624 0x806e2624 +806e2744 0x806e2744 +806e274c 0x806e274c +806e27a4 0x806e27a4 +806e27fc 0x806e27fc +806e2804 0x806e2804 +806e2860 0x806e2860 +806e286c 0x806e286c +806e2878 0x806e2878 +806e287c 0x806e287c +806e2880 0x806e2880 +806e2888 0x806e2888 +806e28c8 0x806e28c8 +806e28d0 0x806e28d0 +806e28d4 0x806e28d4 +806e28d8 0x806e28d8 +806e28e4 0x806e28e4 +806e28f0 0x806e28f0 +806e28f4 0x806e28f4 +806e28fc 0x806e28fc +806e2904 0x806e2904 +806e2b7c 0x806e2b7c +806e2be0 0x806e2be0 +806e2c4c 0x806e2c4c +806e2cf0 0x806e2cf0 +806e2d80 0x806e2d80 +806e2e08 0x806e2e08 +806e2f24 0x806e2f24 +806e2f34 0x806e2f34 +806e2f38 0x806e2f38 +806e3034 0x806e3034 +806e327c 0x806e327c +806e3324 0x806e3324 +806e344c 0x806e344c +806e3458 0x806e3458 +806e384c 0x806e384c +806e388c 0x806e388c +806e3920 0x806e3920 +806e3a64 0x806e3a64 +806e3a68 0x806e3a68 +806e3a6c 0x806e3a6c +806e3a70 0x806e3a70 +806e3a74 0x806e3a74 +806e3a7c 0x806e3a7c +806e3a84 0x806e3a84 +806e3a8c 0x806e3a8c +806e3b8c 0x806e3b8c +806e3bc8 0x806e3bc8 +806e3de0 0x806e3de0 +806e3e20 Objects::Pile::OnStart +806e3edc Objects::Pile::Update +806e3ef0 0x806e3ef0 +806e3f20 0x806e3f20 +806e41a4 Objects::Wanwan_Chn::LoadCollision +806e4208 Objects::Wanwan_Chn::Update +806e4218 0x806e4218 +806e4220 Objects::Wanwan_Chn::UpdateCollision +806e4224 Objects::Wanwan::__ct +806e4a64 Objects::Wanwan_Chn::OnStart +806e4a88 0x806e4a88 +806e4aec Objects::Wanwan::__dt +806e4b98 Objects::Wanwan::vf_0x10 +806e4b9c Objects::Wanwan::OnStart +806e4f2c Objects::Wanwan::Update +806e526c Objects::Wanwan::OnCollision +806e546c Objects::Wanwan::OnItemCollision +806e56c4 0x806e56c4 +806e59bc 0x806e59bc +806e5a8c 0x806e5a8c +806e5d70 0x806e5d70 +806e5edc 0x806e5edc +806e6208 Objects::Wanwan::BeforeWanderB +806e63a4 Objects::Wanwan::WanderBUpdate +806e6730 Objects::Wanwan::BeforeWanderA +806e6838 Objects::Wanwan::WanderAUpdate +806e6b58 Objects::Wanwan::BeforeLookUp +806e6b7c Objects::Wanwan::LookUpUpdate +806e6c54 Objects::Wanwan::BeforeDigDown +806e6c98 Objects::Wanwan::DigDownUpdate +806e6dd4 Objects::Wanwan::BeforeLookStraight +806e6dd8 Objects::Wanwan::LookStraightUpdate +806e6eb0 Objects::Wanwan::BeforeAttack +806e6f6c Objects::Wanwan::AttackUpdate +806e70c4 0x806e70c4 +806e73c4 Objects::Wanwan::BeforeRetreat +806e7494 Objects::Wanwan::RetreatUpdate +806e7594 0x806e7594 +806e7638 0x806e7638 +806e78b0 0x806e78b0 +806e794c 0x806e794c +806e79e4 0x806e79e4 +806e7b7c 0x806e7b7c +806e7ba4 0x806e7ba4 +806e7e38 0x806e7e38 +806e7edc vector3_lerp +806e7f64 0x806e7f64 +806e8010 0x806e8010 +806e8014 0x806e8014 +806e8340 0x806e8340 +806e8488 0x806e8488 +806e8520 0x806e8520 +806e87c8 0x806e87c8 +806e896c 0x806e896c +806e8a60 0x806e8a60 +806e8f4c 0x806e8f4c +806e9084 0x806e9084 +806e9464 Objects::Wanwan::UpdateModelMatrix +806e9468 Objects::Wanwan::LoadAnimations +806e94b4 Objects::Wanwan::vf_0xC +806e94bc Objects::Wanwan::GetPropertiesBitfield +806e94c4 Objects::Wanwan_Chn::GetCollisionDiameter +806e94dc Objects::Wanwan_Chn::GetName +806e94e8 Objects::Wanwan_Chn::GetSubFileName +806e94f4 Objects::Wanwan_Chn::GetPropertiesBitfield +806e94fc Objects::Wanwan_Chn::__dt +806e953c Objects::Pile::GetName +806e9548 Objects::Pile::GetSubFileName +806e9554 Objects::Pile::GetBRRESName +806e9560 Objects::Pile::GetPropertiesBitfield +806e9568 Objects::Pile::__dt +806e95a8 Objects::Wanwan::__dt +806e95b0 Objects::HwanwanBall::__ct +806e96c0 0x806e96c0 +806e9724 Objects::HwanwanBall::OnStart +806e9a78 Objects::HwanwanBall::Update +806e9cac Objects::HwanwanBall::OnCollision +806e9d6c Objects::HwanwanBall::OnItemCollision +806e9e04 0x806e9e04 +806e9e10 0x806e9e10 +806e9fe0 0x806e9fe0 +806e9fe4 0x806e9fe4 +806ea288 0x806ea288 +806ea2fc 0x806ea2fc +806ea6c4 0x806ea6c4 +806ea784 0x806ea784 +806eaae8 0x806eaae8 +806eac4c 0x806eac4c +806eacd4 0x806eacd4 +806eb0a8 0x806eb0a8 +806eb184 0x806eb184 +806eb460 0x806eb460 +806eb650 0x806eb650 +806eb86c 0x806eb86c +806ebd58 0x806ebd58 +806ebe90 Objects::HwanwanBall::UpdateShadow +806ebf8c 0x806ebf8c +806ec030 0x806ec030 +806ec0f0 0x806ec0f0 +806ec280 0x806ec280 +806ec2fc 0x806ec2fc +806ec44c 0x806ec44c +806ec520 0x806ec520 +806ec5e8 0x806ec5e8 +806ec6e0 Objects::HwanwanBall::__dt +806ec74c Objects::HwanwanBall::UpdateModelMatrix +806ec750 Objects::HwanwanBall::LoadAnimations +806ec7a0 Objects::HwanwanBall::vf_0xc +806ec7a8 Objects::HwanwanBall::LoadRoute +806ec7ac Objects::HwanwanBall::GetSubFileName +806ec7b8 Objects::HwanwanBall::GetPropertiesBitfield +806ec7c0 Objects::HwanwanBall::__dt(thunk) +806ec7c8 Effects::Menus::__ct +806ec914 Effects::Menus::__dt +806ec994 Effects::Menus::GetEffectByIdx +806ec9a4 Route::__ct(POTI) +806ecafc RoutePoint::__ct +806ecb00 RoutePoint::__dt +806ecb40 Route::__ct(PointsArray) +806ecc40 Route::__dt +806eccc0 Route::FillNormals +806ece5c Route::FillBLIGHTIndexes +806ecf88 Route::UpdatePoints +806ed110 Route::AddPoint +806ed150 Route::GetPointById +806ed160 RouteController::__ct +806ed204 0x806ed204 +806ed24c RouteController::GetNormal +806ed2b4 RouteController::GetBLIGHTIndex +806ed30c RouteController::GetTotalDistance +806ed34c 0x806ed34c +806ed3e4 0x806ed3e4 +806ed428 0x806ed428 +806ed53c RouteController::__dt +806ed57c RouteCurved::__ct(POTI) +806ed6c0 RouteArc::__ct +806ed6c4 RouteArc::__ct +806ed704 RouteCurved::__ct(PointsArray) +806ed828 RouteCurved::__dt +806ed8bc RouteCurved::Update +806ed960 RouteCurved::UpdateTotalDistance +806eda04 RouteCurved::Init +806ee27c 0x806ee27c +806ee408 0x806ee408 +806ee4b8 RouteCurved::Calc +806ee56c 0x806ee56c +806ee72c 0x806ee72c +806ee830 RouteCurvedController::__ct +806ee924 RouteCurvedController::Init +806eead8 RouteCurvedController::SetParams +806eeb94 RouteCurvedController::UpdateSpeed +806eebec RouteCurvedController::Update +806eeebc RouteCurvedController::vf_0x24 +806eefa0 RouteCurvedController::vf_0x28 +806ef09c RouteCurvedController::GetDistanceToNextPoint +806ef0d8 RouteCurvedController::GetTotalPercent +806ef0f8 RouteCurvedController::GetTotalPercentNormalDir +806ef224 RouteCurvedController::CalculateBezier +806ef350 RouteCurvedController::CalculateBezierCurve +806ef454 RouteCurvedController::CalculateBezierDir +806ef648 RouteController::IsAtRouteEnd +806ef664 0x806ef664 +806ef804 0x806ef804 +806ef870 0x806ef870 +806ef8f0 0x806ef8f0 +806ef93c RouteCurvedController::GetSpeed +806ef944 RouteCurvedController::__dt +806ef984 RouteCurved::GetPercentFromOriginArrays +806ef98c RouteCurved::GetPercent +806ef994 RouteCurved::GetPointsPerArc +806ef99c RouteCurved::GetArcArray +806ef9a4 RouteCurved::GetSegmentArray +806ef9ac RouteCurved::GetTotalDistance +806ef9b4 RouteStraight::__ct(POTI) +806efbf0 RouteSegment::__ct +806efbf4 RouteSegment::__dt +806efc34 RouteStraight::__ct(PointsArray) +806efd6c RouteStraight::__dt +806efdc4 RouteStraightController::__ct +806efe50 RouteStraightController::SetParams +806efeac RouteStraightController::Init +806efff4 RouteStraightController::UpdateSpeed +806f0050 RouteStraightController::Update +806f02ec RouteStraightController::vf_0x24 +806f041c RouteStraightController::vf_0x28 +806f050c RouteStraightController::GetDistanceToNextPoint +806f0540 0x806f0540 +806f05f4 0x806f05f4 +806f0610 0x806f0610 +806f0814 0x806f0814 +806f0880 0x806f0880 +806f0900 0x806f0900 +806f0944 RouteStraightController::GetSpeed +806f094c RouteStraightController::__dt +806f098c RouteStraight::UpdateTotalDistance +806f0990 RouteStraight::Update +806f0994 RouteStraight::GetPercentFromOriginArrays +806f099c RouteStraight::GetPercent +806f09a8 RouteStraight::GetPointsPerArc +806f09b0 RouteStraight::GetArcArray +806f09b8 RouteStraight::GetSegmentArray +806f09c0 RouteStraight::GetTotalDistance +806f09c8 RouteHolder::CreateInstance +806f0a3c RouteHolder::__ct +806f0a4c RouteHolder::DestroyInstance +806f0a98 RouteHolder::__dt +806f0ad8 RouteHolder::Init +806f0e58 RouteHolder::CreateRoute +806f0fc8 RouteHolder::UpdateRoutePoints +806f1064 RouteHolder::AddRoutePoint +806f10fc RouteHolder::GetRouteById +806f110c 0x806f110c +806f1154 0x806f1154 +806f1158 0x806f1158 +806f1194 0x806f1194 +806f1224 0x806f1224 +806f12b4 0x806f12b4 +806f133c 0x806f133c +806f13fc 0x806f13fc +806f14bc 0x806f14bc +806f1568 0x806f1568 +806f162c 0x806f162c +806f16ec 0x806f16ec +806f1878 0x806f1878 +806f1998 0x806f1998 +806f19f8 0x806f19f8 +806f1a54 0x806f1a54 +806f1c58 0x806f1c58 +806f1db8 0x806f1db8 +806f1f14 0x806f1f14 +806f2074 0x806f2074 +806f220c 0x806f220c +806f22c8 0x806f22c8 +806f2380 0x806f2380 +806f243c 0x806f243c +806f2450 0x806f2450 +806f2470 0x806f2470 +806f2490 0x806f2490 +806f2730 0x806f2730 +806f2944 0x806f2944 +806f2978 0x806f2978 +806f2a38 0x806f2a38 +806f2dac 0x806f2dac +806f2e34 0x806f2e34 +806f2fe8 Linked3DPoints::__ct +806f31f4 Linked3DPoints::__dt +806f3284 0x806f3284 +806f3370 0x806f3370 +806f3450 0x806f3450 +806f3ab4 0x806f3ab4 +806f3ce8 0x806f3ce8 +806f3d78 0x806f3d78 +806f3f2c 0x806f3f2c +806f402c 0x806f402c +806f40f8 0x806f40f8 +806f4160 0x806f4160 +806f4384 0x806f4384 +806f43e8 0x806f43e8 +806f442c 0x806f442c +806f451c 0x806f451c +806f4560 0x806f4560 +806f45a4 0x806f45a4 +806f4600 0x806f4600 +806f4630 0x806f4630 +806f46a8 0x806f46a8 +806f46ec 0x806f46ec +806f471c 0x806f471c +806f4780 0x806f4780 +806f47b0 0x806f47b0 +806f47d4 0x806f47d4 +806f47f8 0x806f47f8 +806f481c 0x806f481c +806f4840 0x806f4840 +806f4864 0x806f4864 +806f4888 0x806f4888 +806f48ac aramGetFirstUserAddress +806f48d0 0x806f48d0 +806f490c 0x806f490c +806f49b4 0x806f49b4 +806f49b8 0x806f49b8 +806f49bc 0x806f49bc +806f4de8 0x806f4de8 +806f5290 0x806f5290 +806f531c 0x806f531c +806f53b8 0x806f53b8 +806f5440 0x806f5440 +806f562c 0x806f562c +806f56d4 0x806f56d4 +806f5900 0x806f5900 +806f5a8c 0x806f5a8c +806f5af0 0x806f5af0 +806f5bf0 0x806f5bf0 +806f5c90 0x806f5c90 +806f5cd0 0x806f5cd0 +806f5d50 0x806f5d50 +806f6044 0x806f6044 +806f60c4 0x806f60c4 +806f6130 0x806f6130 +806f6194 0x806f6194 +806f62fc 0x806f62fc +806f64ac 0x806f64ac +806f661c 0x806f661c +806f6838 0x806f6838 +806f696c Audio::Engine3D::Disposer__dt +806f6a04 Audio::Engine3D::CreateInstance +806f6a94 Audio::Engine3D::DestroyInstance +806f6ac8 Audio::Engine3D::SetCurSound +806f6ad4 0x806f6ad4 +806f6b5c 0x806f6b5c +806f6bec 0x806f6bec +806f6c68 Audio::Engine3D::__ct +806f6ccc 0x806f6ccc +806f6d0c 0x806f6d0c +806f6d4c Audio::Engine3D::__dt +806f6df4 Audio::Engine3D::UpdateAmbientParam +806f7608 Audio::Engine3D::GetRequiredVoiceOutCount +806f7610 0x806f7610 +806f7698 Audio::ItemWarning::GetPan +806f772c Audio::ItemWarning::__ct +806f7740 0x806f7740 +806f7780 0x806f7780 +806f77d4 Audio::ItemWarning::Init +806f7828 Audio::ItemWarningMgr::PlayTargetedWarning +806f7a54 0x806f7a54 +806f7aa8 Audio::ItemWarningMgr::PlayUntargetedWarning +806f7ce0 Audio::ItemWarningMgr::PlayWarningOnWiimote +806f7d58 Audio::ItemWarningMgr::Disposer__dt +806f7ee8 Audio::ItemWarningMgr::CreateInstance +806f7f70 Audio::ItemWarning::__dt +806f7fcc Audio::ItemWarningMgr::DestroyInstance +806f80ac Audio::ItemWarningMgr::__dt +806f8188 Audio::ItemWarningMgr::Init +806f8204 Audio::ItemWarningMgr::Shutdown +806f8210 Audio::ItemWarningMgr::GetItemWarning +806f8220 Audio::SinglePlayer::CalcWaitTime +806f84a8 Audio::SinglePlayer::Disposer__dt +806f8648 Audio::SinglePlayer::CreateInstance +806f86f0 Audio::SinglePlayer::DestroyInstance +806f886c Audio::SinglePlayer::__ct +806f88e8 Audio::Handle::__ct +806f88f4 Audio::Handle::__dt +806f8934 Audio::SinglePlayer::__dt +806f8a64 Audio::SinglePlayer::Calc +806f8b7c Audio::SinglePlayer::PlaySound +806f8d98 Audio::Manager::PrepareSound +806f8dcc Audio::SinglePlayer::PrepareSound +806f8ed8 0x806f8ed8 +806f9034 Audio::SinglePlayer::PlayPreparedSound +806f90b8 Audio::SinglePlayer::PreventActiveSoundStop +806f90c4 Audio::SinglePlayer::StopSound +806f913c Audio::SinglePlayer::StartSound +806f933c Audio::SinglePlayer::SetupActiveHandle +806f9408 Audio::SinglePlayer::SetupActiveHandle_switch +806f940c Audio::SinglePlayer::SetupActiveHandle_caseD_83 +806f943c Audio::SinglePlayer::SetupActiveHandle_caseD_91 +806f9458 Audio::SinglePlayer::SetupActiveHandle_caseD_85 +806f9498 Audio::SinglePlayer::SetupActiveHandle_caseD_7b +806f94c4 Audio::SinglePlayer::SetupActiveHandle_caseD_7d +806f94e0 0x806f94e0 +806f95d8 Audio::SinglePlayer::StartActiveSound +806f9698 Audio::SinglePlayer::GetFreeHandle +806f9724 0x806f9724 +806f9728 Audio::SinglePlayer::GetFanfareLength +806f98ac 0x806f98ac +806f9900 Audio::SinglePlayer::StopInactiveSounds +806f9990 Audio::SinglePlayerValuesMgr::Disposer__dt +806f9abc Audio::SinglePlayerValuesMgr::CreateInstance +806f9b64 Audio::SinglePlayerValuesMgr::DestroyInstance +806f9c18 Audio::SinglePlayerValuesMgr::__dt +806f9cbc Audio::SinglePlayerValuesMgr::SetHandle +806f9ce0 Audio::SinglePlayerValuesMgr::Calc +806f9e14 Audio::SinglePlayerValuesMgr::DetachHandle +806f9e20 Audio::SinglePlayerValuesMgr::UpdateTracks +806f9e78 Audio::SinglePlayerValuesMgr::SetVolume +806f9ebc 0x806f9ebc +806f9f80 Audio::StreamsMgr::Disposer_dt +806fa0ac Audio::StreamsMgr::CreateInstance +806fa154 Audio::StreamsMgr::DestroyInstance +806fa208 Audio::StreamsMgr::__dt +806fa2ac Audio::StreamsMgr::PrepareStreams +806fa3fc Audio::StreamsMgr::ChangeStreamVolume +806fa420 Audio::StreamsMgr::Calc +806fa5ac Audio::StreamsMgr::ResetVolume +806fa604 Audio::SinglePlayer::PlayBGSound +806fa678 Audio::SinglePlayer::UpdateWaitTime +806fa6e4 0x806fa6e4 +806fa738 Audio::SinglePlayer::CalcTitlePageNext +806faab4 Audio::StreamsMgr::ChangeMultipleStreamsVolume +806fab2c Audio::StreamsMgr::ChangeStream +806fae60 Audio::EngineMgr::Init +806fafb4 Audio::EngineMgr::Update +806fb2fc Audio::EngineMgr::CalcSoundId +806fb394 0x806fb394 +806fb444 Audio::EngineMgr::CalcAccelerating +806fb558 Audio::EngineMgr::CalcBraking +806fb640 Audio::EngineMgr::CalcPitch +806fb720 Audio::RaceAmbienceMgr::Disposer__dt +806fb8b8 Audio::RaceAmbienceMgr::CreateInstance +806fb964 Audio::RaceAmbienceMgr::DestroyInstance +806fbab0 Audio::RaceAmbienceMgr::__ct +806fbb30 0x806fbb30 +806fbb70 Audio::RaceAmbienceMgr::__dt +806fbc90 Audio::RaceAmbienceMgr::Init +806fbcc4 Audio::RaceAmbienceMgr::StopGlobalSound +806fbcdc Audio::RaceAmbienceMgr::Update +806fbe14 Audio::RaceAmbienceMgr::StopGlobalSoundFadeOut +806fbe30 Audio::RaceAmbienceMgr::PlayIntroAmbience +806fbec0 Audio::RaceAmbienceMgr::PlayIntroAmbience_switch +806fbec4 Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_1 +806fbf68 Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_2 +806fbf90 Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_3 +806fc064 Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_4 +806fc140 Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_5 +806fc1fc Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_6 +806fc278 Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_7 +806fc2c4 Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_3a +806fc2ec Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_a +806fc368 Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_b +806fc3dc Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_c +806fc44c Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_d +806fc4d0 Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_e +806fc51c Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_f +806fc5a0 Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_10 +806fc628 Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_11 +806fc660 Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_12 +806fc704 Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_13 +806fc7bc Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_14 +806fc860 Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_16 +806fc8cc Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_17 +806fc960 Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_19 +806fc9b0 Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_1a +806fc9dc Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_1b +806fca2c Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_1c +806fca8c Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_1d +806fcb30 Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_1e +806fcb5c Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_1f +806fcb84 Audio::RaceAmbienceMgr::PlayIntroAmbience_caseD_0 +806fcba0 Audio::RaceAmbienceMgr::PlayGlobalSound +806fcd74 Audio::RaceAmbienceMgr::StopGlobalSound(soundId) +806fcda8 Audio::RaceAmbienceMgr::PlayAmbience +806fcefc Audio::RaceAmbienceMgr::SetAmbienceVolume +806fcfa0 Audio::RaceAmbienceMgr::SetAllAmbiencesVolume +806fd03c Audio::RaceAmbienceMgr::UpdateAmbiences +806fd0ac Audio::RaceAmbienceMgr::UpdateVeniceAndRingMissionAmbiences +806fd55c 0x806fd55c +806fd624 Audio::Ambience::__ct +806fd6b0 Audio::Ambience::__dt +806fd73c Audio::Ambience::IsPlayingSoundId +806fd770 Audio::Ambience::PlayAmbience +806fd7d4 Audio::Ambience::ResetVolume +806fd800 Audio::Ambience::SetVolume +806fd818 Audio::Ambience::Update +806fd9e0 0x806fd9e0 +806fdae4 Audio::GameHeap::Disposer__dt +806fdc1c Audio::GameHeap::CreateInstance +806fdcec Audio::GameHeap::DestroyInstance +806fde08 Audio::GameHeap::__ct +806fdeac Audio::GameHeapImpl::__dt +806fdf18 Audio::GameHeap::__dt +806fdffc Audio::GameHeap::Initialize +806fe044 Audio::GameHeap::DestroySoundHeap +806fe084 Audio::GameHeap::SetType +806fe0a0 0x806fe0a0 +806fe0e0 EGG::SoundHeapMgr::SaveState(using) +806fe1b4 EGG::SoundHeapMgr::LoadState(using) +806fe240 EGG::SoundHeapMgr::GetCurrentLevel(using) +806fe294 0x806fe294 +806fe2b8 Audio::ReverbMgr::Disposer__dt +806fe3e4 Audio::ReverbMgr::CreateInstance +806fe4d0 Audio::ReverbMgr::DestroyInstance +806fe584 Audio::ReverbMgr::__ct +806fe654 Audio::ReverbMgr::__dt +806fe6f8 Audio::ReverbMgr::Disable +806fe71c Audio::ReverbMgr::Clear +806fe86c 0x806fe86c +806fe8ac Audio::ReverbMgr::Prepare +806fe944 Audio::ReverbMgr::Play +806fe9bc Audio::ReverbMgr::Disable2 +806fe9e0 Audio::FileManager::__dt +806fea20 0x806fea20 +806fea60 0x806fea60 +806feaa0 Audio::FileManager::Init +806febcc 0x806febcc +806fec98 0x806fec98 +806fee90 Audio::FileManager::LoadBank +806fefd0 0x806fefd0 +806ff110 0x806ff110 +806ff17c Audio::FileManager::IsBankLoaded +806ff204 0x806ff204 +806ff28c 0x806ff28c +806ff4cc 0x806ff4cc +806ff578 Audio::FileManager::InvalidateWaveData +806ff5c4 Audio::FileManager::InvalidateData +806ff610 Audio::FileManager::GetFileWaveDataAddress +806ff638 Audio::FileManager::GetFileAddress +806ff660 Audio::FileManager::AudioFileManager +806ff6b0 Audio::FileManager::__dt(thunk) +806ff6b8 Audio::FileManager::InvalidateData(thunk) +806ff6c0 Audio::FileManager::InvalidateWaveData(thunk) +806ff6c8 Audio::FileManager::SetRequest +806ff864 0x806ff864 +806ff8a4 Audio::FileManager::LoadGroupAsync +806ffab4 0x806ffab4 +806ffbac Audio::GroupBankRequester::Disposer__dt +806ffcd8 Audio::GroupBankRequester::CreateInstance +806ffe40 Audio::GroupBankRequester::DestroyInstance +806ffef4 Audio::GroupBankRequester::__ct +80700030 Audio::GroupBankRequester::__dt +807000d4 Audio::GroupBankRequester::RequestLoad(id) +8070010c Audio::GroupBankRequester::RequestLoad(actorId) +80700184 0x80700184 +807001a4 Audio::Manager::LoadState +80700230 Audio::GroupBankRequester::ProcessRequests +807002bc Audio::GroupBankRequester::LoadAsync +80700440 Audio::GroupBankRequester::GroupBankLoader +80700460 Audio::GroupBankRequester::ConstructRequests +80700474 Audio::GroupBankRequester::Request::__ct +80700578 0x80700578 +807005b8 Audio::Actor<4>::__dt +8070062c 0x8070062c +807006b4 0x807006b4 +80700728 Audio::RaceActor::SetupBRASD +80700754 Audio::RaceActor::UpdateBRASDFrame +807007cc Audio::RaceActor::Update +80700814 Audio::Actor<4>::SetPosition +80700818 Audio::RaceActor::SetupSound +80700894 ObjectSoundAnimNoHandle::__ct +807009bc Audio::RaceActor::__dt +80700a80 ObjectSoundAnimNoHandle::Update +80700ac8 ObjectSoundAnimNoHandle::Link +80700b38 ObjectSoundAnimNoHandle::DisableConditional +80700b80 Audio::RaceActor::DisableConditional +80700b9c ObjectSoundAnimNoHandle::UpdateUserParam3 +80700bbc Audio::RaceActor::UpdateUserParam3 +80700bdc ObjectSoundAnimNoHandle::StartSoundLimited +80700d4c Audio::RaceActor::StartSoundLimited +80700ea8 ObjectSoundAnimNoHandle::HoldSoundLimited +80701018 Audio::RaceAnimActor::__ct +80701140 Audio::RaceAnimActor::StartSound +80701154 Audio::RaceActor::StartSound +807011c8 0x807011c8 +807011e0 Audio::RaceActor::StartSound(StartInfo) +80701260 Audio::RaceAnimActor::HoldSound +80701274 0x80701274 +807012e8 0x807012e8 +80701300 Audio::RaceActor::HoldSound +80701380 Audio::RaceAnimActor::StopSound +80701394 Audio::RaceActor::StopSound +80701398 Audio::RaceActor::HoldSound +807013a0 Audio::RaceActor::StartSound +807013a8 Audio::RaceActor::StartNewSoundLimited +807014e8 Audio::RaceActor::HoldSoundLimited +807016fc Audio::RaceActor::StopAllSound +80701760 Audio::RaceActor::GetPosition +80701768 Audio::RaceActor::UpdateUserParam +8070177c Audio::RaceActor::UpdateUserParam2 +80701794 Audio::RaceActor::detail_UpdateAmbientArg +807017f8 Audio::Actor<4>::TryStartSound(u32) +80701884 Audio::Actor<4>::TryStartSound(ul) +80701910 Audio::Actor<4>::TryStartSound(str) +807019d0 Audio::Actor<4>::TryStopAndStartSound(u32) +80701a64 Audio::Actor<4>::TryStopAndStartSound(ul) +80701af8 Audio::Actor<4>::TryStopAndStartSound(str) +80701bb4 Audio::Actor<4>::TryHoldSound(u32) +80701c9c Audio::Actor<4>::TryHoldSound(ul) +80701d84 Audio::Actor<4>::TryHoldSound(str) +80701ea0 Audio::Actor<4>::TryHoldSound(u32, handleId) +80701f08 Audio::Actor<4>::TryHoldSound(u3l, handleId) +80701f70 Audio::Actor<4>::TryHoldSound(str, handleId) +80702014 Audio::Actor<4>::TryPrepareSound(u32) +807020a0 Audio::Actor<4>::TryPrepareSound(ul) +8070212c Audio::Actor<4>::TryPrepareSound(str) +807021ec Audio::Actor<4>::TryStopAndPrepareSound(u32) +80702280 Audio::Actor<4>::TryStopAndPrepareSound(ul) +80702314 Audio::Actor<4>::TryStopAndPrepareSound(str) +807023d0 ObjectSoundAnimNoHandle::__dt +80702498 Audio::RaceAnimActor::__dt +80702570 Audio::Actor::detail_UpdateAmbientArg +80702578 0x80702578 +80702580 0x80702580 +80702588 Audio::RaceActor::__dt +80702590 ObjectSoundAnimNoHandle::__dt +80702598 ObjectSoundAnimNoHandle::HoldSoundLimited +807025a0 ObjectSoundAnimNoHandle::StartSoundLimited +807025a8 ObjectSoundAnimNoHandle::UpdateUserParam3 +807025b0 ObjectSoundAnimNoHandle::DisableConditional +807025b8 ObjectSoundAnimNoHandle::Update +807025c0 ObjectSoundAnimNoHandle::Link +807025c8 Audio::RaceActor::detail_UpdateAmbientArg +807025d0 0x807025d0 +807025d8 Audio::RaceActor::UpdateBRASDFrame +807025e0 Audio::RaceActor::SetupBRASD +807025e8 Audio::RaceActor::HoldSound +807025f0 Audio::RaceActor::HoldSound(StartInfo) +807025f8 Audio::RaceActor::StartSound +80702600 Audio::RaceActor::StartSound(StartInfo) +80702608 Audio::RaceAnimActor::StopSound +80702610 Audio::RaceAnimActor::HoldSound +80702618 Audio::RaceAnimActor::StartSound +80702620 Audio::RaceActor::StartNewSoundLimited +80702628 Audio::RaceActor::StopAllSound +80702630 Audio::RaceActor::HoldSoundLimited +80702638 Audio::RaceActor::StartSoundLimited +80702640 Audio::RaceActor::UpdateUserParam3 +80702648 Audio::RaceActor::DisableCondition +80702650 Audio::RaceActor::UpdateUserParam2 +80702658 Audio::RaceActor::UpdateUserParam +80702660 Audio::RaceActor::Reset +80702668 Audio::RaceActor::GetPosition +80702670 Audio::RaceActor::Update +80702678 Audio::LinkedRaceActor::__ct +807026cc Audio::LinkedRaceActor::__dt +80702714 Audio::LinkedRaceActor::Link +807027d0 Audio::LinkedRaceActor::Unlink +80702828 0x80702828 +80702848 0x80702848 +807028e4 Audio::LinkedRaceActor::CannotPlaySounds +8070296c 0x8070296c +80702970 Audio::LinkedRaceActor::Toggle +807029cc Audio::LinkedRaceActor::GetKartPosition +807029d4 Audio::LinkedRaceActor::GetObjectPosition +807029e0 Audio::LinkedRaceActor::SetSoundParams +807029f0 Audio::ObjectActorAnim::__ct +80702a44 0x80702a44 +80702a84 Audio::ObjectActorAnim::Link +80702af4 Audio::ObjectActorAnim::Unlink +80702afc Audio::ObjectActorAnim::Update +80702b00 Audio::ObjectActorAnim::StartSoundLimited +807030a4 Audio::ObjectActorAnim::HoldSoundLimited +807034c4 Audio::ObjectActorAnim::StartNewSoundLimited +807036cc Audio::ObjectActorAnim::StartSound +80703ac8 Audio::ObjectActorAnim::SetupSound +80703d64 Audio::ObjectActorAnim::DisableConditional +80703f00 Audio::ObjectActorAnim::UpdateUserParam3 +80703fb0 Audio::ObjectActorAnim::__dt +8070408c Audio::ObjectActorAnim::__dt(thunk) +80704094 Audio::ObjectActorAnim::StartSound(thunk) +8070409c Audio::ObjectActorAnim::StartNewSoundLimited(thunk) +807040a4 Audio::ObjectActorAnim::HoldSoundLimited(thunk) +807040ac Audio::ObjectActorAnim::StartSoundLimited(thunk) +807040b4 Audio::ObjectActorAnim::UpdateUserParam3(thunk) +807040bc Audio::ObjectActorAnim::DisableConditional(thunk) +807040c4 Audio::ObjectActorAnim::Update(thunk) +807040cc Audio::ObjectActorAnim::Unlink(thunk) +807040d4 Audio::ObjectActorAnim::Link(thunk) +807040dc Audio::ItemActor::__ct +80704184 0x80704184 +807041c4 Audio::Actor<2>::__dt +80704238 0x80704238 +807042c0 Audio::ItemActor::Link +807042c8 Audio::ItemActor::Unlink +807042d0 Audio::ItemActor::Update +807042e8 Audio::Actor<2>::SetPosition +807042ec Audio::ItemActor::DisableConditional +80704308 0x80704308 +80704324 Audio::ItemActor::UpdateUserParam3 +80704344 0x80704344 +80704364 Audio::ItemActor::StartSoundLimited +80704594 0x80704594 +807046a0 Audio::ItemActor::HoldSoundLimited +8070482c 0x8070482c +80704998 Audio::ItemActor::StartNewSoundLimited +80704a94 0x80704a94 +80704b7c 0x80704b7c +80704bd4 Audio::GessoActor::__ct +80704c7c 0x80704c7c +80704cf0 0x80704cf0 +80704d78 Audio::GessoActor::Link +80704d80 Audio::GessoActor::Update +80704d84 0x80704d84 +80704d94 0x80704d94 +80704d98 Audio::GessoActor::StartSoundLimited +80704e90 0x80704e90 +80704f74 Audio::GessoActor::SetupBRASD +80704f78 Audio::GessoActor::UpdateBRASDFrame +80704f7c Audio::ItemActor::SetupBRASD +80704f80 Audio::ItemActor::UpdateBRASD +80704f84 Audio::GessoActor::StartNewSoundLimited +80705040 Audio::GessoActor::HoldSoundLimited +80705158 Audio::GessoActor::StopAllSound +8070516c Audio::GessoActor::StartSound +807051e0 Audio::GessoActor::StartSound(StartInfo) +80705260 Audio::GessoActor::HoldSound +807052d4 Audio::GessoActor::HoldSound(StartInfo) +80705354 Audio::GessoActor::Reset +80705394 Audio::GessoActor::GetPosition +8070539c Audio::GessoActor::UpdateUserParam +807053b0 Audio::GessoActor::UpdateUserParam2 +807053c8 Audio::GessoActor::DisableConditional +807053e4 Audio::GessoActor::UpdateUserPar +80705404 Audio::GessoActor::detail_UpdateAmbientArg +80705468 Audio::ItemActor::StopAllSound +807054cc Audio::ItemActor::StartSound +80705540 Audio::ItemActor::StartSound(StartInfo) +807055c0 Audio::ItemActor::HoldSound +80705634 Audio::ItemActor::HoldSound(StartInfo) +807056b4 Audio::ItemActor::Reset +807056f4 Audio::ItemActor::GetPosition +807056fc Audio::ItemActor::UpdateUserParam +80705710 Audio::ItemActor::UpdateUserParam2 +80705728 Audio::ItemActor::detail_UpdateAmbientArg +8070578c 0x8070578c +80705818 0x80705818 +807058a4 0x807058a4 +80705964 0x80705964 +807059f8 0x807059f8 +80705a8c 0x80705a8c +80705b48 0x80705b48 +80705c30 0x80705c30 +80705d18 0x80705d18 +80705e34 0x80705e34 +80705e9c 0x80705e9c +80705f04 0x80705f04 +80705fa8 0x80705fa8 +80706034 0x80706034 +807060c0 0x807060c0 +80706180 0x80706180 +80706214 0x80706214 +807062a8 0x807062a8 +80706364 Audio::Actor<2>::TryStartSound(u32) +807063f0 Audio::Actor<2>::TryStartSound(ul) +8070647c Audio::Actor<2>::TryStartSound(str) +8070653c Audio::Actor<2>::TryStopAndStartSound(u32) +807065d0 Audio::Actor<2>::TryStopAndStartSound(ul) +80706664 Audio::Actor<2>::TryStopAndStartSound(str) +80706720 Audio::Actor<2>::TryHoldSound(u32) +80706808 Audio::Actor<2>::TryHoldSound(ul) +807068f0 Audio::Actor<2>::TryHoldSound(str) +80706a0c Audio::Actor<2>::TryHoldSound(u32, handleId) +80706a74 Audio::Actor<2>::TryHoldSound(ul, handleId) +80706adc Audio::Actor<2>::TryHoldSound(str, handleId) +80706b80 Audio::Actor<2>::TryPrepareSound(u32) +80706c0c Audio::Actor<2>::TryPrepareSound(ul) +80706c98 Audio::Actor<2>::TryPrepareSound(str) +80706d58 Audio::Actor<2>::TryStopAndPrepareSound(u32) +80706dec Audio::Actor<2>::TryStopAndPrepareSound(ul) +80706e80 Audio::Actor<2>::TryStopAndPrepareSound(str) +80706f3c Audio::ItemActor::__dt +80706fc8 Audio::GessoActor::__dt +80707054 0x80707054 +8070705c 0x8070705c +80707064 0x80707064 +8070706c 0x8070706c +80707074 0x80707074 +8070707c 0x8070707c +80707084 0x80707084 +8070708c Audio::ItemActor::detail_UpdateAmbientArg(thunk) +80707094 Audio::ItemActor::__dt(thunk) +8070709c Audio::ItemActor::UpdateBRASDFrame(thunk) +807070a4 Audio::ItemActor::SetupBRASD(thunk) +807070ac Audio::ItemActor::HoldSound(thunk) +807070b4 Audio::ItemActor::HoldSound(StartInfo)(thunk) +807070bc Audio::ItemActor::StartSound(thunk) +807070c4 Audio::ItemActor::StartSound(StartInfo)(thunk) +807070cc Audio::ItemActor::StartNewSoundLimited(thunk) +807070d4 Audio::ItemActor::StopAllSound(thunk) +807070dc Audio::ItemActor::HoldSoundLimited(thunk) +807070e4 Audio::ItemActor::StartSoundLimited(thunk) +807070ec Audio::ItemActor::UpdateUserParam3(thunk) +807070f4 Audio::ItemActor::DisableConditional(thunk) +807070fc Audio::ItemActor::UpdateUserParam2(thunk) +80707104 Audio::ItemActor::UpdateUserParam(thunk) +8070710c Audio::ItemActor::Reset(thunk) +80707114 Audio::ItemActor::GetPosition(thunk) +8070711c Audio::ItemActor::Update(thunk) +80707124 Audio::ItemActor::Unlink(thunk) +8070712c Audio::ItemActor::Link(thunk) +80707134 0x80707134 +8070713c 0x8070713c +80707144 0x80707144 +8070714c Audio::GessoActor::detail_UpdateAmbientArg(thunk) +80707154 Audio::GessoActor::__dt(thunk) +8070715c Audio::GessoActor::UpdateBRASDFrame(thunk) +80707164 Audio::GessoActor::SetupBRASD(thunk) +8070716c Audio::GessoActor::HoldSound(thunk) +80707174 Audio::GessoActor::HoldSound(StartInfo)(thunk) +8070717c Audio::GessoActor::StartSound(thunk) +80707184 Audio::GessoActor::StartSound(StartInfo)(thunk) +8070718c Audio::GessoActor::StartNewSoundLimited(thunk) +80707194 Audio::GessoActor::StopAllSound(thunk) +8070719c Audio::GessoActor::HoldSoundLimited(thunk) +807071a4 Audio::GessoActor::StartSoundLimited(thunk) +807071ac Audio::GessoActor::UpdateUserParam3(thunk) +807071b4 Audio::GessoActor::DisableConditional(thunk) +807071bc Audio::GessoActor::UpdateUserParam2(thunk) +807071c4 Audio::GessoActor::UpdateUserParam(thunk) +807071cc Audio::GessoActor::Reset(thunk) +807071d4 Audio::GessoActor::GetPosition(thunk) +807071dc Audio::GessoActor::Update(thunk) +807071e4 Audio::GessoActor::Link(thunk) +807071ec 0x807071ec +8070722c Audio::KartActor::__ct +80707360 Audio::SimpleRaceActor::__dt +807073ec Audio::KartActor::__dt +807074b0 Audio::KartActor::SetActorType +807074d0 Audio::KartActor::DisableConditional +80707514 Audio::KartActor::UpdateUserParam3 +80707534 Audio::KartActor::Link +80707648 Audio::KartActor::Unlink +807076d0 Audio::KartActor::Update +80707e44 0x80707e44 +80707f7c Audio::KartActor::StopAllSound +8070803c Audio::KartActor::SetupSound +80708040 Audio::KartActor::HoldSoundLimited +807082d8 Audio::KartActor::StartSoundLimited +80708b20 Audio::KartActor::OutputSoundToWiimotes +80708b44 Audio::KartActor::ApplyKCLSoundTrigger +80708bac Audio::KartActor::PlayBoostSound +80708d48 Audio::KartActor::PlayRampSound +80708ee4 0x80708ee4 +80709150 Audio::KartActor::PlayCannonSound +807091ac Audio::KartActor::PlayHornSound +807092b4 Audio::KartActor::StartSound +80709338 Audio::KartActor::HoldSound +807093c4 Audio::KartActor::PlayMTSounds +80709610 Audio::KartActor::PlayRoadSound +80709824 0x80709824 +807098d8 0x807098d8 +80709980 Audio::KartActor::StartNewSoundLimited +80709b08 Audio::KartActor::OnObjectNormalCollision +80709ca4 Audio::KartActor::OnObjectWallCollision +80709df8 Audio::KartActor::PlayObjectCollisionSounds +8070afcc Audio::KartActor::PlayWallCollisionSound +8070b09c Audio::KartActor::PlayBushCollisionSound +8070b0fc 0x8070b0fc +8070b1a8 0x8070b1a8 +8070b1d0 0x8070b1d0 +8070b250 Audio::KartActor::UpdateLapSounds +8070b490 Audio::KartActor::UpdateKCLValues +8070b818 0x8070b818 +8070b8dc Audio::KartActor::SetRoadState +8070b8f0 Audio::KartActor::GetDriftAndSSMTRoadSoundId +8070b954 Audio::KartActor::GetDriftAndSSMTRoadSoundId_switch +8070b958 Audio::KartActor::GetDriftAndSSMTRoadSoundId_caseD_16 +8070b96c Audio::KartActor::GetDriftAndSSMTRoadSoundId_caseD_15 +8070b978 Audio::KartActor::GetDriftAndSSMTRoadSoundId_caseD_13 +8070b98c Audio::KartActor::GetDriftAndSSMTRoadSoundId_caseD_b +8070b9a0 Audio::KartActor::GetDriftAndSSMTRoadSoundId_caseD_5 +8070b9b4 Audio::KartActor::GetDriftAndSSMTRoadSoundId_caseD_4 +8070b9c8 Audio::KartActor::GetDriftAndSSMTRoadSoundId_caseD_3 +8070b9dc Audio::KartActor::GetDriftAndSSMTRoadSoundId_caseD_2 +8070b9f0 Audio::KartActor::GetDriftAndSSMTRoadSoundId_caseD_1 +8070ba04 Audio::KartActor::GetDriftAndSSMTRoadSoundId_caseD_0 +8070ba14 Audio::KartActor::GetDriftAndSSMTRoadSoundId_caseD_17 +8070ba28 Audio::KartActor::GetDriftAndSSMTRoadSoundId_caseD_1d +8070ba38 Audio::KartActor::GetDriftAndSSMTRoadSoundId_caseD_10 +8070ba40 Audio::KartActor::GetLandingSoundId +8070ba80 Audio::KartActor::GetLandingSoundId_switch +8070ba84 Audio::KartActor::GetLandingSoundId_caseD_16 +8070ba98 Audio::KartActor::GetLandingSoundId_caseD_15 +8070baa4 Audio::KartActor::GetLandingSoundId_caseD_13 +8070bab8 Audio::KartActor::GetLandingSoundId_caseD_b +8070bacc Audio::KartActor::GetLandingSoundId_caseD_5 +8070bae0 Audio::KartActor::GetLandingSoundId_caseD_4 +8070baf4 Audio::KartActor::GetLandingSoundId_caseD_3 +8070bb08 Audio::KartActor::GetLandingSoundId_caseD_2 +8070bb1c Audio::KartActor::GetLandingSoundId_caseD_1 +8070bb30 Audio::KartActor::GetLandingSoundId_caseD_0 +8070bb40 Audio::KartActor::GetLandingSoundId_caseD_17 +8070bb54 Audio::KartActor::GetLandingSoundId_caseD_1d +8070bb64 Audio::KartActor::GetLandingSoundId_caseD_10 +8070bb6c Audio::KartActor::GetRoadSoundId +8070bbd4 Audio::KartActor::GetRoadSoundId_caseD_16 +8070bbe8 Audio::KartActor::GetRoadSoundId_caseD_15 +8070bbf4 Audio::KartActor::GetRoadSoundId_caseD_13 +8070bc08 Audio::KartActor::GetRoadSoundId_caseD_b +8070bc1c Audio::KartActor::GetRoadSoundId_caseD_5 +8070bc30 Audio::KartActor::GetRoadSoundId_caseD_4 +8070bc44 Audio::KartActor::GetRoadSoundId_caseD_3 +8070bc58 Audio::KartActor::GetRoadSoundId_caseD_2 +8070bc6c Audio::KartActor::GetRoadSoundId_caseD_1 +8070bc80 Audio::KartActor::GetRoadSoundId_caseD_0 +8070bc90 Audio::KartActor::GetRoadSoundId_caseD_17 +8070bca4 Audio::KartActor::GetRoadSoundId_caseD_1d +8070bcb4 Audio::KartActor::GetRoadSoundId_caseD_10 +8070bcbc Audio::KartActor::GetBrakingSoundId +8070bd24 Audio::KartActor::GetBrakingSoundId_caseD_16 +8070bd38 Audio::KartActor::GetBrakingSoundId_caseD_15 +8070bd44 Audio::KartActor::GetBrakingSoundId_caseD_13 +8070bd58 Audio::KartActor::GetBrakingSoundId_caseD_b +8070bd6c Audio::KartActor::GetBrakingSoundId_caseD_5 +8070bd80 Audio::KartActor::GetBrakingSoundId_caseD_4 +8070bd94 Audio::KartActor::GetBrakingSoundId_caseD_3 +8070bda8 Audio::KartActor::GetBrakingSoundId_caseD_2 +8070bdbc Audio::KartActor::GetBrakingSoundId_caseD_1 +8070bdd0 Audio::KartActor::GetBrakingSoundId_caseD_0 +8070bde0 Audio::KartActor::GetBrakingSoundId_caseD_17 +8070bdf4 Audio::KartActor::GetBrakingSoundId_caseD_1d +8070be04 Audio::KartActor::GetBrakingSoundId_caseD_10 +8070be0c Audio::KartActor::SetNonLocalVolumeAndPriority +8070c468 0x8070c468 +8070c564 Audio::KartActor::PlayEngineSound +8070c758 Audio::KartActor::SetPitch +8070c868 0x8070c868 +8070c8b0 0x8070c8b0 +8070c8e4 Audio::KartActor::PlayOtherKartCollisionSound +8070c988 0x8070c988 +8070ca08 Audio::SimpleRaceActor::SetupBRASD +8070ca0c Audio::SimpleRaceActor::UpdateBRASDFrame +8070ca10 0x8070ca10 +8070ca14 Audio::SimpleRaceActor::__dt(thunk) +8070ca1c Audio::KartActor::__dt(thunk) +8070ca24 Audio::SimpleRaceActor::UpdateBRASDFrame(thunk) +8070ca2c Audio::SimpleRaceActor::SetupBRASD(thunk) +8070ca34 Audio::KartActor::HoldSound(thunk) +8070ca3c Audio::KartActor::StartSound(thunk) +8070ca44 Audio::KartActor::StartNewSoundLimited(thunk) +8070ca4c Audio::KartActor::StopAllSound(thunk) +8070ca54 Audio::KartActor::HoldSoundLimited(thunk) +8070ca5c Audio::KartActor::StartSoundLimited(thunk) +8070ca64 Audio::KartActor::UpdateUserParam3(thunk) +8070ca6c Audio::KartActor::DisableConditional(thunk) +8070ca74 Audio::KartActor::Update(thunk) +8070ca7c Audio::KartActor::Unlink(thunk) +8070ca84 Audio::KartActor::Link(thunk) +8070ca8c Audio::ObjectActor::__ct +8070cb38 0x8070cb38 +8070cb78 Audio::ObjectActor::__dt +8070cc08 Audio::ObjectActor::Update +8070cd30 Audio::ObjectActor::Unlink +8070cd38 Audio::ObjectActor::Link +8070cda8 0x8070cda8 +8070ce00 Audio::ObjectActor::StartNewSoundLimited +8070d978 Audio::ObjectActor::HoldSoundLimited +8070dc8c Audio::ObjectActor::StartSoundLimited +8070de48 0x8070de48 +8070e188 Audio::ObjectActor::DisableConditional +8070e5f0 Audio::ObjectActor::UpdateUserParam3 +8070e674 Audio::ObjectActor::SetupSound +8070e72c 0x8070e72c_ +8070e770 Audio::ObjectActor::__dt(thunk) +8070e778 Audio::ObjectActor::StartNew(thunk) +8070e780 Audio::ObjectActor::HoldSoundLimited(thunk) +8070e788 Audio::ObjectActor::StartSoundLimited(thunk) +8070e790 Audio::ObjectActor::UpdateUserParam3(thunk) +8070e798 Audio::ObjectActor::DisableConditional(thunk) +8070e7a0 Audio::ObjectActor::Update(thunk) +8070e7a8 Audio::ObjectActor::Unlink(thunk) +8070e7b0 Audio::ObjectActor::Link(thunk) +8070e7b8 0x8070e7b8 +8070e874 0x8070e874 +8070e8b4 0x8070e8b4 +8070e940 0x8070e940 +8070ea24 0x8070ea24 +8070eb30 0x8070eb30 +8070eb38 0x8070eb38 +8070ec90 0x8070ec90 +8070ecb4 0x8070ec90_caseD_a +8070eccc 0x8070ec90_switch +8070ecd0 0x8070ec90_caseD_5 +8070ed0c 0x8070ec90_caseD_6 +8070ed1c 0x8070ec90_caseD_8 +8070ed2c 0x8070ec90_caseD_9 +8070ed5c 0x8070ec90_caseD_a +8070ed98 0x8070ec90_caseD_b +8070edb8 0x8070ec90_caseD_c +8070edc8 0x8070ec90_caseD_e +8070ede4 0x8070ec90_caseD_7 +8070ede8 0x8070ede8 +8070ee78 0x8070ee78 +8070ee7c 0x8070ee7c +8070ee80 0x8070ee80 +8070ee88 0x8070ee88 +8070ee90 0x8070ee90 +8070ee98 0x8070ee98 +8070eea0 0x8070eea0 +8070eea8 0x8070eea8 +8070eeb0 0x8070eeb0 +8070eeb8 0x8070eeb8 +8070eec0 0x8070eec0 +8070eec8 Audio::PlayersVolumeMgr::Disposer__dt +8070eff4 Audio::PlayersVolumeMgr::CreateInstance +8070f0e8 Audio::PlayersVolumeMgr::DestroyInstance +8070f19c Audio::PlayersVolumeMgr::__dt +8070f240 0x8070f240 +8070f308 0x8070f308 +8070f3b8 0x8070f3b8 +8070f3d0 0x8070f3d0 +8070f3e8 Audio::PlayersVolumeMgr::Calc +8070f470 Audio::PlayersVolumeMgr::StopAllPlayersButTrackUI +8070f4d0 Audio::PlayersVolumeMgr::MuteAllButTrackUI +8070f554 Audio::PlayersVolumeMgr::ResetAllButUI +8070f59c Audio::PlayersVolumeMgr::Reset +8070f648 Audio::PlayersVolumeMgr::PauseAllButTrackUI +8070f6b8 Audio::PlayersVolumeMgr::SetVolume +8070f7a4 0x8070f7a4 +8070f85c 0x8070f85c +8070f87c 0x8070f87c +8070f8b8 Audio::ItemAlterationMgr::Disposer__dt +8070fa08 Audio::ItemAlterationMgr::CreateInstance +8070fa7c 0x8070fa7c +8070fabc Audio::ItemAlterationMgr::DestroyInstance +8070fbc0 Audio::ItemAlterationMgr::__dt +8070fc98 Audio::ItemAlterationMgr::Init +8070fcf0 Audio::ItemAlterationMgr::IsDemo +8070fd10 Audio::ItemAlterationMgr::Update +8070feb0 Audio::ItemAlterationMgr::ResetItemState +8070fec8 Audio::ItemAlterationMgr::UpdateStatus +8071007c Audio::ItemAlterationMgr::SetAudioAlteration +807101f4 Audio::ItemAlterationMgr::GetCourseSoundId +80710214 Audio::ItemAlterationMgr::CalcStatus2Values +807102b0 Audio::ItemAlterationMgr::PlayStarMusic +80710308 Audio::ItemAlterationMgr::PlayMegaMusic +80710340 Audio::RaceMgr::Disposer__dt +807104d0 Audio::RaceMgr::CreateInstance +80710520 Audio::RaceMgr::DestroyInstance +80710664 Audio::RaceMgr::ActorHolder::Compare +80710688 Audio::RaceMgr::__ct +807108a8 0x807108a8 +807108e8 Audio::RaceMgr::__dt +80710a00 Audio::RaceMgr::Init +80710bac Audio::RaceMgr::Shutdown +80710ca0 Audio::RaceMgr::Calc +807110a0 0x807110a0 +80711198 Audio::RaceMgr::UpdatePlayerMatrix +807112fc 0x807112fc +80711390 0x80711390 +80711418 Audio::RaceMgr::StartRace +807114a4 Audio::RaceMgr::UpdateActors +80711514 Audio::RaceMgr::UpdateLocalPlayerParams +80711668 Audio::RaceMgr::IsAPlayerSquishedOrSmall +8071172c Audio::RaceMgr::IsAPlayerInStar +807117a0 Audio::RaceMgr::IsAPlayerInMega +80711814 0x80711814 +8071185c 0x8071185c +80711960 Audio::RaceMgr::SetNonTrackVolumes +80711a0c Audio::RaceMgr::SetEngineVolume +80711a14 Audio::RaceMgr::SetNonTrackAndEngineVolume +80711ac4 Audio::RaceMgr::SetRaceState +807121c0 0x807121c0 +80712234 0x807121c0_switch +80712238 0x807121c0_caseD_0 +8071224c 0x807121c0_caseD_1 +80712260 0x807121c0_caseD_2 +80712280 0x807121c0_caseD_3 +80712380 0x807121c0_caseD_4 +807123a0 0x807121c0_caseD_5 +807123bc 0x807121c0_caseD_6 +8071244c 0x8071244c +807124a8 0x807124a8 +80712540 0x80712540 +807125d4 Audio::RaceMgr::CheckRaceState +80712adc 0x80712adc +80712b58 Audio::RaceMgr::SortAndToggleActors +80713754 Audio::RaceMgr::SetKartSound +807137a8 0x807137a8 +807137ec 0x807137ec +807137f4 0x807137f4 +80713890 0x80713890 +807138d0 0x807138d0 +807139fc 0x807139fc +80713a64 0x80713a64 +80713b18 0x80713b18 +80713bbc 0x80713bbc +80713bc4 0x80713bc4 +80713c80 Audio::RSARPlayer::OutputSoundToWiimotes +80713d64 0x80713d64 +80713dcc Audio::RSARPlayer::IsDemo +80713de8 0x80713de8 +80713e10 0x80713e10 +80713e90 Audio::RSARPlayer::CreateInstance +807140b4 Audio::RSARPlayer::DestroyInstance +8071412c Audio::RSARPlayer::Close +80714184 Audio::RSARPlayer::OnLoad +80714190 Audio::RSARPlayer::Stop +807141ec Audio::RSARPlayer::HasFinishedLoadingGroups +807141f8 Audio::RSARPlayer::Calc +80714370 Audio::RSARPlayer::OnNewPageLayer +80714374 Audio::MenuRSARPlayer::OnNewPageLayer +80714598 0x80714598 +80714690 0x80714690 +8071469c 0x8071469c +807146a8 Audio::RSARPlayer::PlaySoundById +8071476c Audio::RSARPlayer::HoldSoundById +807147d0 0x807147d0 +8071493c 0x8071493c +8071497c Audio::RSARPlayer::PlaySound +807155e4 Audio::RSARPlayer::StopSoundById +80715624 Audio::RSARPlayer::HoldSound +80715648 0x80715648 +807156f4 0x807156f4 +807158e0 0x807158e0 +80715988 0x80715988 +80715a4c 0x80715a4c +80715a70 Audio::RSARPlayer::PrepareReverb +80715a74 Audio::RSARPlayer::Reset +80715a98 Audio::MenuRSARPlayer::__ct +80715acc Audio::MenuRSARPlayer::Prepare +80715b7c Audio::MenuRSARPlayer::Close +80715bd4 Audio::MenuRSARPlayer::OnLoad +80715c3c Audio::MenuRSARPlayer::Stop +80715c98 Audio::MenuRSARPlayer::PrepareReverb +80715da4 Audio::RaceRSARPlayer::__ct +80715e24 Audio::RaceRSARPlayer::OnLoad +80715e64 Audio::RaceRSARPlayer::Stop +80715ecc Audio::RaceRSARPlayer::Pause +80715f60 Audio::RaceRSARPlayer::Resume +80715ff0 Audio::RaceRSARPlayer::PlayEndRaceMenuButtonClickSound +80716008 Audio::RaceRSARPlayer::SetFullVolume +80716018 Audio::RaceRSARPlayer::HalveVolume +80716028 Audio::RaceRSARPlayer::Prepare +80716170 Audio::RaceRSARPlayer::LoadCharacterGroups +807162b4 Audio::RaceRSARPlayer::PrepareReverb +80716404 Audio::RaceRSARPlayer::Close +8071646c Audio::RaceRSARPlayer::Calc +80716608 Audio::RaceRSARPlayer::PlaySound +8071677c Audio::RaceRSARPlayer::HoldSound +807167b8 Audio::TestRSARPlayer::__ct +807167ec Audio::TestRSARPlayer::Prepare +8071681c Audio::TestRSARPlayer::Close +80716874 Audio::TestRSARPlayer::OnDeactivate +80716880 Audio::GlobeRSARPlayer::__ct +807168bc Audio::RSARPlayer::__dt +80716940 Audio::GlobeRSARPlayer::OnDeactivate +8071698c Audio::GlobeRSARPlayer::Prepare +80716a20 Audio::GlobeRSARPlayer::Calc +80716b98 Audio::GlobeRSARPlayer::PrepareReverb +80716ca4 Audio::GlobeRSARPlayer::Close +80716cfc 0x80716cfc +80716d68 Audio::Manager::OpenBRSAR +80716d94 Audio::Manager::Disposer__dt +80716e2c Audio::Manager::CreateInstance +80716f10 Audio::Manager::DestroyInstance +80716f44 Audio::Manager::__ct +80717004 Audio::Manager::__dt +80717104 Audio::Manager::DestroyAudioInstances +80717150 Audio::Manager::Init +8071724c Audio::Manager::InitSelf +80717424 0x80717424 +807174a0 0x807174a0 +807174e0 0x807174e0 +80717520 Audio::Manager::CreateAudioInstances +807175a0 Audio::Manager::LoadControlsGroup +8071763c Audio::Manager::Calc +80717744 Audio::Manager::StartSound +8071778c Audio::Manager::HoldSound +807177d4 Audio::Manager::EnterHBM +8071784c Audio::Manager::ExitHBM +807178d4 Audio::Manager::AudioUtilityCallback +807178ec Audio::Manager::Lock +807178f8 Audio::Manager::Unlock +80717904 Audio::Manager::PauseOnDVDBusy +807179b4 Audio::Manager::SetupMemoryArchive +807179c4 Audio::Manager::HoldSound +807179f8 Audio::Manager::HoldSound(str) +80717a68 Audio::Manager::PrepareSound +80717a9c Audio::Manager::PrepareSound(str) +80717b0c Audio::Manager::StartSound +80717b40 Audio::Manager::StartSound(str) +80717bb0 0x80717bb0 +80717bd0 Audio::Manager::LoadState(thunk) +80717bd8 Audio::Manager::HoldSound(thunk) +80717be0 Audio::Manager::StartSound(thunk) +80717be8 Audio::Manager::Calc(thunk) +80717bf0 Audio::Manager::LoadGroup(thunk) +80717bf8 Audio::Manager::LoadGroup(thunk) +80717c00 Audio::Manager::LoadGroup(thunk) +80717c08 Audio::Manager::CloseArchive(thunk) +80717c10 Audio::Manager::SetupMemoryArchive(thunk) +80717c18 Audio::Manager::SetupMemoryArchive(thunk) +80717c20 Audio::Manager::OpenMemoryArchive(thunk) +80717c28 Audio::Manager::OpenNANDArchive(thunk) +80717c30 Audio::Manager::OpenDVDArchive(thunk) +80717c38 Audio::Manager::OpenArchive(thunk) +80717c40 Audio::Manager::__dt(thunk) +80717c48 Audio::Manager::WriteSeqVariable +80717cc8 0x80717cc8 +80717d08 Audio::Track::SetValue +80717d2c Audio::Track::Calc +80717d70 Audio::PitchModuler::_ct +80717d84 Audio::PitchModuler::Update +80717e00 Audio::PitchModuler::GetNextLimit +80717e34 Audio::EchoMgr::Disposer__dt +80717f60 Audio::EchoMgr::CreateInstance +80718014 Audio::EchoMgr::DestroyInstance +807180c8 Audio::EchoMgr::__ct +80718150 Audio::EchoMgr::__dt +807181f4 Audio::EchoMgr::Init +80718218 Audio::EchoMgr::Shutdown +8071821c Audio::EchoMgr::Update +807182b8 Audio::EchoMgr::SetVolume +807182d0 Audio::EchoMgr::SetEcho +807182e8 Audio::SoundTriggerMgr::Disposer__dt +80718414 Audio::SoundTriggerMgr::CreateInstance +80718498 Audio::SoundTriggerMgr::DestroyInstance +8071854c Audio::SoundTriggerMgr::__ct +807185a4 Audio::SoundTriggerMgr::__dt +80718648 Audio::SoundTriggerMgr::Init +807186a0 Audio::SoundTriggerMgr::Init_switch +807186a4 Audio::SoundTriggerMgr::Init_caseD_3a +80718720 Audio::SoundTriggerMgr::Init_caseD_1 +8071879c Audio::SoundTriggerMgr::Init_caseD_4 +80718818 Audio::SoundTriggerMgr::Init_caseD_0 +80718894 Audio::SoundTriggerMgr::Init_caseD_5 +80718910 Audio::SoundTriggerMgr::Init_caseD_6 +8071898c Audio::SoundTriggerMgr::Init_caseD_3 +80718a08 Audio::SoundTriggerMgr::Init_caseD_c +80718a84 Audio::SoundTriggerMgr::Init_caseD_18 +80718b00 Audio::SoundTriggerMgr::Init_caseD_1a +80718b7c Audio::SoundTriggerMgr::Init_caseD_1d +80718bf8 Audio::SoundTriggerMgr::Init_caseD_10 +80718c74 Audio::SoundTriggerMgr::Init_caseD_13 +80718cf0 Audio::SoundTriggerMgr::Init_caseD_14 +80718d6c Audio::SoundTriggerMgr::Init_caseD_17 +80718de8 Audio::SoundTriggerMgr::Init_caseD_15 +80718e64 Audio::SoundTriggerMgr::Init_caseD_20 +80718edc Audio::SoundTriggerMgr::Init_caseD_2a +80718ef8 Audio::SoundTriggerMgr::IsAPlayerOnVariant +80718f3c Audio::SoundTriggerMgr::Shutdown +80718f48 Audio::SoundTriggerMgr::Update +80718f84 Audio::SoundTriggerMgr::IsPlayerAheadOfOtherLocal +80719044 Audio::SoundTriggerMgr::ApplyTrigger +8071919c Audio::SoundTriggerMgr::ApplyTrigger_switch +807191a0 Audio::SoundTriggerMgr::ApplyTrigger_caseD_3a +807192f4 Audio::SoundTriggerMgr::ApplyTrigger_caseD_2 +80719598 Audio::SoundTriggerMgr::ApplyTrigger_caseD_4 +807195b8 Audio::SoundTriggerMgr::ApplyTrigger_switch +807195bc Audio::SoundTriggerMgr::ApplyTrigger_caseD_0 +80719640 Audio::SoundTriggerMgr::ApplyTrigger_caseD_1 +807196ec Audio::SoundTriggerMgr::ApplyTrigger_caseD_2 +80719770 Audio::SoundTriggerMgr::ApplyTrigger_caseD_3 +80719814 Audio::SoundTriggerMgr::ApplyTrigger_caseD_4 +807198c0 Audio::SoundTriggerMgr::ApplyTrigger_caseD_5 +807199e0 Audio::SoundTriggerMgr::ApplyTrigger_caseD_6 +80719a4c Audio::SoundTriggerMgr::ApplyTrigger_caseD_7 +80719b0c Audio::SoundTriggerMgr::ApplyTrigger_caseD_0 +80719b7c Audio::SoundTriggerMgr::ApplyTrigger_caseD_5 +80719db4 Audio::SoundTriggerMgr::ApplyTrigger_caseD_6 +80719f08 Audio::SoundTriggerMgr::ApplyTrigger_caseD_7 +8071a004 Audio::SoundTriggerMgr::ApplyTrigger_caseD_f +8071a274 Audio::SoundTriggerMgr::ApplyTrigger_caseD_9 +8071a2e4 Audio::SoundTriggerMgr::ApplyTrigger_caseD_e +8071a4a8 Audio::SoundTriggerMgr::ApplyTrigger_caseD_3 +8071a53c Audio::SoundTriggerMgr::ApplyTrigger_caseD_a +8071a748 Audio::SoundTriggerMgr::ApplyTrigger_caseD_b +8071a80c Audio::SoundTriggerMgr::ApplyTrigger_caseD_c +8071a82c Audio::SoundTriggerMgr::ApplyTrigger_switch2 +8071a830 Audio::SoundTriggerMgr::ApplyTrigger_case2D_0 +8071a88c Audio::SoundTriggerMgr::ApplyTrigger_case2D_1 +8071a8fc Audio::SoundTriggerMgr::ApplyTrigger_case2D_2 +8071a94c Audio::SoundTriggerMgr::ApplyTrigger_case2D_3 +8071a99c Audio::SoundTriggerMgr::ApplyTrigger_case2D_4 +8071a9ec Audio::SoundTriggerMgr::ApplyTrigger_case2D_5 +8071aa08 Audio::SoundTriggerMgr::ApplyTrigger_case2D_6 +8071aa24 Audio::SoundTriggerMgr::ApplyTrigger_case2D_7 +8071aa5c Audio::SoundTriggerMgr::ApplyTrigger_case2D_d +8071ac48 Audio::SoundTriggerMgr::ApplyTrigger_case2D_1e +8071acb4 Audio::SoundTriggerMgr::ApplyTrigger_case2D_1f +8071ace4 Audio::SoundTriggerMgr::ApplyTrigger_case2D_1b +8071ae8c Audio::SoundTriggerMgr::ApplyTrigger_case2D_1a +8071af84 Audio::SoundTriggerMgr::ApplyTrigger_case2D_1c +8071b16c Audio::SoundTriggerMgr::ApplyTrigger_case2D_1d +8071b290 Audio::SoundTriggerMgr::ApplyTrigger_case2D_12 +8071b2fc Audio::SoundTriggerMgr::ApplyTrigger_case2D_11 +8071b36c Audio::SoundTriggerMgr::ApplyTrigger_case2D_13 +8071b47c Audio::SoundTriggerMgr::ApplyTrigger_case2D_14 +8071b4ac Audio::SoundTriggerMgr::ApplyTrigger_case2D_16 +8071b4dc Audio::SoundTriggerMgr::ApplyTrigger_case2D_17 +8071b7bc Audio::SoundTriggerMgr::ApplyTrigger_case2D_23 +8071b7ec Audio::SoundTriggerMgr::ApplyTrigger_case2D_1 +8071b808 SetTRKConnected +8071b814 0x8071b814 +8071b860 0x8071b860 +8071b864 0x8071b864 +8071b868 0x8071b868 +8071b86c MotionControllerSub::__ct +8071bd1c MotionControllerSub::__dt +8071be1c 0x8071be1c +8071c0a4 0x8071c0a4 +8071c144 0x8071c144 +8071c164 0x8071c164 +8071c188 0x8071c188 +8071c1f0 0x8071c1f0 +8071c224 0x8071c224 +8071c2a0 0x8071c2a0 +8071c2ac 0x8071c2ac +8071c2c8 0x8071c2c8 +8071c2e4 0x8071c2e4 +8071c330 0x8071c330 +8071c370 0x8071c370 +8071c39c 0x8071c39c +8071c3d0 0x8071c3d0 +8071c3fc 0x8071c3fc +8071c524 0x8071c524 +8071c61c 0x8071c61c +8071c6cc 0x8071c6cc +8071c724 0x8071c724 +8071c784 0x8071c784 +8071c798 0x8071c798 +8071c7f0 0x8071c7f0 +8071c804 0x8071c804 +8071c818 0x8071c818 +8071c8a8 0x8071c8a8 +8071c900 0x8071c900 +8071c928 0x8071c928 +8071c968 0x8071c968 +8071c978 0x8071c978 +8071cbd8 0x8071cbd8 +8071cdac 0x8071cdac +8071cf20 0x8071cf20 +8071cfd0 0x8071cfd0 +8071d034 0x8071d034 +8071d0a8 0x8071d0a8 +8071d260 0x8071d260 +8071d2a0 0x8071d2a0 +8071d43c 0x8071d43c +8071d5a0 0x8071d5a0 +8071d62c 0x8071d62c +8071d6a0 0x8071d6a0 +8071d79c 0x8071d79c +8071d838 0x8071d838 +8071d860 0x8071d860 +8071d8a0 0x8071d8a0 +8071d95c 0x8071d95c +8071d968 0x8071d968 +8071d9d4 0x8071d9d4 +8071da40 0x8071da40 +8071dadc 0x8071dadc +8071db08 0x8071db08 +8071db3c 0x8071db3c +8071dba0 0x8071dba0 +8071dc60 0x8071dc60 +8071dc94 0x8071dc94 +8071e07c 0x8071e07c +8071e1bc 0x8071e1bc +8071e1d8 Lakitu::Manager::Disposer__dt +8071e270 Lakitu::Manager::CreateInstance +8071e2fc Lakitu::Manager::DestroyInstance +8071e330 Lakitu::Manager::__ct +8071e390 Lakitu::Manager::__dt +8071e480 Lakitu::Manager::CreatePlayers +8071e638 Lakitu::Manager::Init +8071e6c0 Lakitu::Manager::Update +8071e734 Lakitu::Manager::DoEnableRespawnAction +8071e7ac Lakitu::Manager::ToggleVisible +8071e82c 0x8071e82c +8071e8a4 Lakitu::Manager::StartCountdownAnm +8071e90c 0x8071e90c +8071e984 Lakitu::Manager::GetCountdownStartFrame +8071e9b4 Lakitu::Movement::__ct +8071ea80 0x8071ea80 +8071eb6c Lakitu::Movement::Reset +8071ecc4 0x8071ecc4 +8071ee20 0x8071ee20 +8071efa8 0x8071efa8 +8071efd8 Lakitu::Movement::SetBasePosition +8071eff4 0x8071eff4 +8071f044 AI::CalcLinearTransfo +8071f0cc 0x8071f0cc +8071f204 0x8071f204 +8071f31c 0x8071f31c +8071f404 Lakitu::Movement::Update +8071fa2c 0x8071fa2c +8071fc48 0x8071fc48 +8071fc5c 0x8071fc5c +8071fc80 0x8071fc80 +80720024 Lakitu::Movement::UpdateRotation +807201b0 0x807201b0 +807202bc Lakitu::Movement::UpdateExtMtx +807204ec 0x807204ec +80720500 0x80720500 +807205c8 0x807205c8 +80720664 0x80720664 +8072072c 0x8072072c +8072074c 0x8072074c +807208a0 0x807208a0 +80720a20 Lakitu::Movement::UpdateScale +80720c0c 0x80720c0c +80720d30 0x80720d30 +80720e8c 0x80720e8c +80720f44 Lakitu::PlayerBase::__ct +80720f8c Lakitu::PlayerBase::__dt +8072101c Lakitu::PlayerBase::Init +80721024 Lakitu::PlayerBase::Update +80721090 Lakitu::PlayerBase::ToggleModelsScreenVisibility +80721128 Lakitu::PlayerBase::SetModelsVisibility +8072125c 0x8072125c +80721368 0x80721368 +8072143c 0x8072143c +807214a8 0x807214a8 +80721514 Lakitu::Player::__ct +80721c14 AI::Base::__dt +80721c54 AI::Actions::__dt +80721c94 0x80721c94 +80721cec Lakitu::Action::__dt +80721d2c Lakitu::Player::__dt +80721e64 0x80721e64 +80721ec0 Lakitu::Player::CreateEnableActions +80721f88 0x80721f88 +80721ffc 0x80721ffc +80722054 0x80722054 +80722100 Lakitu::Player::Init +807221c4 Lakitu::Player::Update +8072239c Lakitu::Player::GetPlayerIdx +807223a4 Lakitu::Player::StartRespawnAction +807223bc Lakitu::Player::StartCountdownAction +8072240c Lakitu::Player::GetCountdownArmSwipeDuration +80722418 0x80722418 +80722504 Lakitu::Player::LoadGraphics +80722e54 0x80722e54 +80722ea0 0x80722ea0 +80722ed8 0x80722ed8 +80722f6c 0x80722f6c +807230d4 Lakitu::Player::MapToWorldSpace +807232e4 0x807232e4 +807233c0 0x807233c0 +80723458 Lakitu::Player::OnStartIdle +807234a4 Lakitu::Player::UpdateIdle +80723598 Lakitu::Player::OnStartCountdown +8072370c 0x8072370c +807237ac Lakitu::Player::UpdateCountdown +80723af8 0x80723af8 +80723b9c 0x80723b9c +80723c20 Lakitu::Player::OnEndCountdown +80723c7c Lakitu::Player::OnStartLap +80723ec0 Lakitu::Player::UpdateLap +8072429c 0x8072429c +807242f4 Lakitu::Player::OnStartRespawn +80724430 Lakitu::Player::UpdateRespawn +80724778 0x80724778 +80724794 Lakitu::Player::OnStartBackwards +80724880 Lakitu::Player::UpdateBackwards +80724f70 Lakitu::Player::OnEndBackwards +80725060 Lakitu::Player::OnStartEnding +80725170 Lakitu::Player::UpdateEnding +80725424 Lakitu::Player::OnEndEnding +807254a0 Lakitu::Player::OnStartUnknown +807254c0 Lakitu::Player::UpdateUnknown +807255a8 AI::Base::OnStartDefaultAction +807255ac AI::Base::UpdateDefaultAction +807255b0 Lakitu::Action::OnStart +807255f8 Lakitu::Action::Update +80725628 Lakitu::Action::OnEnd +80725670 0x80725670 +807259e4 Lakitu::Player::__dt(thunk) +807259ec Lakitu::EnableLakituAction::__ct +80725a0c Lakitu::EnableLakituAction::__dt +80725a4c Lakitu::EnableLakituAction::ResetState +80725a5c Lakitu::EnableLakituAction::HasActionHappened +80725a78 Lakitu::EnableCountdownAction::__ct +80725a98 Lakitu::EnableCountdownAction::__dt +80725ad8 Lakitu::EnableCountdownAction::ResetState +80725ae8 Lakitu::EnableCountdownAction::EnableAction +80725b48 Lakitu::EnableDisplayLapAction::__ct +80725b68 Lakitu::EnableDisplayLapAction::__dt +80725ba8 Lakitu::EnableDisplayLapAction::ResetState +80725bd8 Lakitu::EnableDisplayLapAction::EnableAction +80725c1c Lakitu::EnableBackwardsAction::__ct +80725c3c Lakitu::EnableBackwardsAction::__dt +80725c7c Lakitu::EnableBackwardsAction::ResetState +80725c98 Lakitu::EnableBackwardsAction::EnableAction +80725dd8 Lakitu::EnableRespawnAction::__ct +80725df8 Lakitu::EnableRespawnAction::__dt +80725e38 Lakitu::EnableRespawnAction::ResetState +80725e48 Lakitu::EnableRespawnAction::EnableAction +80725e4c Lakitu::EnableEndingAction::__ct +80725e6c Lakitu::EnableEndingAction::__dt +80725eac Lakitu::EnableEndingAction::ResetState +80725ebc Lakitu::EnableEndingAction::EnableAction +80725f0c 0x80725f0c +80725f2c 0x80725f2c +80725f6c cGameSFX_Init +80725f7c 0x80725f7c +80725fdc KartAIController::__ct +80726088 KartAIController::__dt +80726110 KartAIController::Init +807261b8 KartAIController::Update +80726204 KartAIController::GetStick +8072624c KartAIController::IsCPU +80726290 KartAIController::IsRealLocal +807262d0 0x807262d0 +80726338 thunk_0x80590664 +8072633c KartAIController::IsGhost +80726380 KartAIController::HaveSameTeam +807263a8 0x807263a8 +807263b0 0x807263b0 +807263c4 0x807263c4 +807263cc 0x807263cc +807264e8 KartAIController::IsOnTheGround +807264fc KartAIController::IsInTheAir +80726510 0x80726510 +80726524 KartAIController::IsManuallyDrifting +80726538 KartAIController::IsHopping +8072654c KartAIController::IsAutomaticDrifting +80726560 KartAIController::IsInABill +80726574 0x80726574 +80726a18 0x80726a18 +80726a58 0x80726a58 +80726a98 0x80726a98 +80726ad8 0x80726ad8 +80726b94 0x80726b94 +80726be8 0x80726be8 +80726bf0 0x80726bf0 +80726cb0 0x80726cb0 +80726d14 0x80726d14 +80726e04 0x80726e04 +80726e5c 0x80726e5c +80726e78 0x80726e78 +80726ff8 0x80726ff8 +80727084 0x80727084 +8072709c 0x8072709c +807272c4 0x807272c4 +80727374 0x80727374 +807273e4 0x807273e4 +807273f0 0x807273f0 +80727400 0x80727400 +807274d0 0x807274d0 +807274dc 0x807274dc +807274ec 0x807274ec +80727520 0x80727520 +807275b4 0x807275b4 +807276bc 0x807276bc +807276c8 0x807276c8 +80727710 0x80727710 +80727740 0x80727740 +80727788 0x80727788 +80727aec 0x80727aec +80727b2c 0x80727b2c +80727bd8 0x80727bd8 +80727ca4 0x80727ca4 +80727cf8 0x80727cf8 +80727d40 0x80727d40 +80727d50 0x80727d50 +80727da0 0x80727da0 +80727e84 0x80727e84 +80727ef4 0x80727ef4 +80727f60 0x80727f60 +80728088 0x80728088 +8072820c 0x8072820c +80728210 0x80728210 +80728214 AI::CPUDriving::GetCalculator +8072821c 0x8072821c +80728220 0x80728220 +80728294 0x80728294 +80728298 0x80728298 +8072829c 0x8072829c +8072835c 0x8072835c +807284d0 0x807284d0 +807285c8 0x807285c8 +807285cc 0x807285cc +80728614 0x80728614 +80728644 0x80728644 +8072868c 0x8072868c +807287b8 0x807287b8 +80728870 0x80728870 +807289a4 0x807289a4 +80728a18 0x80728a18 +80728a58 0x80728a58 +80728a98 0x80728a98 +80728b7c 0x80728b7c +80728c54 0x80728c54 +80728cc8 0x80728cc8 +80728d94 0x80728d94 +80728e2c 0x80728e2c +80728e70 0x80728e70 +80728f60 0x80728f60 +8072910c 0x8072910c +80729188 0x80729188 +80729218 0x80729218 +80729338 0x80729338 +80729340 0x80729340 +80729348 0x80729348 +80729350 0x80729350 +80729614 0x80729614 +80729654 0x80729654 +80729694 0x80729694 +807296b4 0x807296b4 +80729720 0x80729720 +80729728 0x80729728 +80729868 0x80729868 +80729944 0x80729944 +80729a8c 0x80729a8c +80729a98 0x80729a98 +80729ac0 0x80729ac0 +80729acc 0x80729acc +80729b88 0x80729b88 +80729b90 0x80729b90 +80729b9c 0x80729b9c +80729cb4 0x80729cb4 +80729cfc 0x80729cfc +80729d2c 0x80729d2c +80729d74 0x80729d74 +80729d9c AI::CPUDriving::__ct +80729f18 AI::CPUDriving::__dt +8072a0a8 0x8072a0a8 +8072a0e8 AI::CPUDriving::Init +8072a21c AI::CPUDriving::InitEnemyRoute +8072a2a8 0x8072a2a8 +8072a2e8 0x8072a2e8 +8072a2f0 0x8072a2f0 +8072a2f8 0x8072a2f8 +8072a300 0x8072a300 +8072a310 AI::CPUDriving::CalcStartBoost +8072a370 0x8072a370 +8072a374 ParamActionCalculator::GetStartBoost +8072a37c 0x8072a37c +8072a560 0x8072a560 +8072a568 0x8072a568 +8072a570 0x8072a570 +8072a650 0x8072a650 +8072a674 0x8072a674 +8072a6b4 0x8072a6b4 +8072a6c8 0x8072a6c8 +8072a6dc 0x8072a6dc +8072a740 0x8072a740 +8072a754 0x8072a754 +8072a794 0x8072a794 +8072a7a0 0x8072a7a0 +8072a844 0x8072a844 +8072a88c AI::CPUDriving::IsBattle +8072a894 0x8072a894 +8072a898 0x8072a898 +8072a8a0 0x8072a8a0 +8072a8a8 0x8072a8a8 +8072a8ac 0x8072a8ac +8072a8e8 0x8072a8e8 +8072b07c 0x8072b07c +8072b0bc 0x8072b0bc +8072b1dc 0x8072b1dc +8072b3d8 0x8072b3d8 +8072b3dc 0x8072b3dc +8072b454 0x8072b454 +8072b498 0x8072b498 +8072b500 0x8072b500 +8072b5f8 0x8072b5f8 +8072b618 0x8072b618 +8072b680 0x8072b680 +8072b86c 0x8072b86c +8072b874 0x8072b874 +8072b95c 0x8072b95c +8072b964 0x8072b964 +8072b974 0x8072b974 +8072b9b8 0x8072b9b8 +8072bb18 0x8072bb18 +8072bd00 0x8072bd00 +8072bd1c 0x8072bd1c +8072be68 0x8072be68 +8072bf8c 0x8072bf8c +8072c160 0x8072c160 +8072c33c 0x8072c33c +8072c364 0x8072c364 +8072c444 0x8072c444 +8072c448 0x8072c448 +8072c45c 0x8072c45c +8072c468 0x8072c468 +8072c548 0x8072c548 +8072c568 0x8072c568 +8072c5a8 0x8072c5a8 +8072c5e8 0x8072c5e8 +8072c740 0x8072c740 +8072c80c 0x8072c80c +8072c814 0x8072c814 +8072c984 0x8072c984 +8072ca04 0x8072ca04 +8072ca18 0x8072ca18 +8072ca58 0x8072ca58 +8072ca68 0x8072ca68 +8072ca7c WorldLoader_GetStadiumTerrain +8072ca90 0x8072ca90 +8072caa0 0x8072caa0 +8072cab4 0x8072cab4 +8072caf4 0x8072caf4 +8072cc68 0x8072cc68 +8072cc70 0x8072cc70 +8072cc84 0x8072cc84 +8072ccc4 0x8072ccc4 +8072d1dc 0x8072d1dc +8072d1f0 0x8072d1f0 +8072d230 0x8072d230 +8072d4f4 0x8072d4f4 +8072d528 0x8072d528 +8072d568 0x8072d568 +8072d5bc 0x8072d5bc +8072d5cc 0x8072d5cc +8072d7e8 0x8072d7e8 +8072d9d4 0x8072d9d4 +8072db40 0x8072db40 +8072dc28 0x8072dc28 +8072dce4 0x8072dce4 +8072dd18 0x8072dd18 +8072dd58 0x8072dd58 +8072ddb0 0x8072ddb0 +8072dde4 0x8072dde4 +8072de24 0x8072de24 +8072de64 0x8072de64 +8072de6c 0x8072de6c +8072de74 0x8072de74 +8072de7c 0x8072de7c +8072de84 0x8072de84 +8072de8c 0x8072de8c +8072de94 0x8072de94 +8072de9c 0x8072de9c +8072dea0 0x8072dea0 +8072dee0 0x8072dee0 +8072dee8 0x8072dee8 +8072def0 0x8072def0 +8072def8 0x8072def8 +8072df00 0x8072df00 +8072df08 0x8072df08 +8072df10 0x8072df10 +8072df58 0x8072df58 +8072df88 0x8072df88 +8072dfd0 0x8072dfd0 +8072e2b0 0x8072e2b0 +8072e2f0 0x8072e2f0 +8072e348 0x8072e348 +8072e3e8 0x8072e3e8 +8072e460 0x8072e460 +8072e470 0x8072e470 +8072e494 0x8072e494 +8072e4b0 0x8072e4b0 +8072e4d8 0x8072e4d8 +8072e678 0x8072e678 +8072e680 0x8072e680 +8072e688 0x8072e688 +8072e690 0x8072e690 +8072e6a0 0x8072e6a0 +8072e6a4 0x8072e6a4 +8072e6ac 0x8072e6ac +8072e6b4 0x8072e6b4 +8072e6bc 0x8072e6bc +8072e6c4 0x8072e6c4 +8072e6cc 0x8072e6cc +8072e6d4 0x8072e6d4 +8072e6dc 0x8072e6dc +8072e6e0 0x8072e6e0 +8072e720 0x8072e720 +8072e768 0x8072e768 +8072e798 0x8072e798 +8072e7e0 0x8072e7e0 +8072e9dc 0x8072e9dc +8072ea1c 0x8072ea1c +8072ea74 0x8072ea74 +8072eb84 0x8072eb84 +8072ec00 0x8072ec00 +8072ec28 0x8072ec28 +8072ecc4 0x8072ecc4 +8072edcc 0x8072edcc +8072ee9c 0x8072ee9c +8072f00c 0x8072f00c +8072f318 0x8072f318 +8072f328 0x8072f328 +8072f528 0x8072f528 +8072f554 0x8072f554 +8072f654 0x8072f654 +8072f734 0x8072f734 +8072f800 0x8072f800 +8072f920 0x8072f920 +8072f960 0x8072f960 +8072f9b8 0x8072f9b8 +8072f9f8 0x8072f9f8 +8072fa74 0x8072fa74 +8072fb7c 0x8072fb7c +8072fc98 0x8072fc98 +8072fca8 0x8072fca8 +8072fd1c 0x8072fd1c +8072fd38 0x8072fd38 +8072ff60 0x8072ff60 +8072ff68 0x8072ff68 +8072ff70 0x8072ff70 +8072ff78 0x8072ff78 +8072ff80 0x8072ff80 +8072ff88 0x8072ff88 +8072ffd0 0x8072ffd0 +80730000 0x80730000 +80730048 0x80730048 +80730090 0x80730090 +807300c0 0x807300c0 +80730108 0x80730108 +80730198 AI::CPUDrivingRace::__ct +80730324 AI::CPUDrivingAction::__dt +80730364 AI::CPUDrivingRace::__dt +80730420 AI::CPUDrivingRace::Init +80730510 AI::CPUDrivingRace::InitEnemyRoute +807306ac 0x807306ac +80730758 0x80730758 +8073078c AI::CPUDrivingRace::Update +80730888 0x80730888 +807308dc 0x807308dc +80730a78 0x80730a78 +80730a7c 0x80730a7c +80730a80 AI::CPUDrivingAction::OnStart +80730ac8 AI::CPUDrivingAction::Update +80730af8 AI::CPUDrivingAction::OnEnd +80730b40 0x80730b40 +80730b60 0x80730b60 +80730ba0 0x80730ba0 +80730bd8 0x80730bd8 +80730da0 0x80730da0 +80730db4 0x80730db4 +80730e9c 0x80730e9c +80731004 0x80731004 +80731038 0x80731038 +80731044 0x80731044 +80731148 0x80731148 +80731204 0x80731204 +807312d0 0x807312d0 +807314a0 0x807314a0 +80731738 0x80731738 +8073177c 0x8073177c +807317a8 0x807317a8 +807317e8 0x807317e8 +80731960 AI::Inputs::__ct +80731984 AI::Inputs::Reset +8073199c AI::Inputs::AddInputs +807319ac AI::Inputs::RemoveInputs +807319bc 0x807319bc +807319c4 AI::Player::__ct +80732360 AI::PlayerActions::__dt +807323a0 AI::Player::__dt +80732508 AI::Player::Init +80732678 AI::Player::Update +8073269c AI::Player::vf_0x20 +8073272c 0x8073272c +8073274c 0x8073274c +807327f0 0x807327f0 +80732890 AI::Player::OnStartPreRace +807328e0 AI::Player::UpdatePreRace +80732a6c AI::Player::OnStartAI::CPUDriving +80732a70 AI::Player::UpdateAI::CPUDriving +80732c6c AI::Player::OnStartRealPlayerDriving +80732c70 AI::Player::UpdateRealPlayerDriving +80732d84 AI::Player::OnStartAction3 +80732d88 AI::Player::UpdateAction3 +80732d8c AI::Player::OnStartPostRaceDriving +80732ddc 0x80732ddc +80732de0 AI::Player::UpdatePostRaceDriving +80732f84 AI::Player::OnStartPostBattleDriving +80732fd4 AI::Player::UpdatePostBattleDriving +807330f4 AI::PlayerActions::OnStart +8073313c AI::PlayerActions::Update +8073316c AI::PlayerActions::OnEnd +807331b4 AI::PlayerBike::__ct +80733238 AI::PlayerBike::__dt +80733290 AI::PlayerBike::Init +80733294 AI::PlayerBike::Update +80733298 AI::PlayerBike::vf_0x20 +8073329c AI::PlayerBike::OnStartAction1 +807332a0 AI::PlayerBike::UpdateAction1 +807332a4 AI::PlayerKart::__ct +807332fc AI::PlayerKart::__dt +80733354 AI::PlayerKart::Init +807333b8 AI::PlayerKart::Update +807333bc AI::PlayerKart::vf_0x20 +807333c0 AI::PlayerKart::OnStartAction1 +807333c4 AI::PlayerKart::UpdateAction1 +807333c8 0x807333c8 +80733a7c 0x80733a7c +80733abc 0x80733abc +80733afc 0x80733afc +80733b1c 0x80733b1c +80733b60 0x80733b60 +80733ba4 0x80733ba4 +80733bac 0x80733bac +80733bb4 0x80733bb4 +80733bbc 0x80733bbc +80733bc4 0x80733bc4 +80733bcc 0x80733bcc +80733c60 0x80733c60 +80733ce4 0x80733ce4 +80733d20 0x80733d20 +80733d4c 0x80733d4c +80733d60 0x80733d60 +80733d7c 0x80733d7c +80733d94 0x80733d94 +80733db0 0x80733db0 +80733e18 0x80733e18 +80733e24 0x80733e24 +80734058 0x80734058 +80734060 0x80734060 +80734064 0x80734064 +8073414c 0x8073414c +80734150 0x80734150 +8073421c 0x8073421c +80734220 0x80734220 +807342e8 0x807342e8 +807342ec 0x807342ec +807342f8 0x807342f8 +807343f4 0x807343f4 +80734400 0x80734400 +807344fc 0x807344fc +80734500 0x80734500 +8073458c 0x8073458c +80734590 0x80734590 +80734594 0x80734594 +807345dc 0x807345dc +8073461c 0x8073461c +80734674 0x80734674 +807346b8 0x807346b8 +807346c4 0x807346c4 +807347b8 0x807347b8 +807348c0 0x807348c0 +80734a4c 0x80734a4c +80734a58 0x80734a58 +80734a70 0x80734a70 +80734b40 0x80734b40 +80734c34 0x80734c34 +80734c74 0x80734c74 +80734e0c 0x80734e0c +80734ee4 0x80734ee4 +807350d0 0x807350d0 +807350e0 0x807350e0 +807350e4 0x807350e4 +807351dc 0x807351dc +80735224 0x80735224 +80735254 0x80735254 +8073529c 0x8073529c +807352b0 0x807352b0 +807352f0 0x807352f0 +807352f4 0x807352f4 +807352f8 0x807352f8 +80735348 0x80735348 +807353f8 0x807353f8 +8073540c 0x8073540c +8073544c 0x8073544c +80735450 0x80735450 +80735460 0x80735460 +80735464 0x80735464 +8073555c 0x8073555c +80735570 0x80735570 +807355b0 0x807355b0 +807355b4 0x807355b4 +807355f4 0x807355f4 +80735600 0x80735600 +80735724 0x80735724 +8073582c 0x8073582c +80735840 0x80735840 +80735880 0x80735880 +80735948 AI::Character::__ct +80736218 AI::CharacterActions::__dt +80736258 AI::Character::__dt +807362dc AI::Character::Init +807363c0 AI::Character::Update +80736400 AI::Character::vf_0x20 +80736480 AI::Character::StartDriving +807364d8 0x807364d8 +80736588 0x80736588 +807365c4 0x80736588_switch +807365c8 0x80736588_caseD_1 +807365d0 0x80736588_caseD_0 +807365d4 0x80736588_caseD_13 +807365dc 0x80736588_caseD_18 +807365e4 0x80736588_caseD_1e +807365e8 0x80736588_caseD_24 +807365f4 0x807365f4 +80736650 0x80736650 +8073668c 0x8073668c +807366e4 0x807366e4 +80736764 AI::Character::ArePlayersClose +8073684c 0x8073684c +807369bc AI::Character::vf_0x8c +80736ae4 0x80736ae4 +80736bcc 0x80736bcc +80736ca0 AI::Character::vf_0x94 +80736d24 AI::Character::OnStartAction0 +80736d30 AI::Character::UpdateAction0 +80736d8c AI::Character::OnStartAction1 +80736d90 AI::Character::UpdateAction1 +80736ec4 AI::Character::OnStartAction2 +80736ec8 AI::Character::UpdateAction2 +8073700c AI::Character::OnEndAction2 +80737048 AI::Character::OnStartAction3 +80737054 AI::Character::UpdateAction3 +807371a8 AI::Character::OnStartAction4 +807372e4 AI::Character::UpdateAction4 +807373a8 AI::Character::OnEndAction4 +807373e4 AI::Character::OnStartAction5 +807373f8 AI::Character::UpdateAction5 +80737600 AI::Character::OnEndAction5 +8073763c AI::Character::OnStartAction6 +80737798 AI::Character::UpdateAction6 +80737888 AI::Character::OnEndAction6 +807378c4 AI::Character::OnStartAction7 +807378d4 AI::Character::UpdateAction7 +80737934 AI::Character::OnEndAction7 +80737970 AI::Character::OnStartLookAhead +807379ac AI::Character::UpdateLookAhead +807379b0 AI::Character::OnStartLookAtCamera +807379bc AI::Character::UpdateLookAtCamera +80737c30 0x80737c30 +80737c50 0x80737c50 +80737c90 0x80737c90 +80737c9c 0x80737c9c +80737ecc 0x80737ecc +807381a4 0x807381a4 +80738558 AI::CharacterBattle::__ct +80738594 AI::CharacterBattle::__dt +807385ec AI::CharacterBattle::Init +80738624 AI::CharacterBattle::OnStartAction1 +80738628 AI::CharacterBattle::UpdateAction1 +80738664 AI::CharacterAwards::__ct +807386a0 AI::CharacterAwards::__dt +807386f8 AI::CharacterAwards::Init +80738730 AI::CharacterAwards::OnStartAction1 +80738734 AI::CharacterAwards::UpdateAction1 +80738770 0x80738770 +807387ac 0x807387ac +80738804 0x80738804 +80738848 0x80738848 +80738960 0x80738960 +80738a48 0x80738a48 +80738a4c 0x80738a4c +80738ac4 0x80738ac4 +80738ba4 0x80738ba4 +80738cb4 0x80738cb4 +80738cf0 AI::CharacterAwards::vf_0x94 +80738cf8 AI::CharacterActions::OnStart +80738d40 AI::CharacterActions::Update +80738d70 AI::CharacterActions::OnEnd +80738db8 AI::Manager::Disposer__dt +80738e50 AI::Manager::CreateInstance +80738ea0 AI::Manager::DestroyInstance +80738ed4 AI::Manager::__ct +807390ac AI::Manager::__dt +8073922c AI::Manager::AddKartAIController +80739300 AI::Manager::GetKartAIController +80739310 AI::Manager::GetRealPlayerKartAIController +80739320 obj_checkIfSomething +80739328 AI::Manager::Init +8073942c AI::Manager::Update +80739530 AI::Manager::GetLocalPlayerCount +80739540 AI::Manager::GetEngineClass +80739548 AI::Manager::GetDifficulty +80739550 0x80739550 +8073958c 0x8073958c +8073965c AI::Manager::IsGPOrPrivateVS +80739684 AI::Manager::IsVS +807396a4 AI::Manager::IsTT +807396cc AI::Manager::IsMissionTournament +807396ec AI::Manager::IsOnline +80739714 AI::Manager::IsBattle +80739740 AI::Manager::IsOfflineBattle +80739760 AI::Manager::IsBallonBattle +80739778 AI::Manager::IsCoinRunners +80739794 AI::Manager::IsTT2 +807397bc AI::Manager::IsSpecialItemMode +8073986c AI::Manager::IsTeamMode +80739880 0x80739880 +80739888 AI::Manager::IsAwardsOrCredits +807398ac AiHolder_getRealPlayerCountPerTeam +80739944 AI::Manager::GetRandomValue +8073996c AI::Manager::GetRandomFloat +8073999c AI::Manager::SetCCAndDifficulty +80739a7c 0x80739a7c +80739b28 0x80739b28 +80739bd0 AiHolder_getBestPreviousScore +80739c70 0x80739c70 +80739cac 0x80739cac +80739d38 0x80739d38 +80739d7c 0x80739d7c +80739ee0 0x80739ee0 +80739f4c 0x80739f4c +80739ff0 0x80739ff0 +80739ff4 0x80739ff4 +8073a004 0x8073a004 +8073a024 0x8073a024 +8073a064 0x8073a064 +8073a0e8 0x8073a0e8 +8073a384 0x8073a384 +8073a5e4 0x8073a5e4 +8073a674 0x8073a674 +8073a880 0x8073a880 +8073a994 0x8073a994 +8073aaa4 0x8073aaa4 +8073ab30 0x8073ab30 +8073ab68 AI::Params::__ct +8073abf8 AI::Params::__dt +8073ac80 AI::Params::GetParamSpeed +8073ac88 AI::Params::GetParamAction +8073ac98 AI::Params::LoadParamFile +8073aca8 AI::Params::Load +8073af70 0x8073af70 +8073afcc 0x8073afcc +8073b020 AI::Params::LoadActions +8073b0f8 AI::EnemyRouteController::__ct +8073b49c AI::ENPTSettingsHolder::__dt +8073b4dc AI::EnemyRouteController::__dt +8073b5dc AI::EnemyRouteController::Init +8073b690 AI::EnemyRouteController::Update +8073b7f0 0x8073b7f0 +8073b830 0x8073b830 +8073b874 0x8073b874 +8073b894 0x8073b894 +8073b8b0 0x8073b8b0 +8073b8b8 0x8073b8b8 +8073b8c0 0x8073b8c0 +8073b8e4 0x8073b8e4 +8073b988 0x8073b988 +8073ba70 0x8073ba70 +8073ba78 0x8073ba78 +8073baec 0x8073baec +8073bb40 0x8073bb40 +8073bb50 0x8073bb50 +8073bc28 0x8073bc28 +8073bd08 0x8073bd08 +8073bd50 AI::EnemyRouteController::IsOutCurENPTBounds +8073bf24 0x8073bf24 +8073c03c 0x8073c03c +8073c110 AI::EnemyRouteHolder::AdvanceToNextENPT +8073c24c 0x8073c24c +8073c2c0 AI::EnemyRouteHolder::__ct +8073c308 AI::EnemyRouteHolder::__dt +8073c348 AI::EnemyRouteHolder::AddKartAIController +8073c38c 0x8073c38c +8073c390 0x8073c390 +8073c394 0x8073c394 +8073c540 0x8073c540 +8073c54c AI::ENPTController::__ct +8073c5b8 AI::ENPTController::__dt +8073c5f8 AI::ENPTController::SetStartingENPT +8073c644 AI::ENPTController::Init +8073c9b8 AI::ENPTController::Update +8073c9bc AI::ENPTController::GetPrevPrevPrevENPTPosition +8073ca10 AI::ENPTController::GetPrevPrevENPTPosition +8073ca64 AI::ENPTController::GetPrevENPTPosition +8073cab8 AI::ENPTController::GetCurENPTPosition +8073cb0c AI::ENPTController::GetNextENPTPosition +8073cb60 AI::ENPTController::GetCurPosition +8073cb7c 0x8073cb7c +8073cca8 0x8073cca8 +8073ccec 0x8073ccec +8073cd3c AI::ENPTController::CalcNext +8073d060 0x8073d060 +8073d1f0 0x8073d1f0 +8073d228 0x8073d228 +8073d248 0x8073d248 +8073d284 0x8073d284 +8073d2a4 0x8073d2a4 +8073d2c4 0x8073d2c4 +8073d2d0 0x8073d2d0 +8073d37c 0x8073d37c +8073d3b4 0x8073d3b4 +8073d59c 0x8073d59c +8073d6ec 0x8073d6ec +8073d934 0x8073d934 +8073d98c AI::ENPTController::CalcNextPosition +8073dab8 0x8073dab8 +8073dac4 0x8073dac4 +8073dbcc 0x8073dbcc +8073dbf4 0x8073dbf4 +8073dc38 0x8073dc38 +8073dc78 0x8073dc78 +8073e01c 0x8073e01c +8073e05c 0x8073e01c_switch +8073e060 0x8073e01c_caseD_0 +8073e06c 0x8073e01c_caseD_3 +8073e0a4 0x8073e01c_caseD_4 +8073e0dc 0x8073e01c_caseD_5 +8073e11c 0x8073e01c_caseD_6 +8073e15c 0x8073e01c_caseD_7 +8073e19c 0x8073e01c_caseD_9 +8073e1a8 0x8073e01c_caseD_a +8073e1b0 0x8073e01c_caseD_1 +8073e1cc 0x8073e1cc +8073e2fc 0x8073e2fc +8073e36c 0x8073e36c +8073e3dc 0x8073e3dc +8073e44c 0x8073e44c +8073e4b4 0x8073e4b4 +8073e51c 0x8073e51c +8073e5d0 0x8073e5d0 +8073e688 0x8073e688 +8073e798 0x8073e798 +8073e8a8 0x8073e8a8 +8073e8ec 0x8073e8ec +8073e92c 0x8073e92c +8073e96c 0x8073e92c_switch +8073e970 0x8073e92c_caseD_0 +8073e97c 0x8073e92c_caseD_3 +8073e9b4 0x8073e92c_caseD_4 +8073e9ec 0x8073e92c_caseD_5 +8073ea2c 0x8073e92c_caseD_6 +8073ea6c 0x8073e92c_caseD_7 +8073eaac 0x8073e92c_caseD_8 +8073eae4 0x8073e92c_caseD_9 +8073eaf0 0x8073e92c_caseD_a +8073eaf8 0x8073e92c_caseD_1 +8073eb14 0x8073eb14 +8073eb7c 0x8073eb7c +8073eb8c 0x8073eb8c +8073ebe4 AI::ENPTSettingsHolder::IsCutPoint +8073ebf8 AI::ENPTSettingsHolder::IsUseOffroadCutItem +8073ec0c AI::ENPTSettingsHolder::MustWheelie +8073ec20 AI::ENPTSettingsHolder::MustEndWheelie +8073ec34 AI::ENPTSettingsHolder::IsSetting1Equal5 +8073ec48 AI::ENPTSettingsHolder::MustEndDrift +8073ec5c AI::ENPTSettingsHolder::IsDriftForbidden +8073ec70 AI::ENPTSettingsHolder::MustDrift +8073ec84 AI::ENPTSettingsHolder::IsSetting3HighestBitSet +8073ec90 0x8073ec90 +8073ecac AI::ENPTSettingsHolder::IsDriftAllowed +8073ece0 0x8073ece0 +8073ecf4 0x8073ecf4 +8073ed08 0x8073ed08 +8073ed1c 0x8073ed1c +8073ed30 0x8073ed30 +8073ed3c 0x8073ed3c +8073ed48 0x8073ed48 +8073ed54 0x8073ed54 +8073ed94 0x8073ed94 +8073edd4 0x8073edd4 +8073edf0 AI::ParamActionCalculator::__ct +8073ee14 AI::ParamActionCalculator::__dt +8073ee54 AI::ParamActionCalculator::Init +8073ee8c AI::ParamActionCalculator::CalcStartBoost +8073efa8 AI::ParamActionCalculator::CalcGotDrift +8073f064 AI::ParamActionCalculator::CalcGotMT +8073f108 AI::ParamActionCalculator::CalcGotTrick +8073f190 AI::ParamActionCalculator::CalcGotWheelie +8073f218 AI::ParamActionCalculator::CalcGotParamAction6 +8073f2a0 AI::ParamActionCalculator::CalcGotParamAction7 +8073f328 0x8073f328 +8073f37c 0x8073f37c +8073f3c0 0x8073f3c0 +8073f400 0x8073f400 +8073f43c 0x8073f43c +8073f4d4 0x8073f4d4 +8073f4f4 0x8073f4f4 +8073f50c 0x8073f50c +8073f614 0x8073f614 +8073f668 0x8073f668 +8073f6a4 0x8073f6a4 +8073f6e4 0x8073f6e4 +8073f714 0x8073f714 +8073f738 0x8073f738 +8073f76c 0x8073f76c +8073f7a8 0x8073f7a8 +8073f848 0x8073f848 +8073f8f8 0x8073f8f8 +8073f92c 0x8073f92c +8073fa0c 0x8073fa0c +8073fa20 0x8073fa20 +8073fa5c 0x8073fa5c +8073fa88 0x8073fa88 +8073fab4 0x8073fab4 +8073fb78 0x8073fb78 +8073fbb8 0x8073fbb8 +8073fbf8 0x8073fbf8 +8073fc28 0x8073fc28 +8073fc34 0x8073fc34 +8073fc40 0x8073fc40 +8073fcd0 0x8073fcd0 +8073fd88 0x8073fd88 +807400b0 0x807400b0 +807401b4 0x807401b4 +807404b8 0x807404b8 +807404c8 0x807404c8 +80740604 0x80740604 +80740700 0x80740700 +80740704 0x80740704 +80740738 0x80740738 +80740774 0x80740774 +807407b4 0x807407b4 +807407e4 0x807407e4 +807407f0 0x807407f0 +807407fc 0x807407fc +8074080c 0x8074080c +807409b4 0x807409b4 +80740ab0 0x80740ab0 +80740ab4 0x80740ab4 +80740ae8 0x80740ae8 +80740b24 0x80740b24 +80740b64 0x80740b64 +80740b94 0x80740b94 +80740c90 0x80740c90 +80740c94 0x80740c94 +80740cc8 OS::GetCurrentContext +80740cd4 0x80740cd4 +80740ce0 0x80740ce0 +80740ce4 0x80740ce4 +80740ce8 0x80740ce8 +80740cf0 0x80740cf0 +80740cf8 0x80740cf8 +80740d00 0x80740d00 +80740d08 0x80740d08 +80740d10 0x80740d10 +80740d14 0x80740d14 +80740d18 0x80740d18 +80740d1c 0x80740d1c +80741388 0x80741388 +807413c8 0x807413c8 +807414b8 0x807414b8 +80741568 0x80741568 +80741654 GameInfoManager_SetUserSelectedCupSidekick +80741660 0x80741660 +80741804 0x80741804 +80741878 0x80741878 +80741880 0x80741880 +80741888 0x80741888 +80741898 0x80741898 +807418d8 0x807418d8 +807418e0 0x807418e0 +80741980 0x80741980 +80741998 0x80741998 +80741a10 0x80741a10 +80741cec 0x80741cec +8074204c 0x8074204c +8074210c 0x8074210c +807421fc 0x807421fc +807422cc 0x807422cc +80742398 0x80742398 +807424e0 0x807424e0 +80742584 0x80742584 +807426e4 0x807426e4 +807427f8 0x807427f8 +80742854 0x80742854 +80742858 0x80742858 +80742898 0x80742898 +80742944 0x80742944 +80742a60 0x80742a60 +80742b04 0x80742b04 +80742b10 0x80742b10 +80742bc0 0x80742bc0 +80742c0c 0x80742c0c +80742c58 0x80742c58 +80742cc8 0x80742cc8 +80742e80 0x80742e80 +80742fe0 0x80742fe0 +807431bc 0x807431bc +80743344 0x80743344 +807433e4 0x807433e4 +807433e8 0x807433e8 +80743428 0x80743428 +80743440 0x80743440 +80743458 0x80743458 +80743470 0x80743470 +807434d0 0x807434d0 +8074372c 0x8074372c +80743774 0x80743774 +807437a4 0x807437a4 +807437ec 0x807437ec +80743818 0x80743818 +80743858 0x80743858 +80743890 0x80743890 +807438dc 0x807438dc +8074392c 0x8074392c +8074398c 0x8074398c +80743998 0x80743998 +807439b0 cGame_GetGameTime +807439bc 0x807439bc +807439f0 0x807439f0 +80743c40 0x80743c40 +80743c94 0x80743c94 +80743cd4 0x80743cd4 +80743df4 0x80743df4 +80743e20 0x80743e20 +80743e80 0x80743e80 +80743f1c 0x80743f1c +80743fb8 0x80743fb8 +80744194 0x80744194 +807441cc 0x807441cc +8074420c 0x8074420c +80744258 0x80744258 +80744364 0x80744364 +80744398 0x80744398 +807443d8 0x807443d8 +80744478 0x80744478 +807445fc 0x807445fc +80744634 0x80744634 +80744674 0x80744674 +807446c0 0x807446c0 +80744728 0x80744728 +80744730 0x80744730 +80744a2c 0x80744a2c +80744a6c 0x80744a6c +80744af4 0x80744af4 +80744ba4 0x80744ba4 +80744c1c 0x80744c1c +80744c24 0x80744c24 +80744c48 0x80744c48 +80744ce0 0x80744ce0 +80744cec 0x80744cec +80744dac 0x80744dac +80744ee4 0x80744ee4 +80744f78 0x80744f78 +80744f8c 0x80744f8c +80745164 0x80745164 +807451d8 0x807451d8 +80745658 0x80745658 +807456c0 0x807456c0 +807456f8 0x807456f8 +807456fc 0x807456fc +8074571c 0x8074571c +8074575c 0x8074575c +8074576c 0x8074576c +807457c4 0x807457c4 +8074580c 0x8074580c +8074583c 0x8074583c +80745884 MotionController::__ct +8074591c 0x8074591c +8074595c MotionController::__dt +807459e4 MotionController::Init +80745be4 MotionController::UpdateForNunchuck +80745de4 MotionController::UpdateForCore +8074612c 0x8074612c +807462b8 0x807462b8 +8074644c 0x8074644c +8074649c 0x8074649c +80746568 0x80746568 +8074675c 0x8074675c +807467bc 0x807467bc +80746970 0x80746970 +80746afc 0x80746afc +80746c64 0x80746c64 +80747154 0x80747154 +80747198 Objects::PoihanaBase::__ct +80747208 Objects::PoihanaBase::__dt +80747248 Objects::PoihanaBase::OnStart +807472f4 Objects::PoihanaBase::Update +80747308 Objects::PoihanaBase:GetTranslation +80747324 0x80747324 +80747380 0x80747380 +8074739c 0x8074739c +80747404 0x80747404 +80747494 0x80747494 +807474e4 0x807474e4 +80747530 0x80747530 +807475dc 0x807475dc +807476d8 0x807476d8 +80747788 0x80747788 +8074792c UnkCollidableSub::__ct +80747974 UnkCollidableSub::__dt +807479b4 UnkCollidableSub::Reset +807479d4 UnkCollidableSub::Update +80747bfc 0x80747bfc +80747c18 UnkCollidableSub::vf_0x14 +80747c40 Objects::PoihanaBase::SubChild::__ct +80747c98 Objects::PoihanaBase::SubChild::__dt +80747cd8 Objects::PoihanaBase::SubChild::Reset +80747cfc Objects::PoihanaBase::SubChild::Update +80747f60 0x80747f60 +80748138 Objects::Poihana::SubChild::vf_0x14 +8074815c Objects::PoihanaBase::GetPropertiesBitfield +80748164 Objects::PoihanaBase::__dt(thunk) +8074816c Objects::Poihana::__ct +8074887c Objects::Poihana::Actions::__dt +807488bc Objects::Poihana::__dt +80748958 Objects::Poihana::OnStart +80748b70 Objects::Poihana::Update +80748bd4 0x80748bd4 +80748cec Objects::Poihana::LoadAnimations +80748d98 0x80748d98 +80749378 Objects::Poihana::OnCollision +80749504 Objects::Poihana::OnItemCollision +80749610 0x80749610 +807496c4 0x807496c4 +80749794 0x80749794 +80749808 0x80749808 +80749824 0x80749824 +807498ac Objects::Poihana::OnStartIdle +80749924 Objects::Poihana::UpdateIdleAndAction1 +80749ae8 Objects::Poihana::OnStartChase +80749b00 Objects::Poihana::UpdateChase +80749f54 0x80749f54 +80749f70 Objects::Poihana::OnStartCatapult +8074a104 Objects::Poihana::UpdateCatapult +8074a180 Objects::Poihana::OnEndCatapult +8074a18c Objects::Poihana::OnStartCatapultNoMove +8074a1dc Objects::Poihana::UpdateCatapultNoMove +8074a28c Objects::Poihana::OnEndCatapultNoMove +8074a298 Objects::Poihana::Topple +8074a4a4 Objects::Poihana::OnStartToppleNormal +8074a4bc Objects::Poihana::OnStartToppleInverted +8074a4d8 Objects::Poihana::UpdateTopple +8074a720 Objects::Poihana::OnEndTopple +8074a75c Objects::Poihana::OnStartToppleNoBury +8074a8ac Objects::Poihana::UpdateToppleNoBury +8074ad3c 0x8074ad3c +8074ad58 Objects::Poihana::OnEndToppleNoBury +8074ad64 Objects::Poihana::Actions::OnStart +8074adac Objects::Poihana::Actions::Update +8074addc Objects::Poihana::Actions::OnEnd +8074ae24 Objects::Poihana::__dt(thunk) +8074ae2c AI::Base::__ct +8074af38 AI::Base::UpdateActions +8074afb4 AI::Base::SetInitialActions +8074afcc AI::Base::HasSameActions +8074afe0 AI::Base::SetNextActions +8074afec AI::Actions::OnActionStart +8074b034 AI::Actions::Update +8074b064 AI::Actions::OnActionEnd +8074b0ac 0x8074b0ac +8074b0f8 0x8074b0f8 +8074b19c 0x8074b19c +8074b34c 0x8074b34c +8074b41c 0x8074b41c +8074b668 0x8074b668 +8074b7f4 0x8074b7f4 +8074b82c 0x8074b82c +8074b8a4 0x8074b8a4 +8074b9a8 0x8074b9a8 +8074bb34 0x8074bb34 +8074bbcc 0x8074bbcc +8074bcd0 0x8074bcd0 +8074bcec 0x8074bcec +8074bcf0 0x8074bcf0 +8074be40 0x8074be40 +8074bf28 0x8074bf28 +8074bfa0 0x8074bfa0 +8074bfd0 0x8074bfd0 +8074c048 0x8074c048 +8074c0b4 0x8074c0b4 +8074c1e0 0x8074c1e0 +8074c2a4 0x8074c2a4 +8074c2b4 0x8074c2b4 +8074c34c 0x8074c34c +8074c3f0 0x8074c3f0 +8074c4a8 AwardCupModel::Disposer__dt +8074c5b0 AwardCupModel::CreateInstance +8074c680 AwardCupModel::DestroyInstance +8074c73c AwardCupModel::__ct +8074c7e0 AwardCupModel::__dt +8074c8a8 AwardCupModel::Load +8074c914 AwardCupModel::Load_switch +8074c918 AwardCupModel::Load_caseD_0 +8074c94c AwardCupModel::Load_caseD_1 +8074c980 AwardCupModel::Load_caseD_2 +8074c9b4 AwardCupModel::Load_caseD_3 +8074c9e8 AwardCupModel::Load_caseD_4 +8074ca1c AwardCupModel::Load_caseD_5 +8074ca50 AwardCupModel::Load_caseD_6 +8074ca84 AwardCupModel::Load_caseD_7 +8074cab8 AwardCupModel::Load_caseD_8 +8074cd20 AwardCupModel::Prepare +8074ce50 AwardCupModel::Activate +8074cf00 AwardCupModel::Update +8074d184 0x8074d184 +8074d2ec 0x8074d2ec +8074d390 0x8074d390 +8074d43c 0x8074d43c +8074d580 0x8074d580 +8074d5b8 GlobeMgr::Disposer__dt +8074d650 GlobeMgr::CreateInstance +8074d6cc GlobeMgr::DestroyInstance +8074d700 EarthModel::__ct +8074dc64 0x8074dc64 +8074dcc8 EarthModel::__dt +8074dd48 0x8074dd48 +8074df78 0x8074df78 +8074df7c 0x8074df7c +8074df80 0x8074df80 +8074df84 0x8074df84 +8074e030 0x8074e030 +8074e1f0 0x8074e1f0 +8074e214 0x8074e214 +8074e32c 0x8074e32c +8074e3ac 0x8074e3ac +8074e4d4 0x8074e4d4 +8074e600 0x8074e600 +8074e72c 0x8074e72c +8074e858 0x8074e858 +8074e984 0x8074e984 +8074eab0 0x8074eab0 +8074ebdc 0x8074ebdc +8074ee8c 0x8074ee8c +8074f208 GlobeMii::GetTranslation +8074f210 0x8074f210 +8074f260 0x8074f260 +8074f280 0x8074f280 +8074f2a0 0x8074f2a0 +8074f2c0 0x8074f2c0 +8074f2e4 0x8074f2e4 +8074f304 0x8074f304 +8074f558 0x8074f558 +8074f774 0x8074f774 +8074f9d0 0x8074f9d0 +8074fa40 0x8074fa40 +8074fa80 0x8074fa80 +8074fac0 0x8074fac0 +8074fb00 0x8074fb00 +8074fb44 0x8074fb44 +8074fb84 0x8074fb84 +8074fe38 0x8074fe38 +8074fe3c GlobeMgr::__ct +8074fe8c GlobeMgr::__dt +8074ff54 GlobeMgr::Init +80750018 0x80750018 +80750238 0x80750238 +807502f4 0x807502f4 +80750344 GlobeMgr::Update +807506c8 0x807506c8 +80750708 0x80750708 +807507ec 0x807507ec +807507fc GlobeMgr::IsMiiEnabled +80750808 GlobeMgr::SetPosition +80750998 0x80750998 +8075099c 0x8075099c +807509a0 GlobeMgr::SetMii +807509b4 GlobeMgr::DisplayMii +807509d0 GlobeMgr::ResetGlobeMii +807509e4 0x807509e4 +80750aac GlobeMii::__ct +8075102c GlobeMii::Init +80751144 GlobeMii::Update +80751208 GlobeMii::SetMii +80751528 GlobeMii::IsFemaleMii +80751540 GlobeMii::DisplayMii +807518bc GlobeMii::Reset +8075193c 0x8075193c +807519c8 0x807519c8 +80751a2c 0x80751a2c +80751a6c 0x80751a6c +80751a70 0x80751a70 +80751a74 0x80751a74 +80751b4c 0x80751b4c +80751c24 0x80751c24 +80751c2c 0x80751c2c +80751c6c 0x80751c6c +80751cac 0x80751cac +80751d60 0x80751d60 +80751ee4 0x80751ee4 +807522b8 0x807522b8 +80752304 0x80752304 +8075246c 0x8075246c +807525c0 0x807525c0 +80752704 0x80752704 +807527ec 0x807527ec +807529b8 0x807529b8 +80752a24 0x80752a24 +80752a30 0x80752a30 +80752a44 0x80752a44 +80752aa4 0x80752aa4 +80752aac 0x80752aac +80752ab4 0x80752ab4 +80752b34 0x80752b34 +80752b74 0x80752b74 +80752c20 0x80752c20 +80752cb8 0x80752cb8 +80752d2c 0x80752d2c +80752d30 0x80752d30 +80752eec 0x80752eec +80752fa4 0x80752fa4 +80753048 0x80753048 +807530c0 0x807530c0 +80753268 0x80753268 +807536a0 0x807536a0 +80753714 0x80753714 +80753720 0x80753720 +8075372c 0x8075372c +807538e8 0x807538e8 +80753960 0x80753960 +80753964 0x80753964 +807539e4 0x807539e4 +807539fc 0x807539fc +80753a0c 0x80753a0c +80753b34 0x80753b34 +80753e30 0x80753e30 +80753ea8 0x80753ea8 +80753eac 0x80753eac +80753f2c 0x80753f2c +80753f30 0x80753f30 +80753f34 0x80753f34 +80753f38 0x80753f38 +80753f3c 0x80753f3c +80753f40 0x80753f40 +80753f44 0x80753f44 +80753f4c 0x80753f4c +80753f50 0x80753f50 +80753f54 0x80753f54 +80753f58 0x80753f58 +80753f5c 0x80753f5c +80753f64 0x80753f64 +80753f6c 0x80753f6c +80753fbc 0x80753fbc +80754034 0x80754034 +80754038 0x80754038 +80754088 0x80754088 +80754100 0x80754100 +80754104 0x80754104 +80754264 0x80754264 +80754960 0x80754960 +807549f0 0x807549f0 +807549fc 0x807549fc +80754a08 0x80754a08 +80754ad4 0x80754ad4 +80754c14 0x80754c14 +80754c20 0x80754c20 +80754c2c 0x80754c2c +80754c38 0x80754c38 +80754c44 0x80754c44 +80754c50 0x80754c50 +80754cd8 0x80754cd8 +80754fd4 0x80754fd4 +80755110 0x80755110 +807551c4 0x807551c4 +80755274 0x80755274 +80755330 0x80755330 +807553d4 0x807553d4 +8075547c 0x8075547c +807558c4 0x807558c4 +80755ad8 0x80755ad8 +80755c58 0x80755c58 +80755c8c 0x80755c8c +80755fa0 0x80755fa0 +807560c8 0x807560c8 +807561d8 0x807561d8 +80756360 0x80756360 +80756578 0x80756578 +80756838 0x80756838 +80756a0c 0x80756a0c +80756c10 0x80756c10 +80756d1c 0x80756d1c +80756dcc 0x80756dcc +80756df0 0x80756df0 +8075710c 0x8075710c +807571c0 0x807571c0 +80757514 0x80757514 +8075758c 0x8075758c +807575fc 0x807575fc +80757638 0x80757638 +80757670 0x80757670 +80757764 0x80757764 +807578d8 BegomanManager::__ct +8075794c 0x8075794c +80757950 0x80757950 +80757db8 0x80757db8 +80757dd4 0x80757dd4 +80757e08 0x80757e08 +80757e90 0x80757e90 +80757ee0 0x80757ee0 +80758188 0x80758188 +807581a0 0x807581a0 +8075826c 0x8075826c +80758368 0x80758368 +80758444 0x80758444 +807585d0 0x807585d0 +80758674 0x80758674 +80758984 0x80758984 +80758a68 0x80758a68 +80758a70 0x80758a70 +80758a7c 0x80758a7c +80758a84 0x80758a84 +80758a88 0x80758a88 +80758a8c 0x80758a8c +80758a90 0x80758a90 +80758a94 0x80758a94 +80758a98 0x80758a98 +80758a9c 0x80758a9c +80758aa4 0x80758aa4 +80758aa8 0x80758aa8 +80758aac 0x80758aac +80758ab0 0x80758ab0 +80758ab4 0x80758ab4 +80758ab8 0x80758ab8 +80758abc 0x80758abc +80758ac4 0x80758ac4 +80758b04 0x80758b04 +80758b44 0x80758b44 +80758b84 0x80758b84 +80758bc4 0x80758bc4 +80758bcc 0x80758bcc +80758bd4 0x80758bd4 +80758bdc 0x80758bdc +80758ef0 0x80758ef0 +80758f30 0x80758f30 +80758f68 0x80758f68 +80759364 0x80759364 +80759388 0x80759388 +807596f0 0x807596f0 +807599d0 0x807599d0 +807599d8 0x807599d8 +807599e0 0x807599e0 +807599fc 0x807599fc +80759db8 0x80759db8 +80759de0 0x80759de0 +80759ee8 0x80759ee8 +8075a398 0x8075a398 +8075a3cc 0x8075a3cc +8075a4b8 0x8075a4b8 +8075a56c 0x8075a56c +8075a59c 0x8075a59c +8075a824 0x8075a824 +8075a864 0x8075a864 +8075a904 0x8075a904 +8075a9a4 0x8075a9a4 +8075aac8 0x8075aac8 +8075ae80 cFielder_IsStriker +8075ae90 0x8075ae90 +8075afbc 0x8075afbc +8075b08c 0x8075b08c +8075b178 0x8075b178 +8075b1f4 0x8075b1f4 +8075b260 0x8075b260 +8075b2e4 0x8075b2e4 +8075b3d0 0x8075b3d0 +8075b524 0x8075b524 +8075b61c 0x8075b61c +8075b65c 0x8075b65c +8075b694 0x8075b694 +8075b754 0x8075b754 +8075bc6c 0x8075bc6c +8075bdb4 0x8075bdb4 +8075bf98 0x8075bf98 +8075c0b0 0x8075c0b0 +8075c2a0 0x8075c2a0 +8075c2bc 0x8075c2bc +8075c454 0x8075c454 +8075c51c 0x8075c51c +8075c658 0x8075c658 +8075c888 0x8075c888 +8075c910 0x8075c910 +8075c958 0x8075c958 +8075c9a0 0x8075c9a0 +8075cbe4 0x8075cbe4 +8075cc34 0x8075cc34 +8075cc58 0x8075cc58 +8075cd7c 0x8075cd7c +8075ce20 0x8075ce20 +8075cf2c 0x8075cf2c +8075d000 0x8075d000 +8075d010 0x8075d010 +8075d034 0x8075d034 +8075d13c 0x8075d13c +8075d144 0x8075d144 +8075d26c 0x8075d26c +8075d378 0x8075d378 +8075d58c 0x8075d58c +8075d88c 0x8075d88c +8075d924 0x8075d924 +8075d9d0 0x8075d9d0 +8075d9d8 0x8075d9d8 +8075d9e0 0x8075d9e0 +8075d9ec 0x8075d9ec +8075d9f4 0x8075d9f4 +8075d9fc 0x8075d9fc +8075da08 0x8075da08 +8075da14 0x8075da14 +8075da20 0x8075da20 +8075da2c 0x8075da2c +8075da6c 0x8075da6c +8075daac 0x8075daac +8075daec 0x8075daec +8075db24 0x8075db24 +8075db2c 0x8075db2c +8075db34 0x8075db34 +8075db3c 0x8075db3c +8075dba0 0x8075dba0 +8075dc9c 0x8075dc9c +8075dca0 0x8075dca0 +8075de40 0x8075de40 +8075deac 0x8075deac +8075e070 0x8075e070 +8075e16c 0x8075e16c +8075e1f4 0x8075e1f4 +8075e28c 0x8075e28c +8075e2e0 0x8075e2e0 +8075e6e8 0x8075e6e8 +8075e73c 0x8075e73c +8075e744 0x8075e744 +8075e74c 0x8075e74c +8075e78c 0x8075e78c +8075e804 0x8075e804 +8075e844 0x8075e844 +8075e988 0x8075e988 +8075e98c 0x8075e98c +8075ea94 0x8075ea94 +8075eaf4 0x8075eaf4 +8075eafc obj_dossunc::__ct +8075edcc 0x8075edcc +8075ee68 0x8075ee68 +8075eea8 0x8075eea8 +8075f21c 0x8075f21c +8075f254 obj_dossun_active_update +8075f3f4 0x8075f3f4 +8075f430 0x8075f430 +8075f460 0x8075f460 +8075f4d8 0x8075f4d8 +8075f520 0x8075f520 +8075f584 0x8075f584 +8075f76c 0x8075f76c +8075fa08 0x8075fa08 +8075fb50 0x8075fb50 +8075fc7c 0x8075fc7c +8075fdcc 0x8075fdcc +8075fe80 0x8075fe80 +8075fe8c obj_dossun_endFall +8075fea0 0x8075fea0 +8075ff08 0x8075ff08 +8075ff98 0x8075ff98 +80760118 0x80760118 +80760188 0x80760188 +8076023c 0x8076023c +807602e0 obj_dossun_update +80760490 0x80760490 +807604c4 0x807604c4 +807605ec 0x807605ec +80760634 0x80760634 +80760820 0x80760820 +80760964 obj_dossun_make_active +80760a44 0x80760a44 +80760ad4 0x80760ad4 +80760b20 0x80760b20 +80760bd4 0x80760bd4 +80760c5c 0x80760c5c +80760d18 0x80760d18 +80760d8c 0x80760d8c +8076139c 0x8076139c +807614d0 0x807614d0 +807616c8 0x807616c8 +80761740 0x80761740 +80761744 0x80761744 +8076198c 0x8076198c +80762054 0x80762054 +80762190 0x80762190 +807623ac 0x807623ac +807624f0 0x807624f0 +807625f0 0x807625f0 +80762eec 0x80762eec +807630d0 0x807630d0 +8076321c 0x8076321c +807634c0 0x807634c0 +807638f8 0x807638f8 +80763918 0x80763918 +8076393c 0x8076393c +80763a00 0x80763a00 +80763a04 0x80763a04 +80763a10 0x80763a10 +80763b60 0x80763b60 +80763c14 0x80763c14 +80763e0c 0x80763e0c +80763eac 0x80763eac +80763f6c 0x80763f6c +80764054 0x80764054 +807640f0 0x807640f0 +807642e4 0x807642e4 +807642f8 0x807642f8 +807643a0 0x807643a0 +80764498 0x80764498 +80764510 0x80764510 +807645c4 0x807645c4 +80764604 0x80764604 +80764608 0x80764608 +80764644 0x80764644 +807646a4 0x807646a4 +8076479c 0x8076479c +807648a4 0x807648a4 +807649a4 0x807649a4 +80764a04 0x80764a04 +80764a0c 0x80764a0c +80764a14 0x80764a14 +80764a1c 0x80764a1c +80764a20 0x80764a20 +80764a24 0x80764a24 +80764a28 0x80764a28 +80764a2c 0x80764a2c +80764a30 0x80764a30 +80764a38 0x80764a38 +80764a3c 0x80764a3c +80764a8c 0x80764a8c +80764b04 0x80764b04 +80764b08 0x80764b08 +80764b48 0x80764b48 +80764b88 0x80764b88 +80764bc8 0x80764bc8 +80764c48 0x80764c48 +80764c88 0x80764c88 +80764cc8 obj_Epropeller::__ct +80764dbc 0x80764dbc +80764e34 obj_Epropeller::__dt +80764eb4 obj_Epropeller_0x0c +80765064 0x80765064 +80765068 obj_Epropeller_update +80765190 0x80765190 +807652ac 0x807652ac +807653a8 obj_Epropeller_updateEffect +80765558 0x80765558 +807655b4 0x807655b4 +80765734 0x80765734 +80765738 0x80765738 +807658e0 0x807658e0 +80765930 0x80765930 +80765a54 0x80765a54 +80765bc0 0x80765bc0 +80765bc8 0x80765bc8 +80765c18 0x80765c18 +80765c90 0x80765c90 +80765c94 0x80765c94 +80765d38 0x80765d38 +80765db0 0x80765db0 +80765e30 0x80765e30 +8076613c 0x8076613c +80766140 0x80766140 +80766144 0x80766144 +807662f8 0x807662f8 +80766754 0x80766754 +807667dc 0x807667dc +80766864 0x80766864 +807668d4 0x807668d4 +807669c8 0x807669c8 +80766a08 0x80766a08 +80766a54 0x80766a54 +80766a58 0x80766a58 +80766a98 0x80766a98 +80766ad8 0x80766ad8 +80766b18 0x80766b18 +80766b58 0x80766b58 +80766bac 0x80766bac +80766bb4 0x80766bb4 +80766bc0 0x80766bc0 +80766bcc 0x80766bcc +80766ca4 0x80766ca4 +80766cac 0x80766cac +80766cb0 0x80766cb0 +80766cb8 0x80766cb8 +80766cc4 0x80766cc4 +80766cd0 0x80766cd0 +80766cdc 0x80766cdc +80766ce8 0x80766ce8 +80766cf4 0x80766cf4 +80766d00 0x80766d00 +80766d50 0x80766d50 +80766dc8 0x80766dc8 +80766dcc 0x80766dcc +80766e0c 0x80766e0c +80766e4c 0x80766e4c +80766e8c 0x80766e8c +80766f4c Objects::Karehayama::__ct +80766f88 Objects::Karehayama::__dt +80766fe0 Objects::Karehayama::OnStart +80767038 Objects::Karehayama::LoadEffects +80767114 Objects::Karehayama::CreateCollisionEffectSoft +807671dc Objects::Karehayama::CreateCollisionEffect +8076724c Objects::Karehayama::CreateExplosionEffect +80767250 Objects::Karehayama::Respawn +807672a4 Objects::Karehayama::DropItem +8076734c Objects::Karehayama::OnNormalCollision +80767448 Objects::Karehayama::OnImperviousCollision +8076757c Objects::Karehayama::OnNormalItemCollision +80767648 Objects::Karehayama::OnImperviousItemCollision +80767704 Objects::Karehayama::LoadShadow +807677e0 Object::ItemDropper::UpdateRespawn +807677e4 Object::ItemDropper::GetPropertiesBitfield +807677ec Objects::Karehayama::LoadAnimations +80767840 Objects::Karehayama::vf_0x114 +80767848 Objects::Karehayama::OnWallCollision +8076784c Objects::Karehayama::GetBRRESName +807678a0 Objects::Karehayama::GetSubFileName +807678f4 0x807678f4 +80767d70 0x80767d70 +80767de8 0x80767de8 +80767dec 0x80767dec +80767e04 0x80767e04 +80767ff4 0x80767ff4 +807683f0 0x807683f0 +80768408 0x80768408 +80768650 0x80768650 +80768698 0x80768698 +807686b8 0x807686b8 +807686bc 0x807686bc +80768710 0x80768710 +8076871c 0x8076871c +80768728 0x80768728 +80768730 0x80768730 +80768734 0x80768734 +80768738 0x80768738 +8076873c 0x8076873c +80768740 0x80768740 +80768748 0x80768748 +80768790 0x80768790 +807687d8 0x807687d8 +807687e0 0x807687e0 +80768830 0x80768830 +807688a8 0x807688a8 +807688ac 0x807688ac +8076892c 0x8076892c +807689ac 0x807689ac +807689ec Objects::Hanabi::__ct +80768a50 Objects::Hanabi::__dt +80768a90 Objects::Hanabi::OnStart +80768ae4 0x80768ae4 +80768af8 Objects::Hanabi::Update +80768c18 0x80768c18 +80768d08 Objects::Hanabi::LoadClipInfo +80768d0c Objects::Hanabi::LoadCollision +80768d10 Objects::Hanabi::LoadAnimations +80768d14 Objects::Hanabi::LoadModels +80768d18 Objects::Hanabi::GetPropertiesBitfield +80768d20 ObjVeniceGondola_construct +80768d84 ObjVeniceGondola_destruct +80768dc4 0x80768dc4 +80768f0c 0x80768f0c +80768f10 0x80768f10 +807691f8 0x807691f8 +80769280 0x80769280 +80769308 0x80769308 +80769370 0x80769370 +807693ac 0x807693ac +807693ec 0x807693ec +8076942c 0x8076942c +807694e8 0x807694e8 +80769584 0x80769584 +80769590 0x80769590 +8076959c 0x8076959c +807695a4 0x807695a4 +80769608 0x80769608 +80769610 0x80769610 +8076961c 0x8076961c +80769628 0x80769628 +80769630 0x80769630 +80769634 0x80769634 +807696cc 0x807696cc +80769794 0x80769794 +807697d4 0x807697d4 +8076983c 0x8076983c +80769840 0x80769840 +80769980 0x80769980 +80769998 0x80769998 +80769b30 0x80769b30 +80769b90 0x80769b90 +80769f84 0x80769f84 +8076a024 0x8076a024 +8076a148 0x8076a148 +8076a1fc 0x8076a1fc +8076a2b8 0x8076a2b8 +8076a338 0x8076a338 +8076a3b0 0x8076a3b0 +8076a508 0x8076a508 +8076a560 0x8076a560 +8076a56c 0x8076a56c +8076a638 0x8076a638 +8076a824 0x8076a824 +8076a828 0x8076a828 +8076a8a8 0x8076a8a8 +8076a920 0x8076a920 +8076a9d4 0x8076a9d4 +8076a9e0 0x8076a9e0 +8076a9e4 0x8076a9e4 +8076adac 0x8076adac +8076adb0 0x8076adb0 +8076afa8 0x8076afa8 +8076b080 0x8076b080 +8076b238 0x8076b238 +8076b304 0x8076b304 +8076b4c0 0x8076b4c0 +8076b554 0x8076b554 +8076b990 0x8076b990 +8076b994 0x8076b994 +8076ba0c 0x8076ba0c +8076ba84 0x8076ba84 +8076ba88 0x8076ba88 +8076bb44 0x8076bb44 +8076bb94 0x8076bb94 +8076bdf4 0x8076bdf4 +8076c038 0x8076c038 +8076c154 0x8076c154 +8076c1f4 0x8076c1f4 +8076c328 0x8076c328 +8076c420 0x8076c420 +8076c424 0x8076c424 +8076c428 0x8076c428 +8076c42c 0x8076c42c +8076c430 0x8076c430 +8076c438 0x8076c438 +8076c43c 0x8076c43c +8076c440 0x8076c440 +8076c444 0x8076c444 +8076c448 0x8076c448 +8076c450 0x8076c450 +8076c458 0x8076c458 +8076c4a8 0x8076c4a8 +8076c520 0x8076c520 +8076c524 0x8076c524 +8076c574 0x8076c574 +8076c5ec 0x8076c5ec +8076c5f0 0x8076c5f0 +8076c640 0x8076c640 +8076c6b8 0x8076c6b8 +8076c6bc 0x8076c6bc +8076c774 0x8076c774 +8076c858 0x8076c858 +8076c85c Objects::W_Itembox::__ct +8076c9c8 Objects::SubObjectArray::__dt +8076ca40 Objects::SubObjectArray::vf_0x18 +8076ca44 Objects::W_Itembox::__dt +8076cac4 Objects::W_Itembox::OnStart +8076cafc 0x8076cafc +8076cb08 Objects::W_Itembox::Update +8076cbf8 0x8076cbf8 +8076ccc8 Objects::W_Itembox::Box::__ct +8076cd04 Objects::W_Itembox::Box::__dt +8076cd44 Objects::W_Itembox::Box::OnStart +8076cd54 0x8076cd54 +8076cdb8 Objects::W_Itembox::Box::Update +8076ceb0 0x8076ceb0 +8076cefc Objects::W_Itembox::Box::OnCollision +8076cf34 Objects::W_Itembox::Box::OnItemCollision +8076cf3c Objects::W_Itembox::UpdateModel +8076cf40 Objects::W_Itembox::LoadClipInfo +8076cf44 Objects::W_Itembox::LoadCollision +8076cf48 Objects::W_Itembox::LoadAnimations +8076cf4c Objects::W_Itembox::LoadModels +8076cf50 Objects::W_Itembox::GetPropertiesBitfield +8076cf58 Objects::W_Itembox::Box::GetSubFileName +8076cf64 Objects::W_Itembox::Box::GetBRRESName +8076cf70 Objects::W_Itembox::Box::GetPropertiesBitfield +8076cf78 Objects::SubObjectArray::CreateArray +8076cfc8 Objects::SubObjectArray::CreateArray(heap) +8076d040 Objects::SubObjectArray::vf_0x14 +8076d044 Objects::W_Itemboxline::__ct +8076d460 Objects::SubObjectArray::__dt +8076d4d8 Objects::SubObjectArray::__dt +8076d550 Objects::SubObjectArray::vf_0x18 +8076d554 Objects::SubObjectArray::vf_0x18 +8076d558 Objects::W_Itemboxline::__dt +8076d604 Objects::W_Itemboxline::OnStart +8076d640 0x8076d640 +8076d64c Objects::W_Itemboxline::Update +8076d828 0x8076d828 +8076d9e4 Objects::W_Itemboxline::Box::GetID +8076da48 Objects::W_Itemboxline::Box::__dt +8076da88 Objects::W_Itemboxline::Box::OnStart +8076da98 0x8076da98 +8076daf4 Objects::W_Itemboxline::Box::Update +8076df44 0x8076df44 +8076e0bc Objects::W_Itemboxline::Box::OnCollision +8076e0f4 0x8076e0f4 +8076e164 Objects::W_Itemboxline::Block::__dt +8076e1a4 Objects::W_Itemboxline::Block::OnStart +8076e210 0x8076e210 +8076e2c0 Objects::W_Itemboxline::Block::Update +8076e400 0x8076e400 +8076e4e8 Objects::W_Itemboxline::Block::OnCollision +8076e5d8 Objects::W_Itemboxline::Block::OnItemCollision +8076e69c 0x8076e69c +8076e7ac Objects::W_Itemboxline::Press::__ct +8076e818 Objects::W_Itemboxline::Press::__dt +8076e870 Objects::W_Itemboxline::Press::TryFall +8076e8b0 Objects::W_Itemboxline::Press::UpdateEffect +8076e9a8 Objects::W_Itemboxline::UpdateModel +8076e9ac Objects::W_Itemboxline::LoadClipInfo +8076e9b0 Objects::W_Itemboxline::LoadCollision +8076e9b4 Objects::W_Itemboxline::LoadAnimations +8076e9b8 Objects::W_Itemboxline::LoadModels +8076e9bc Objects::W_Itemboxline::GetPropertiesBitfield +8076e9c4 0x8076e9c4 +8076e9cc 0x8076e9cc +8076e9d8 0x8076e9d8 +8076e9e4 Objects::W_Itemboxline::Box::GetPropertiesBitfield +8076e9ec Objects::W_Itemboxline::Block::GetPropertiesBitfield +8076e9f4 Objects::W_Itemboxline::Block::GetBRRESName +8076ea00 Objects::W_Itemboxline::Block::GetSubFileName +8076ea0c Objects::Press::GetCollisionDiameter +8076ea18 Objects::Press::GetShadowListIdx +8076ea20 Objects::W_Itemboxline::Press::GetPropertiesBitfield +8076ea28 Objects::W_Itemboxline::Press::GetID +8076ea30 Objects::W_Itemboxline::Press::GetBRRESName +8076ea3c Objects::W_Itemboxline::Press::GetSubFileName +8076ea48 Objects::SubObjectArray::CreateArray +8076ea98 Objects::SubObjectArray::CreateArray(heap) +8076eb10 Objects::SubObjectArray::vf_0x14 +8076eb14 Objects::SubObjectArray::CreateArray +8076eb64 Objects::SubObjectArray::CreateArray(heap) +8076ebdc Objects::SubObjectArray::vf_0x14 +8076ebe0 Objects::ItemDropper::__ct +8076ec28 Objects::ItemDropper::__dt +8076ec68 Objects::ItemDropper::OnStart +8076ed18 Objects::ItemDropper::LoadEffects +8076ed1c Objects::ItemDropper::Update +8076ed70 Objects::ItemDropper::RespawnImpl +8076edd4 Objects::ItemDropper::Respawn +8076ede4 Objects::ItemDropper::DropItem +8076efdc Objects::ItemDropper::CreateExplosionEffect +8076f0ac Objects::ItemDropper::OnCollision +8076f1d4 Objects::ItemDropper::OnNormalCollision +8076f1d8 Objects::ItemDropper::OnImperviousCollision +8076f1dc Objects::ItemDropper::OnItemCollision +8076f2d8 Objects::ItemDropper::OnNormalItemCollision +8076f2dc Objects::ItemDropper::OnImperviousItemCollision +8076f2e0 Objects::DKRockGC::__ct +8076f344 Objects::DKRockGC::__dt +8076f384 Objects::DKRockGC::OnStart +8076f58c 0x8076f58c +8076f590 Objects::DKRockGC::Update +8076f768 0x8076f768 +8076f868 0x8076f868 +8076f91c 0x8076f91c +8076fa60 0x8076fa60 +8076fd90 0x8076fd90 +8076ffc0 0x8076ffc0 +80770068 Objects::DKRockGC::OnCollision +807701f8 Objects::DKRockGC::OnItemCollision +8077028c Objects::DKRockGC::UpdateShadow +8077036c Objects::DKRockGC::GetCollisionTranslation +80770374 Objects::DKRockGC::GetShadowListIdx +8077037c Objects::DKRockGC::GetPropertiesBitfield +80770384 Objects::KoopaBall::__ct +807703d0 Objects::KoopaBall::OnStart +8077079c 0x8077079c +807707ac 0x807707ac +80770924 0x80770924 +80770a64 0x80770a64 +80770adc Objects::KoopaBall::Update +80770f4c 0x80770f4c +80771248 0x80771248 +80771324 0x80771324 +8077151c 0x8077151c +80771624 0x80771624 +80771774 0x80771774 +807718d0 0x807718d0 +807719a0 0x807719a0 +807719e8 0x807719e8 +80771b50 0x80771b50 +80771bcc 0x80771bcc +80771bf4 Objects::KoopaBall::OnCollision +80771e24 Objects::KoopaBall::UpdateShadow +80771f60 Objects::KoopaBall::GetCollisionTranslation +80771f68 Objects::KoopaBall::GetPropertiesBitfield +80771f70 Objects::KoopaBall::__dt +80771fb0 0x80771fb0 +80772054 0x80772054 +807720ec 0x807720ec +8077213c 0x8077213c +8077231c 0x8077231c +80772448 0x80772448 +80772504 0x80772504 +80772530 0x80772530 +80772650 0x80772650 +807726b4 0x807726b4 +807726bc 0x807726bc +807726c4 0x807726c4 +80772704 0x80772704 +80772744 0x80772744 +8077275c 0x8077275c +80772b1c 0x80772b1c +80772c78 0x80772c78 +80772c7c 0x80772c7c +80772cbc 0x80772cbc +80772cfc 0x80772cfc +8077308c 0x8077308c +80773094 0x80773094 +807730d4 Objects::MiiObj::__ct +80773114 Objects::MiiObj::__dt +80773180 Objects::MiiObj::OnStart +8077318c Objects::MiiObj::LoadModels +807736c0 Objects::MiiObj::GetPropertiesBitfield +807736c8 Objects::MiiPoster::__ct +80773708 Objects::MiiPoster::__dt +80773714 Objects::MiiPoster::OnStart +807738e8 Objects::MiiPoster::LoadModels +807738f0 Objects::MiiPoster::GetPropertiesBitfield +80773930 Objects::MiiKanban::__ct +80773970 Objects::MiiKanban::LoadModels +80773c08 Objects::MiiKanban::OnStart +80773c14 Objects::MiiKanban::GetPropertiesBitfield +80773c1c Objects::MiiKanban::__dt +80773c5c ObjMonteA_construct +80773d00 0x80773d00 +80773dac 0x80773dac +80774044 0x80774044 +807740ec 0x807740ec +80774208 0x80774208 +807742f8 0x807742f8 +8077439c 0x8077439c +807743a4 0x807743a4 +807743e4 0x807743e4 +80774750 0x80774750 +80774754 0x80774754 +80774a00 0x80774a00 +80774a84 0x80774a84 +80774c1c 0x80774c1c +80774c80 0x80774c80 +80774cb0 0x80774cb0 +80774eb8 0x80774eb8 +80774f20 0x80774f20 +8077502c 0x8077502c +80775108 0x80775108 +807753b4 0x807753b4 +80775458 0x80775458 +80775464 0x80775464 +807754ec 0x807754ec +807754f4 0x807754f4 +807754fc 0x807754fc +8077553c 0x8077553c +8077557c 0x8077557c +80775614 0x80775614 +8077561c 0x8077561c +80775624 0x80775624 +80775670 Objects::Penguin::__dt +807756b0 Objects::Penguin::OnStart +80775758 0x80775758 +80775764 Objects::Penguin::Update +807757a0 Objects::Penguin::UpdateWithRot +80775804 Objects::Penguin::UpdateNoRot +8077588c Objects::Penguin::vf_0x104 +80775950 Objects::Penguin::UpdateMtxNoRot +80775b1c Objects::Penguin::UpdateMtxWithRot +80775ba4 0x80775ba4 +80775c2c Objects::Penguin::UpdatePosition +80775c58 Objects::Penguin::OnCollision +80775ce0 Objects::Penguin::OnItemCollision +80775d2c Objects::Penguin::SpinOut +80775e60 Objects::Penguin_s::__ct +80775fc4 Objects::PenguinUnkObj::LoadAnimations +80776070 Objects::Penguin_s::__dt +807760b0 Objects::Penguin_s::OnStart +80776188 0x80776188 +80776198 Objects::Penguin_s::Update +8077637c Objects::Penguin_s::UpdateWithRot +807763d0 0x807763d0 +80776498 0x80776498 +80776670 0x80776670 +807766e8 Objects::Penguin_s::UpdateNoRot +807768a0 0x807768a0 +80776af0 Objects::Penguin_s::vf_0x104 +80776b7c 0x80776b7c +80776b94 0x80776b94 +80776c58 0x80776c58 +80776cdc 0x80776cdc +80776d90 Objects::Penguin_s::SpinOut +80776ecc Objects::Penguin_s::UpdateModelMatrix +8077704c 0x8077704c +8077708c Objects::Penguin_m::__ct +80777130 Objects::Penguin_l::__ct +807771dc Objects::Penguin_l::OnStart +80777284 Objects::Penguin_l::UpdateModelMatrix +80777288 0x80777288 +807772c8 Objects::Penguin::LoadAnimations +8077731c Objects::Penguin::vf_0xec +80777324 Objects::Penguin::GetPropertiesBitfield +8077732c Objects::Penguin_l::DoCalcShadowMtx +80777334 0x80777334 +80777388 0x80777388 +80777390 0x80777390 +80777394 0x80777394 +8077739c Objects::Penguin_s::LoadAnimations +80777448 Objects::Penguin_s::vf_0xec +80777450 Objects::PenguinUnkObj::vf_0xec +80777458 Objects::PenguinUnkObj::Update +8077745c Objects::PenguinUnkObj::GetScnObjDrawOptionsIdx +80777464 Objects::PenguinUnkObj::__dt +807774a4 Objects::Penguin_m::_dt +807774e4 0x807774e4 +80777524 Objects::Penguin_l::__dt +80777564 Objects::Press::__ct +807775fc Objects::Press::OnStart +8077763c 0x8077763c +8077786c 0x8077786c +8077788c Objects::Press::Update +80777a90 Objects::Press::TryFall +80777b54 0x80777b54 +80777b90 0x80777b90 +80777bc0 0x80777bc0 +80777cb0 0x80777cb0 +80777d10 Objects::Press::UpdateColliding +80777ef8 Objects::Press::UpdateEffect +80777fc8 0x80777fc8 +80778080 0x80778080 +8077808c 0x8077808c +80778140 0x80778140 +8077814c 0x8077814c +80778168 0x80778168 +80778218 0x80778218 +807782d4 Objects::Press::OnCollision +8077840c Objects::Press::LoadCollision +807786e4 Objects::Press::LoadAnimations +807787e8 Objects::Press::GetPropertiesBitfield +807787f0 Objects::Dokan_sfc::__ct +80778830 Objects::Dokan_sfc::OnStart +807788c8 Objects::Dokan_sfc::Update +8077894c 0x8077894c +807789bc 0x807789bc +80778ba0 0x80778ba0 +80778c0c Objects::Dokan_sfc::OnCollision +80778cc4 Objects::Dokan_sfc::OnItemCollision +80778d50 Objects::Dokan_sfc::UpdateCollision +80778ea8 Objects::Dokan_sfc::UpdateShadow +80778f5c Objects::Dokan_sfc::LoadAnimations +80778fdc Objects::Dokan_sfc::vf_0xec +80778fe4 Objects::Dokan_sfc::GetPropertiesBitfield +80778fec Objects::Dokan_sfc::__dt +8077902c 0x8077902c +8077906c 0x8077906c +80779170 0x80779170 +80779174 0x80779174 +8077921c 0x8077921c +80779390 0x80779390 +80779400 0x80779400 +807795a4 0x807795a4 +8077962c 0x8077962c +807796b4 0x807796b4 +8077971c 0x8077971c +80779a68 0x80779a68 +80779d5c 0x80779d5c +80779e68 0x80779e68 +80779eec 0x80779eec +80779ef4 0x80779ef4 +80779efc 0x80779efc +80779f3c 0x80779f3c +8077a1a8 0x8077a1a8 +8077a1e8 0x8077a1e8 +8077a35c nlListContainer__ct +8077a36c 0x8077a36c +8077a5f8 0x8077a5f8 +8077a6d4 0x8077a6d4 +8077a8b8 0x8077a8b8 +8077aa78 0x8077aa78 +8077ab98 0x8077ab98 +8077acbc 0x8077acbc +8077ace0 0x8077ace0 +8077ad60 0x8077ad60 +8077b248 0x8077b248 +8077b358 0x8077b358 +8077b394 0x8077b394 +8077b3d4 0x8077b3d4 +8077b450 0x8077b450 +8077b5bc 0x8077b5bc +8077ba18 0x8077ba18 +8077ba80 0x8077ba80 +8077bb98 0x8077bb98 +8077bbe0 0x8077bbe0 +8077bc20 0x8077bc20 +8077bc68 0x8077bc68 +8077bca8 0x8077bca8 +8077bd30 0x8077bd30 +8077bd38 0x8077bd38 +8077bd40 0x8077bd40 +8077bd44 0x8077bd44 +8077bd50 0x8077bd50 +8077bd5c 0x8077bd5c +8077bd68 0x8077bd68 +8077bd74 0x8077bd74 +8077bd80 0x8077bd80 +8077bf44 0x8077bf44 +8077bfbc 0x8077bfbc +8077bfc0 0x8077bfc0 +8077bfc4 0x8077bfc4 +8077bfc8 0x8077bfc8 +8077c1d0 0x8077c1d0 +8077c220 0x8077c220 +8077c2ec 0x8077c2ec +8077c2f0 0x8077c2f0 +8077c2f4 0x8077c2f4 +8077c344 0x8077c344 +8077c384 0x8077c384 +8077c500 0x8077c500 +8077c504 0x8077c504 +8077c57c 0x8077c57c +8077c580 0x8077c580 +8077c5e0 0x8077c5e0 +8077c7ec 0x8077c7ec +8077c7f0 0x8077c7f0 +8077c8f4 0x8077c8f4 +8077cb54 0x8077cb54 +8077cbb4 0x8077cbb4 +8077cc78 0x8077cc78 +8077cccc 0x8077cccc +8077ccd4 0x8077ccd4 +8077ccdc 0x8077ccdc +8077cce0 0x8077cce0 +8077cce4 0x8077cce4 +8077cce8 0x8077cce8 +8077ccec 0x8077ccec +8077ccf0 0x8077ccf0 +8077ccf4 0x8077ccf4 +8077ccfc 0x8077ccfc +8077cd4c 0x8077cd4c +8077cdc4 0x8077cdc4 +8077cdc8 0x8077cdc8 +8077ce48 0x8077ce48 +8077ce88 0x8077ce88 +8077cec8 Objects::Sin_ItemBox::__ct +8077cf04 Objects::Sin_ItemBox::__dt +8077cf44 Objects::Sin_ItemBox::OnStart +8077d024 Objects::Sin_ItemBox::Update +8077d168 0x8077d168 +8077d274 Objects::Sin_ItemBox::GetPropertiesBitfield +8077d27c obj_inseki::__ct +8077d34c 0x8077d34c +8077d38c 0x8077d38c +8077d3d4 obj_inseki_update +8077d4dc 0x8077d4dc +8077d4e4 0x8077d4e4 +8077d548 0x8077d548 +8077d588 0x8077d588 +8077d644 0x8077d644 +8077d66c 0x8077d66c +8077d7d0 0x8077d7d0 +8077d834 0x8077d834 +8077d940 0x8077d940 +8077d9b0 0x8077d9b0 +8077d9c4 0x8077d9c4 +8077d9c8 0x8077d9c8 +8077d9cc 0x8077d9cc +8077d9d0 0x8077d9d0 +8077d9d8 Objects::Fall::__ct +8077dd10 Objects::Fall::__dt +8077dd50 Objects::Fall::OnStart +8077dd54 Objects::Fall::Update +8077de38 0x8077de38 +8077df1c Objects::Fall::GetPropertiesBitfield +8077df24 Objects::W_Woodbox::__ct +8077e0a4 Objects::SubObjectArray::__dt +8077e11c Objects::SubObjectArray::vf_0x18 +8077e120 Objects::W_Woodbox::__dt +8077e1a0 Objects::W_Woodbox::OnStart +8077e1d8 0x8077e1d8 +8077e1e4 Objects::W_Woodbox::Update +8077e2a8 0x8077e2a8 +8077e34c Objects::W_Woodbox::Box::__ct +8077e388 Objects::W_Woodbox::Box::__dt +8077e3e4 Objects::W_Woodbox::Box::OnStart +8077e444 Objects::W_Woodbox::Box::RespawnImpl +8077e49c Objects::W_Woodbox::Box::Update +8077e56c 0x8077e56c +8077e5e4 Objects::Woodbox::__ct +8077e620 Objects::Woodbox::__dt +8077e678 Objects::Woodbox::OnStart +8077e6b8 Objects::Woodbox::LoadEffects +8077e704 Objects::Woodbox::DropItem +8077e750 Objects::Woodbox::Respawn +8077e7b0 Objects::Woodbox::UpdateRespawn +8077e7e0 Objects::Woodbox::UpdateRespawnImpl +8077e928 Objects::Woodbox::OnNormalCollision +8077e9c0 Objects::Woodbox::OnImperviousCollision +8077ea6c Objects::Woodbox::OnNormalItemCollision +8077eb08 Objects::Woodbox::OnImperviousItemCollision +8077ebb8 Objects::Woodbox::UpdateCollision +8077ecc8 Objects::W_Woodbox::UpdateModel +8077eccc Objects::W_Woodbox::LoadClipInfo +8077ecd0 Objects::W_Woodbox::LoadCollision +8077ecd4 Objects::W_Woodbox::LoadAnimations +8077ecd8 Objects::W_Woodbox::LoadModels +8077ecdc Objects::W_Woodbox::GetPropertiesBitfield +8077ece4 Objects::Woodbox::InitEntity +8077ed70 Objects::Woodbox::GetBRRESName +8077ed7c Objects::Woodbox::GetSubFileName +8077eda4 Objects::W_Woodbox::Box::GetPropertiesBitfield +8077edac Objects::Woodbox::GetPropertiesBitfield +8077edb4 Objects::SubObjectArray::CreateArray +8077ee04 Objects::SubObjectArray::CreateArray(Heap) +8077ee7c Objects::SubObjectArray::vf_0x14 +8077ee80 MiiRenderMgr::Disposer__dt +8077ef84 MiiRenderMgr::CreateInstance +8077f010 MiiRenderMgr::DestroyInstance +8077f0c8 MiiRenderMgr::__ct +8077f128 0x8077f128 +8077f168 MiiRenderMgr::__dt +8077f22c MiiRenderMgr::Init2 +8077f570 MiiRenderMgr::Init +8077f6e4 MiiRenderMgr::CreateTextures +8077f82c 0x8077f82c +8077f8c8 MiiRenderMgr::SetPercentages +8077f8d4 MiiRenderMgr::InitMenuModels +8077fc3c 0x8077fc3c +80780100 MiiRenderMgr::InitRaceModels +807803bc 0x807803bc +8078041c 0x8078041c +807805d0 0x807805d0 +80780714 0x80780714 +80780aa0 0x80780aa0 +80780ab8 0x80780ab8 +80780be0 0x80780be0 +80780cb0 0x80780cb0 +80780d88 0x80780d88 +80780ed0 0x80780ed0 +80780f0c MiiRenderMgr::InitLights +80781128 0x80781128 +80781290 MiiRenderMgr::AddMii +80781474 MiiRenderMgr::CreateMiisAndRender +80781584 MiiRenderMgr::CreateAndRenderFromMiis +80781694 MiiRenderMgr::CreateAndDraw +80781bc8 0x80781bc8 +80781f6c 0x80781f6c +80782014 MiiRenderMgr::SetMiiForModel +80782100 MiiRenderMgr::UpdateScreenFromTexture +807822b8 0x807822b8 +8078235c MiiRenderMgr::RequestAddMii +807823e4 MiiRenderMgr::ProcessTopAddRequest +80782498 MiiRenderMgr::DeleteMii +80782528 MiiRenderMgr::ProcessAllAddRequests +807825dc 0x807825dc +80782668 0x80782668 +80782804 MiiRenderMgr::CalcWorldCB::ExecCallbackB +807828b0 MiiRenderMgr::CalcWorldCB::__dt +807828f0 0x807828f0 +807829c8 MiiRenderMgr::CalcWorldCB::ExecCallbackB(thunk) +807829d0 MiiRenderMgr::CalcWorldCB::__dt +807829d8 0x807829d8 +807829e8 0x807829e8 +80782a28 0x80782a28 +80782a2c 0x80782a2c +80782b08 0x80782b08 +80782c14 0x80782c14 +80782cd8 0x80782cd8 +80782d74 0x80782d74 +80782e50 0x80782e50 +80782f74 Object::GetRaceinfoRandom +80782fa8 0x80782fa8 +80783050 0x80783050 +807830f8 0x807830f8 +80783150 0x80783150 +807831c4 0x807831c4 +80783254 0x80783254 +80783374 RaceEndTallies::Disposer__dt +807834a0 RaceEndTallies::CreateInstance +80783548 RaceEndTallies::DestroyInstance +807835fc RaceEndTallies::__dt +807836a0 RaceEndTallies::__ct +8078371c RaceEndTallies::Init +807838a4 RaceEndTallies::Init_switch +807838a8 RaceEndTallies::Init_caseD_0 +807838b4 RaceEndTallies::Init_caseD_3 +807838bc RaceEndTallies::Init_caseD_4 +80783a08 RaceEndTallies::UpdateLicenseTallies +80783d14 RaceEndTallies::Reset +80783d58 RaceEndTallies::SetVSBattleBool +80783db0 RaceEndTallies::SetTotalDistance +80783dc8 RaceEndTallies::SetTotalDistanceVS +80783e24 RaceEndTallies::SetCharacterAndKart +80783ea8 RaceEndTallies::SetCourseId +80783ee0 0x80783ee0 +80783eec 0x80783eec +80783ef4 0x80783ef4 +80783ef8 RaceEndTallies::SetWheelBool +80783fe0 RaceEndTallies::SetItemsTricksCounts +80783ffc 0x80783ffc +80784050 0x80784050 +807840a8 0x807840a8 +80784250 0x80784250 +80784360 0x80784360 +807845c8 0x807845c8 +80784640 0x80784640 +807846b8 0x807846b8 +807846bc 0x807846bc +807846c0 0x807846c0 +807849bc 0x807849bc +80784c58 0x80784c58 +80784f94 0x80784f94 +80785124 0x80785124 +807851bc 0x807851bc +80785254 0x80785254 +807852d8 0x807852d8 +8078534c 0x8078534c +8078539c 0x8078539c +80785414 0x80785414 +80785418 0x80785418 +80785468 0x80785468 +807854e0 0x807854e0 +807854e4 EntityManager::__dt +807855dc EntityManager::CreateInstance +8078562c EntityManager::DestroyInstance +807856e0 EntityManager::__ct +807858d8 0x807858d8 +8078597c 0x8078597c +807859b0 EntityManager::Update +80785d8c EntityManager::GetNextKartPlayer +80785df4 EntityManager::GetNextObj +80785e5c EntityManager::GetNextObjectCollidable +80785ec4 EntityManager::GetNextObjectKCL +80785f2c EntityManager::UpdateIndexes +80786050 EntityManager::InsertKartPlayer +80786064 EntityManager::InsertItemEntity +80786078 EntityManager::InsertObjectCollidable +80786120 EntityManager::InsertObjectKCL +80786134 EntityManager::InsertEntity +80786578 EntityManager::RemoveEntity +80786774 EntityManager::PrepareFromEntity +807868c0 EntityManager::FindEntitiesCloseToEntity +80786b14 EntityManager::PrepareFromSphere +80786c60 EntityManager::FindEntitiesCloseToSphere +80786dbc 0x80786dbc +80786e60 EntityManager::IsPrepared +80786ed0 Entity::__ct +80786ef4 Entity::__dt +80786f34 Entity::Init +80786f6c Entity::MakeDynamic +80786f7c Entity::Resize +80786f98 AreaMasker_ +80786fa8 EntityManager::Prepare +80786fc0 ClipInfoMgr::GetArea8And9GroupIDs +8078707c ClipInfoMgr::UpdateScreenInfo +807872c0 ClipInfoMgr::NormalizeVector +80787340 ClipInfoMgr::Disposer__dt +8078746c ClipInfoMgr::CreateInstance +80787538 ClipInfoMgr::DestroyInstance +807875ec ClipInfoMgr::__ct +8078768c ClipScreenInfo::__ct +80787690 ClipScreenInfo::__dt +807876d0 ClipInfoMgr::__dt +80787774 ClipInfoMgr::Update +80787ab8 ClipInfoMgr::Insert +80787b88 0x80787b88 +80787ba0 ClipInfoMgr::CalcAreaType8Refs +80787bd8 ClipInfo::__ct +80787be4 ClipInfo::__dt +80787c24 ClipInfo::Init +80787d04 0x80787d04 +80787d18 0x80787d18 +80787d50 0x80787d50 +80787d84 AwardsMgr::Disposer__dt +80787eb0 AwardsMgr::CreateInstance +80787fe0 AwardsMgr::DestroyInstance +80788094 AwardsMgr::__ct +80788198 AwardsMgr::__dt +8078823c AwardsMgr::Init +80789008 AwardsMgr::LoadStand +80789340 AwardsMgr::LoadPlayers +80789b94 0x80789b94 +80789fdc 0x80789fdc +8078a1d4 0x8078a1d4 +8078a1d8 AwardsMgr::SetParams +8078a298 0x8078a298 +8078a2bc 0x8078a2bc +8078a728 0x8078a728 +8078a768 0x8078a768 +8078a904 0x8078a904 +8078aae0 0x8078aae0 +8078acb8 0x8078acb8 +8078ae84 0x8078ae84 +8078b2d4 0x8078b2d4 +8078b540 0x8078b540 +8078b5d0 IKParams::__ct +8078b7ac IKParams::Load +8078c3e0 0x8078c3e0 +8078c960 DriverMgr::Disposer__dt +8078c9e8 DriverMgr::CreateInstance +8078ca38 DriverMgr::DestroyInstance +8078ca5c DriverMgr::__ct +8078ce24 DriverMgr::__dt +8078cf38 0x8078cf38 +8078cf4c DriverMgr::SetController +8078cf5c 0x8078cf5c +8078cfa4 0x8078cfa4 +8078d1a0 0x8078d1a0 +8078d378 0x8078d378 +8078d44c 0x8078d44c +8078d4c0 0x8078d4c0 +8078d4cc 0x8078d4cc +8078d614 0x8078d614 +8078d61c 0x8078d61c +8078d784 0x8078d784 +8078d7e8 DriverMgr::InitAwards +8078d824 DriverMgr::Update +8078d980 0x8078d980 +8078d9e8 0x8078d9e8 +8078da90 0x8078da90 +8078db10 0x8078db10 +8078dc6c 0x8078dc6c +8078ddb4 0x8078ddb4 +8078dde4 DriverMgr::ChangeFocusedPlayer +8078ddf4 0x8078ddf4 +8078de04 MHModelDirector::__ct +8078de60 MHModelDirector::SetMatsTEVColors +8078dfe8 CourseMgr::CreateInstance +8078e124 CourseMgr::DestroyInstance +8078e1b0 CourseMgr::LoadModels +8078e33c 0x8078e33c +8078e454 0x8078e454 +8078e4e8 CourseMgr::SetSoftWallInfo +8078e4f0 CourseMgr::UpdateKCL +8078e588 0x8078e588 +8078e694 0x8078e694 +8078e7c4 0x8078e7c4 +8078e8ec 0x8078e8ec +8078e8f0 0x8078e8f0 +8078ea98 0x8078ea98 +8078eb00 0x8078eb00 +8078eca8 0x8078eca8 +8078eef4 0x8078eef4 +8078f140 CourseMgr::IsCollidingNoTerrainInfoNoTriangleCheck +8078f320 CourseMgr::IsCollidingAddEntryNoTerrainInfoNoTriangleCheck +8078f500 CourseMgr::IsColliding +8078f784 CourseMgr::IsCollidingAddEntryNoTriangleCheck +8078fa08 0x8078fa08 +8078fbb0 0x8078fbb0 +8078fd58 0x8078fd58 +8078ffa4 0x8078ffa4 +807901f0 CourseMgr::IsCollidingNoTerrainInfo +807903bc CourseMgr::IsCollidingAddEntryNoTerrainInfo +80790588 0x80790588 +807907f8 CourseMgr::IsCollidingAddEntry +80790a68 0x80790a68 +80790ac0 _sinit_CourseMgr_cpp +80790ad0 MHModelDirector::__dt(thunk) +80790ad8 Item::LoadObjProperties +80790afc Item::InitAllObjProperties +80790e1c Item::IsTT +80790e28 Item::IsAwards +80790e30 Item::GetDriverMgr +80790e3c Item::GetKMPObjectsCount +80790e60 Item::GetGOBJHolder +80790e6c Item::GetKMPObjectId +80790e7c Item::ObjProperties::__ct(Copy) +80790fb8 Item::ObjProperties::ComputeCapacities +8079126c 0x8079126c +807912e0 0x807912e0 +80791358 0x80791358 +807913d0 0x807913d0 +80791448 0x80791448 +807914c0 0x807914c0 +80791534 0x80791534 +807915a8 0x807915a8 +80791644 0x80791644 +80791760 Item::PlayerObj::__ct +80791800 0x80791800 +80791840 Item::PlayerObj::Init +80791910 Item::PlayerObj::UseItem +80791c08 0x80791c08 +80791cc8 0x80791cc8 +80791de8 0x80791de8 +80791df8 0x80791df8 +80791e5c Item::PlayerObj::UpdateParams +80792140 0x80792140 +807921f0 0x807921f0 +8079231c Item::PlayerObj::UpdateTetheredItems +80792520 0x80792520 +80792618 0x80792618 +80792b08 Item::PlayerObj::TrailPrepare +80792c68 Item::PlayerObj::TrailUpdate +80792fc8 0x80792fc8 +80793030 0x80793030 +80793164 0x80793164 +807931dc Item::PlayerObj::TripleTrailPrepare +8079356c Item::PlayerObj::TripleTrailUpdate +80793f04 0x80793f04 +80793f74 0x80793f74 +807941f0 Item::PlayerObj::SpinCalc +8079455c Item::PlayerObj::SpinPrepare +80794694 Item::PlayerObj::SpinUpdate +80794a64 0x80794a64 +80794ab0 0x80794ab0 +80794b78 0x80794b78 +80794bac 0x80794bac +80794c44 0x80794c44 +80794d1c 0x80794d1c +80794e88 Item::PlayerObj::CalcInitialObjSpeedAndDir +80795030 0x80795030 +807951ac 0x807951ac +807952dc 0x807952dc +80795350 Item::PlayerObj::LoseItem +807954d8 0x807954d8 +807955c4 Item::PlayerObj::ComputeWheelPosRelativeToKart +8079565c 0x8079565c +80795668 Item::PlayerObj::UpdateRemote +80795848 0x80795848 +80795b60 0x80795b60 +80795c40 0x80795c40 +80795ca0 0x80795ca0 +80795cc8 Item::ObjHolder::__ct +80795ccc Item::ObjHolder::Init +80795d94 Item::ObjHolder::HideAll +80795e04 Item::ObjHolder::Spawn +80795f28 0x80795f28 +80795f68 Item::ObjHolder::OnObjKillFinish +807961f0 0x807961f0 +80796348 Item::ObjHolder::SortObjs +80796470 Item::ObjHolder::Update +8079698c 0x8079698c +807969a8 0x807969a8 +80796a48 0x80796a48 +80796b30 Item::ObjHolder::UpdateModelPositions +80796bbc Item::ObjHolder::OnFinishThrow +80796c38 0x80796c38 +80796ca0 Item::ObjHolder::ProcessRecvEVENT +80796f70 Item::ObjHolder::ProcessRecvShootEVENT +80797140 Item::ObjHolder::ProcessRecvHitFreeEVENT +807972b8 0x807972b8 +80797424 0x80797424 +807974ac Item::ObjHolder::GetTotalItemCount +80797524 0x80797524 +8079754c Item::Player::__ct +8079759c 0x8079759c +807975dc 0x807975dc +8079761c 0x8079761c +8079765c Item::PlayerObj::__dt +807976e0 Item::Player::Init +80797890 0x80797890 +80797928 Item::Player::Update +807981e4 0x807981e4 +8079820c 0x8079820c +8079824c 0x8079824c +80798290 0x80798290 +807984dc 0x807984dc +80798584 0x80798584 +8079864c Item::Player::ActivateMushroom +80798684 Item::Player::ActivateStar +807986b4 Item::Player::ActivateMega +807986e4 Item::Player::ActivateBullet +8079870c 0x8079870c +80798710 0x80798710 +80798728 0x80798728 +807987ec 0x807987ec +80798848 0x80798848 +807988f0 0x807988f0 +80798928 Item::Player::OnObjTetherEnd +8079898c 0x8079898c +80798a0c 0x80798a0c +80798a30 Item::Player::LoseItemFromDmg +80798b28 Item::Player::LoseTetheredItem +80798b8c 0x80798b8c +80798c38 Item::Player::DecideItem +80798d04 0x80798d04 +80798d0c 0x80798d0c +80798db4 0x80798db4 +80798dbc Item::Player::HasTripleItem +80798e40 0x80798e40 +80798f0c Item::Player::GetHeldCount +80798f84 0x80798f84 +80798f90 0x80798f90 +80798f9c Item::Manager::Disposer__dt +80799138 Item::Manager::CreateInstance +80799188 Item::Manager::DestroyInstance +807992d8 Item::Manager::__ct +807994dc Item::ObjHolder::__dt +8079951c Item::Player::__dt +807995b0 0x807995b0 +807995f0 0x807995f0 +80799630 0x80799630 +80799670 Item::Manager::__dt +80799794 Item::Manager::Init +80799850 Item::Manager::Update +807999a0 Item::Manager::UpdateAllObjModelPos +807999f8 Item::Manager::UpdateObjHolders +80799a50 Item::Manager::UpdateTotalItemCountMinus30 +80799acc Item::Manager::UpdatePlayers +80799b2c 0x80799b2c +80799be8 Item::Manager::IsThereCapacityForItem +80799c70 0x80799c70 +80799cac Item::Manager::CheckKartCollision +80799e50 ItemManager::InitItemDirect +80799fbc Item::Manager::CreateItemDirect +8079a12c Item::Manager::GenerateItemDirect +8079a298 Item::Manager::SpawnCloudForLocal +8079a3b8 Item::Manager::SpawnCloudForRemote +8079a498 0x8079a498 +8079a578 0x8079a578 +8079a5f4 Item::Manager::DoSomethingWithFIBObjTevColors +8079a654 0x8079a654 +8079a65c Item::Manager::GetHeldCount +8079a6d0 0x8079a6d0 +8079a778 Item::EVENTBuffer::Disposer__dt +8079a8a4 Item::EVENTBuffer::CreateInstance +8079ab68 Item::EVENTBuffer::DestroyInstance +8079ac1c 0x8079ac1c +8079aca4 0x8079aca4 +8079acc0 0x8079acc0 +8079ad30 Item::EVENTBuffer::GetPlayerIdFromBitfield +8079ad38 Item::EVENTBuffer::FillOrExtractShootPos +8079af08 Item::EVENTBuffer::FillOrExtractPosDiff +8079b078 0x8079b078 +8079b30c 0x8079b30c +8079b338 Item::EVENTBuffer::FillOrExtractBitfield +8079b398 Item::EVENTBuffer::FillOrExtractTargetingShoot +8079b440 Item::EVENTBuffer::FillOrExtractBombShoot +8079b4ac Item::EVENTBuffer::FillOrExtractShoot +8079bd94 Item::EVENTBuffer::UpdateSend +8079bed0 Item::EVENTBuffer::UpdateReceive +8079bfec Item::EVENTBuffer::QueueSendEntry +8079c0ac Item::EVENTBuffer::QueueReceivedEntry +8079c174 0x8079c174 +8079c220 Item::Obj::AddUseEVENTEntry +8079c2e4 Item::Obj::AddShootEVENTEntry +8079c3c4 Item::Obj::AddHitFreeEVENTEntry +8079c498 Item::Obj::AddHitDraggedEVENTEntry +8079c554 Item::Obj::AddTCLostPassEVENTEntry +8079c620 Item::Obj::AddAction6EVENTEntry +8079c6e4 Item::Obj::AddTCLostEndEVENTEntry +8079c7c4 Item::Obj::AddDropEVENTEntry +8079c880 Item::Obj::GetHitFreeEntryPlayerIdAndType +8079c898 0x8079c898 +8079c8a0 0x8079c8a0 +8079c8c8 Item::EVENTBuffer::GetEntryBitfield +8079c8e4 Item::EVENTBuffer::GetUSEPlayerId +8079c8ec 0x8079c8ec +8079c930 0x8079c930 +8079c960 Item::Obj::SendOrExtractHitDraggedEVENT +8079d2e0 thunk_0x8079dee4 +8079d3dc Item::EVENTBuffer::__ct +8079d674 Item::EVENTBuffer::__dt +8079d718 0x8079d718 +8079d720 0x8079d720 +8079d744 0x8079d744 +8079d74c 0x8079d74c +8079d754 0x8079d754 +8079d75c 0x8079d75c +8079d764 0x8079d764 +8079d76c GetEVENTEntryDataLength +8079d804 Item::Obj::InitSelf +8079d84c Item::Obj::CalcWorld::ExecCallbackB +8079d8bc Item::Obj::__ct +8079d920 0x8079d920 +8079d9fc Item::Obj::SpawnModel +8079da9c Item::Obj::PrepareLight +8079dc44 0x8079dc44 +8079dc60 0x8079dc60 +8079dcb4 Item::Obj::SetInitialPosition +8079dcbc Item::Obj::OnKill +8079dd6c Item::Obj::OnTetherBreak +8079dd70 Item::Obj::OnFinishKill +8079de34 Item::Obj::ProcessRecvShootEVENT +8079dee4 Item::Obj::vf_0x30 +8079e1f0 Item::Obj::vf_0x34 +8079e224 Item::Obj::Init +8079e300 Item::Obj::RemoveFromScn +8079e38c 0x8079e38c +8079e550 Item::Obj::Spawn +8079e5f4 Item::Obj::Set +8079e884 Item::Obj::FinishKill +8079e8f4 0x8079e8f4 +8079e9e8 0x8079e9e8 +8079ea58 0x8079ea58 +8079ebbc 0x8079ebbc +8079ec44 0x8079ec44 +8079ec98 0x8079ec98 +8079ed1c 0x8079ed1c +8079ed4c 0x8079ed4c +8079edb4 0x8079edb4 +8079ee30 Item::Obj::RegisterEntity +8079eecc Item::Obj::UnregisterEntity +8079eee0 0x8079eee0 +8079efec Item::Obj::Update +8079f8b4 0x8079f8b4 +8079f8f4 0x8079f8f4 +8079f934 0x8079f934 +8079fc50 0x8079fc50 +8079fd84 0x8079fd84 +8079fe30 Item::Obj::TryDisappearDueToExcess +8079feac 0x8079feac +8079feb4 Item::Obj::CanDisappearFromDuration +8079ff14 0x8079ff14 +8079ff5c Item::Obj::LoadGraphicsImplicitBRRESNoFunc +8079ffa4 Item::Obj::LoadGraphicsDefault +8079ffdc Item::Obj::LoadGraphicsImplicitBRRES +807a0040 Item::Obj::LoadGraphics +807a0380 Item::Obj::LoadItemLight +807a05d0 Item::Obj::UpdateModelPosition +807a05f4 Item::Obj::UpdateModelPositionNoClip +807a07b8 Item::Obj::UpdateModelFromVecs +807a0a68 Item::Obj::UpdateShadow +807a0cd4 Item::Obj::UpdateModelFromQuat +807a0e28 Item::Obj::OnObjCollision +807a1074 0x807a1074 +807a125c 0x807a125c +807a139c 0x807a139c +807a14d4 Item::Obj::CheckKartCollision +807a1680 0x807a1680 +807a1864 0x807a1864 +807a18ec Item::XZVecMag +807a18fc Item::Obj::ProcessOtherCollision +807a1c94 Item::Obj::CheckOtherObjsCollision +807a1ed8 0x807a1ed8 +807a2234 0x807a2234 +807a2250 0x807a2250 +807a2270 0x807a2270 +807a234c 0x807a234c +807a24f8 0x807a24f8 +807a2570 0x807a2570 +807a25b0 Item::Obj::UpdateWithClipping +807a2aa8 0x807a2aa8 +807a2de8 0x807a2de8 +807a2e68 0x807a2e68 +807a2edc 0x807a2edc +807a2f00 0x807a2f00 +807a2f18 0x807a2f18 +807a2f38 0x807a2f38 +807a2f58 0x807a2f58 +807a2f78 0x807a2f78 +807a2f7c 0x807a2f7c +807a2f88 0x807a2f88 +807a2fbc 0x807a2fbc +807a30c4 0x807a30c4 +807a3100 0x807a3100 +807a313c 0x807a313c +807a3178 0x807a3178 +807a31c0 Item::Obj::PrepareShootEventPacket +807a3370 Item::Obj::SendOrExtractShootEVENT +807a33e0 0x807a33e0 +807a3474 0x807a3474 +807a34cc 0x807a34cc +807a3544 0x807a3544 +807a35b0 0x807a35b0 +807a3618 0x807a3618 +807a3648 0x807a3648 +807a377c 0x807a377c +807a3790 Item::Obj::OnPlayerCollision +807a3870 0x807a3870 +807a38e0 0x807a38e0 +807a38e4 Item::Obj::KillThunk +807a38e8 0x807a38e8 +807a38ec 0x807a38ec +807a38f0 Item::ObjKouraGreen::UpdateThunk +807a38f4 Item::ObjKouraRed::UpdateThunk +807a38f8 Item::ObjKouraTogezo::UpdateThunk +807a38fc Item::ObjBanana::UpdateThunk +807a3900 Item::ObjBomb::UpdateThunk +807a3904 Item::ObjFIB::UpdateThunk +807a3908 Item::ObjKumo::UpdateThunk +807a390c 0x807a390c +807a3910 0x807a3910 +807a3914 0x807a3914 +807a3918 0x807a3918 +807a391c 0x807a391c +807a3920 0x807a3920 +807a3924 0x807a3924 +807a3928 0x807a3928 +807a392c 0x807a392c +807a3930 0x807a3930 +807a3934 0x807a3934 +807a3938 0x807a3938 +807a393c 0x807a393c +807a3940 0x807a3940 +807a3944 0x807a3944 +807a3948 0x807a3948 +807a394c 0x807a394c +807a3950 0x807a3950 +807a3954 0x807a3954 +807a397c 0x807a397c +807a3984 0x807a3984 +807a3988 0x807a3988 +807a398c 0x807a398c +807a3994 0x807a3994 +807a39d4 Item::Obj::CalcWorldCB::__dt +807a3a14 Item::Obj::CalcWorldCB::ExecCallbackB(thunk) +807a3a1c Item::Obj::CalcWorldCB::__dt(thunk) +807a3a24 Item::ObjBanana::CreateArray +807a3aa0 0x807a3aa0 +807a3ae0 0x807a3ae0 +807a3b20 0x807a3b20 +807a3b60 0x807a3b60 +807a3ba0 0x807a3ba0 +807a3be0 Item::ObjBanana::SetInitialPosition +807a3c80 0x807a3c80 +807a3e78 Item::ObjBanana::Update +807a400c Item::ObjBanana::vf_0x30 +807a4070 Item::ObjBanana::InitSelf +807a40f0 Item::ObjBanana::SpawnModel +807a40f4 Item::ObjBanana::OnKill +807a41c4 Item::ObjBomb::CreateArray +807a4384 0x807a4384 +807a43fc 0x807a43fc +807a4554 0x807a4554 +807a45e0 0x807a45e0 +807a4734 0x807a4734 +807a4850 Item::ObjBomb::__ct +807a49b4 Item::ObjExplosion::__dt +807a4a18 Item::ObjBomb::SetInitialPosition +807a4b48 0x807a4b48 +807a4cdc Item::ObjBomb::Update +807a4fa0 Item::ObjBomb::SendOrExtractShootEVENT +807a500c Item::ObjBomb::vf_0x30 +807a5090 Item::ObjBomb::vf_0x34 +807a5144 Item::ObjBomb::InitSelf +807a5340 Item::ObjBomb::UpdateModelPosition +807a59e0 Item::ObjBomb::UpdateModelPositionNoClip +807a5a20 0x807a5a20 +807a5ad0 0x807a5ad0 +807a5b80 Item::ObjBomb::ToggleVisible +807a5bc4 Item::ObjBomb::SpawnModel +807a5cfc Item::ObjBomb::PrepareLight +807a5d48 0x807a5d48 +807a5d88 0x807a5d88 +807a5ea4 0x807a5ea4 +807a5f6c Item::ObjBomb::OnKill +807a60f8 Item::ObjBomb::OnTetherBreak +807a619c Item::ObjBomb::OnFinishKill +807a61f8 0x807a61f8 +807a6248 0x807a6248 +807a626c Item::ObjBomb::__dt +807a62d8 __sinit_ItemObjBomb_cpp +807a6320 Item::ObjBomb::__dt(thunk) +807a6328 Item::Obj::KillObj +807a64f0 0x807a64f0 +807a6560 Item::Obj::KillFromOtherCollision +807a6614 Item::Obj::KillFromPlayerCollision +807a6700 Item::Obj::UpdateKill +807a6738 0x807a6738 +807a6928 Item::ObjMiddle::__ct +807a6978 0x807a6978 +807a69f8 0x807a69f8 +807a6c14 Item::Obj::DisappearDueToExcess +807a6c90 0x807a6c90 +807a6d88 0x807a6d88 +807a6ec8 0x807a6ec8 +807a6f64 0x807a6f64 +807a6f6c 0x807a6f6c +807a6f74 0x807a6f74 +807a71e4 0x807a71e4 +807a72ac 0x807a72ac +807a7350 0x807a7350 +807a784c 0x807a784c +807a78a8 Item::ObjFIB::CreateArray +807a7940 Item::ObjFIB::SetInitialPosition +807a79e0 Item::ObjFIB::Update +807a7a5c Item::ObjFIB::vf_0x30 +807a7ac0 Item::ObjFIB::InitSelf +807a7d60 0x807a7d60 +807a7d90 0x807a7d90 +807a7f1c Item::ObjFIB::SpawnModel +807a7fec Item::ObjFIB::UpdateModelPosition +807a8134 Item::ObjFIB::OnKill +807a8138 Item::ObjGesso::CreateArray +807a81b4 Item::Player::UseBlooper +807a8378 Item::ObjGesso::InitSelf +807a8394 0x807a8394 +807a8410 0x807a8410 +807a8460 Item::GessoScreen::__ct +807a85f4 Item::GessoScreen::__dt +807a8634 0x807a8634 +807a86f8 0x807a86f8 +807a8750 Item::GessoScreen::UpdateModel +807a8aa0 0x807a8aa0 +807a8bd0 0x807a8bd0 +807a8c0c 0x807a8c0c +807a8c90 0x807a8c90 +807a8d78 0x807a8d78 +807a8f04 Item::GessoMgr::CreateInstance +807a8fa0 Item::GessoMgr::DestroyInstance +807a8fd8 Item::GessoMgr::Init +807a90a0 Item::GessoMgr::__ct +807a9128 Item::GessoMgr::DeployBlooper +807a92b0 Item::GessoMgr::Update +807a972c Item::GessoMgr::ApplyBlooper +807a9a58 __sinit_ItemObjGesso_cpp +807a9a80 Item::ObjKiller::CreateArray +807a9afc Item::Player::UseBullet +807a9b40 Item::ObjKiller::InitSelf +807a9b5c Item::ObjKiller::UpdateModelPosition +807a9c98 __sinit_ItemObjKiller_cpp +807a9cc0 Item::ObjKinoko::CreateArray +807a9d3c Item::Player::UseMushroom +807a9d90 Item::ObjKinoko::InitSelf +807a9dac __sinit_ItemObjKinoko_cpp +807a9dd4 Item::ObjKinokoBig::CreateArray +807a9e50 Item::Player::UseMegaMushroom +807a9ea4 Item::ObjKinokoBig::InitSelf +807a9ec0 __sinit_ItemObjKinokoBig_cpp +807a9ee8 Item::ObjKinokoPowerful::CreateArray +807a9f64 Item::Player::UseGoldenMushroom +807a9fac Item::ObjKinokoPowerful::InitSelf +807a9fc8 __sinit_ItemObjKinokoPowerful_cpp +807a9ff0 Item::ObjKouraRed::CreateArray +807aa1b8 Item::ObjKouraRed::__ct +807aa358 0x807aa358 +807aa398 0x807aa398 +807aa41c 0x807aa41c +807aa4b0 0x807aa4b0 +807aa548 0x807aa548 +807aa5ac 0x807aa5ac +807aa608 0x807aa608 +807aa688 Item::ObjKouraRed::SetInitialPosition +807aa87c Item::ObjKouraRed::Update +807aaa5c Item::ObjKouraRed::UpdateModelPosition +807aaafc Item::ObjKouraRed::UpdateModelPositionNoClip +807aab88 0x807aab88 +807aabc8 0x807aabc8 +807aac80 0x807aac80 +807aaccc 0x807aaccc +807aad78 0x807aad78 +807aae58 0x807aae58 +807aae78 0x807aae78 +807ab004 0x807ab004 +807ab024 0x807ab024 +807ab180 0x807ab180 +807ab238 0x807ab238 +807ab37c 0x807ab37c +807ab388 0x807ab388 +807ab38c Item::ObjKouraRed::InitSelf +807ab40c Item::ObjKouraRed::SendOrExtractShootEVENT +807ab508 Item::ObjKouraRed::vf_0x30 +807ab654 0x807ab654 +807ab788 Item::ObjKouraRed::OnKill +807ab810 Item::ObjKouraRed::OnFinishKill +807ab854 Item::ObjKouraRed::SpawnModel +807ab894 Item::ObjKouraRed::GetTargetedPlayerId +807ab8b0 0x807ab8b0 +807ab96c __sinit_ItemObjKouraRed_cpp +807ab9d8 0x807ab9d8 +807ab9e0 Item::ObjKouraTogezo::CreateArray +807abc14 Item::ObjKouraTogezo::__ct +807abde8 0x807abde8 +807abe80 0x807abe80 +807abee4 0x807abee4 +807abef0 0x807abef0 +807abf2c Item::ObjKouraTogezo::ToggleVisible +807abf70 Item::ObjKouraTogezo::SpawnModel +807ac008 Item::ObjKouraTogezo::InitSelf +807ac1a8 Item::ObjKouraTogezo::CalcTargetedPlayerId +807ac49c Item::ObjKouraTogezo::Reset +807ac5b4 0x807ac5b4 +807ac5f4 0x807ac5f4 +807ac634 Item::ObjKouraTogezo::SetInitialPosition +807ac7d4 Item::ObjKouraTogezo::UpdateModelPosition +807ac9ec Item::ObjKouraTogezo::UpdateModelPositionNoClip +807acb24 0x807acb24 +807ace1c 0x807ace1c +807ad118 Item::ObjKouraTogezo::Update +807ad5a4 0x807ad5a4 +807ad5ec Item::ObjKouraTogezo::UpdateRouteFlying +807ad7e4 Item::ObjKouraTogezo::807ad7e4 +807ad824 Item::ObjKouraTogezo::UpdateHomeIn +807ad914 0x807ad914 +807ad958 Item::ObjKouraTogezo::UpdateDiveIn +807adb54 0x807adb54 +807adc08 0x807adc08 +807adc5c Item::ObjKouraTogezo::UpdateHover +807adeac 0x807adeac +807adf14 0x807adf14 +807adfa4 0x807adfa4 +807ae180 Item::ObjKouraTogezo::807ae180_ +807ae34c Item::ObjKouraTogezo::UpdateExplosion +807ae414 Item::ObjKouraTogezo::OnKill +807ae574 Item::ObjKouraTogezo::OnFinishKill +807ae5d0 Item::ObjKouraTogezo::SendOrExtractShootEVENT +807ae6ec Item::ObjKouraTogezo::vf_0x30 +807ae8a8 0x807ae8a8 +807ae8b0 Item::ObjKouraTogezo::GetTargetedPlayerId +807ae8e8 0x807ae8e8 +807ae9bc Item::ObjKouraGreen::__dt +807aea78 __sinit_ItemObjKouraTogezo_cpp +807aeae4 Item::ObjKouraGreen::__dt(thunk) +807aeaec Item::ObjKouraGreen::CreateArray +807aeb68 get_1 +807aeb74 0x807aeb74 +807aeb80 0x807aeb80 +807aeb8c 0x807aeb8c +807aeb98 Item::ObjKouraGreen::SetInitialPosition +807aec64 Item::ObjKouraGreen::Update +807aed2c Item::ObjKouraGreen::UpdateModelPosition +807aedcc Item::ObjKouraGreen::UpdateModelPositionNoClip +807aee58 Item::ObjKouraGreen::SendOrExtractShootEVENT +807aef38 Item::ObjKouraGreen::vf_0x30 +807aefb4 Item::ObjKouraGreen::InitSelf +807af034 Item::ObjKouraGreen::OnKill +807af0bc Item::ObjKouraGreen::OnFinishKill +807af100 Item::ObjKouraGreen::SpawnModel +807af140 Item::ObjKumo::CreateArray +807af1bc Item::Player::UseTC +807af200 Item::ObjKumo::SpawnModel +807af234 0x807af234 +807af39c Item::ObjKumo::UpdateModelPosition +807af534 Item::ObjKumo::InitSelf +807af5a0 0x807af5a0 +807af76c Item::ObjKumo::Update +807b07c0 0x807b07c0 +807b08f4 0x807b08f4 +807b0960 Item::ObjKumo::OnFinishKill +807b09f0 0x807b09f0 +807b0b4c 0x807b0b4c +807b0f68 0x807b0f68 +807b1108 0x807b1108 +807b120c 0x807b120c +807b1218 0x807b1218 +807b1220 0x807b1220 +807b123c Item::ObjKumo::OnKill +807b1304 0x807b1304 +807b1640 0x807b1640 +807b189c 0x807b189c +807b1950 0x807b1950 +807b1a24 0x807b1a24 +807b1a44 0x807b1a44 +807b1a6c 0x807b1a6c +807b1a94 0x807b1a94 +807b1ab0 Item::ObjPow::CreateArray +807b1b2c Item::Player::UsePow +807b1ba4 Item::ObjPow::InitSelf +807b1bc0 Item::PowMgr::CreateInstance +807b1c6c 0x807b1c6c +807b1c70 Item::PowMgr::DestroyInstance +807b1ca8 Item::PowMgr::Init +807b1d18 Item::PowMgr::__ct +807b1db0 0x807b1db0 +807b1db4 Item::PowMgr::DeployPow +807b1eec Item::PowMgr::AnimateModel +807b20bc Item::PowMgr::CreateShockwave +807b22f8 Item::PowMgr::Update +807b2474 0x807b2474 +807b24f0 0x807b24f0 +807b2540 Item::PowScreen::__ct +807b27fc 0x807b27fc +807b2868 0x807b2868 +807b2884 0x807b2884 +807b2924 0x807b2924 +807b2980 0x807b2980 +807b29dc Item::PowScreen::UpdateModel +807b2c14 0x807b2c14 +807b2d64 0x807b2d64 +807b2e90 Item::PowMgr::IsPowActive +807b2eac Item::PowMgr::GetFramesUntilPOWSmash +807b2ec0 Item::PowMgr::GetPowUserPlayerId +807b2ed0 __sinit_ItemObjPow_cpp +807b2ef8 0x807b2ef8 +807b2f14 0x807b2f14 +807b2f54 0x807b2f54 +807b381c 0x807b381c +807b3bac 0x807b3bac +807b3c8c 0x807b3c8c +807b3db0 0x807b3db0 +807b3ec0 0x807b3ec0 +807b3eec Item::ObjKouraTogezoBase::ResetTargeting +807b3efc Item::ObjKouraTogezoBase::SpawnModel +807b3f3c 0x807b3f3c +807b3fdc 0x807b3fdc +807b43dc 0x807b43dc +807b4400 0x807b4400 +807b4430 0x807b4430 +807b45b0 Item::ObjKouraTogezo::AcquireTarget +807b4810 0x807b4810 +807b4878 0x807b4878 +807b4944 0x807b4944 +807b4ae4 0x807b4ae4 +807b4d2c 0x807b4d2c +807b5068 0x807b5068 +807b50f4 0x807b50f4 +807b5178 0x807b5178 +807b532c 0x807b532c +807b53f4 0x807b53f4 +807b5484 0x807b5484 +807b54f4 0x807b54f4 +807b60a0 0x807b60a0 +807b6148 0x807b6148 +807b61cc 0x807b61cc +807b62b8 0x807b62b8 +807b64f8 0x807b64f8 +807b660c 0x807b660c +807b66f0 0x807b66f0 +807b681c 0x807b681c +807b6910 Item::ObjThrowable::OnFinishThrow +807b6b98 0x807b6b98 +807b6bfc KouraGreen_07b6bfc_ +807b6f54 0x807b6f54 +807b6ff0 Item::ObjStar::CreateArray +807b706c Item::Player::UseStar +807b70c0 Item::ObjStar::InitSelf +807b70dc __sinit_ItemObjStar_cpp +807b7104 Item::ObjThrowable::SetInitialPositionImpl +807b74d4 0x807b74d4 +807b7570 0x807b7570 +807b7a48 0x807b7a48 +807b7aa4 0x807b7aa4 +807b7b00 Item::ObjThunder::CreateArray +807b7b7c Item::Player::UseThunder +807b7c04 Item::ObjThunder::InitSelf +807b7c20 Item::ObjThunder::ApplyToPlayers +807b7d74 __sinit_ItemObjThunder_cpp +807b7d9c 0x807b7d9c +807b7e58 0x807b7e58 +807b7e6c 0x807b7e6c +807b7f5c 0x807b7f5c +807b8010 0x807b8010 +807b80e0 0x807b80e0 +807b854c 0x807b854c +807b860c ItemPathManager_createInstance +807b897c 0x807b897c +807b89e4 0x807b89e4 +807b8a9c Player_UpdateItemPoint +807b8f30 Item::Point_update2 +807b90f4 0x807b90f4 +807b91e4 0x807b91e4 +807b99d4 0x807b99d4 +807b9a38 0x807b9a38 +807b9b24 0x807b9b24 +807b9c10 0x807b9c10 +807b9c70 0x807b9c70 +807b9dd4 Item::GetQuatFromMat +807b9e50 0x807b9e50 +807b9ec4 0x807b9ec4 +807b9f48 Item::PlayerRoulette::__ct +807b9f54 0x807b9f54 +807b9f80 Item::PlayerRoulette::Init +807b9fb0 Item::PlayerRoulette::DecideItem +807ba230 0x807ba230 +807ba2d8 Item::PlayerRoulette::OnRouletteEnd +807ba350 0x807ba350 +807ba37c Item::PlayerRoulette::Update +807ba56c 0x807ba56c +807ba5bc 0x807ba5bc +807ba5d0 0x807ba5d0 +807ba5d8 0x807ba5d8 +807ba650 Item::SlotData::Disposer__dt +807ba77c Item::SlotData::CreateInstance +807ba814 Item::SlotData::DestroyInstance +807ba8c8 Item::SlotData::__ct +807ba934 Item::SlotData::__dt +807ba9d8 Item::SlotData::ProcessTable +807bad08 Item::SlotData::GetNextTable +807bad20 Item::SlotData::PostProcessVSTable +807bae80 0x807bae80 +807baed4 Item::SlotData::RandomizeRouletteItem +807baf48 Item::SlotData::Init +807bb374 Item::SlotData::RouletteItems::__ct +807bb380 Item::ItemSlotData::CanItemNotBeObtained +807bb42c Item::SlotData::DecideItem +807bb8d0 Item::SlotData::DecideRouletteItem +807bb9bc Item::SlotData::ResetShockTimer +807bb9c8 Item::SlotData::ResetBlueShellTimer +807bb9d4 Item::SlotData::ResetBlooperTimer +807bb9e0 Item::SlotData::ResetPowTimer +807bb9ec Item::SlotData::UpdateTimers +807bba94 Item::SlotData::GetBinary +807bbb00 Item::SlotData::SetupBattleModeProbabilities +807bbd60 Item::SlotData::SetupOnlineVSProbabilities +807bbe98 0x807bbe98 +807bbef8 Item::SlotData::SetupVSProbabilities +807bc000 Item::SlotData::SetupGPProbabilities +807bc0f8 Item::PlayerInventory::__ct +807bc0fc 0x807bc0fc +807bc130 Item::PlayerInventory::Init +807bc170 0x807bc170 +807bc2b8 0x807bc2b8 +807bc350 Item::PlayerInventory::EjectItems +807bc610 Item::PlayerInventory::LoseItemFromDmg +807bc6e8 Item::PlayerInventory::Update +807bc830 0x807bc830 +807bc908 0x807bc908 +807bc940 Item::PlayerInventory::SetItem +807bc97c Item::PlayerInventory::RemoveItems +807bc9c0 Item::PlayerInventory::ClearAll +807bc9f4 0x807bc9f4 +807bcab0 0x807bcab0 +807bcad0 0x807bcad0 +807bcae0 Item::InitAllBehavior +807bd1d0 DriverMgr::SetParams +807bd210 DriverMgr::SetParams_switch +807bd214 DriverMgr::SetParams_caseD_2 +807bd230 DriverMgr::SetParams_caseD_3 +807bd24c DriverMgr::SetParams_caseD_7 +807bd268 DriverMgr::SetParams_caseD_9 +807bd284 DriverMgr::SetParams_caseD_0 +807bd29c DriverMgr::SetParams_caseD_1 +807bd340 SomeTimers_update +807bd374 0x807bd374 +807bd5bc DriverMgr::IsPlayerComputer +807bd6e4 0x807bd6e4 +807bd718 DriverMgr::GetRaceinfoRandom +807bd768 0x807bd768 +807bd78c 0x807bd78c +807bd7b4 CollisionInfo::ObjInfo::Init +807bd7d8 CollisionInfo::ObjInfo::Set +807bd810 0x807bd810 +807bd850 CollisionInfo::ObjInfo::AreItemsAllowedOnObj +807bd864 CollisionInfo::ObjInfo::SetDistanceCalcBit +807bd88c CollisionInfo::ObjInfo::CalcInitialDistance +807bd8b8 0x807bd8b8 +807bd8d4 0x807bd8d4 +807bd8e8 CollisionInfo::ObjInfo::Update +807bd908 CollisionInfo::ObjInfo::UpdateWithRot +807bd930 CollisionInfo::ObjInfo::Reset +807bd954 0x807bd954 +807bd96c KCLTypeHolder::FindClosestCollisionEntry +807bd9e8 0x807bd9e8 +807bda7c KCLTypeHolder::Reset +807bda9c KCLTypeHolder::ApplyFlagAndRegisterEntry +807bdae8 0x807bdae8 +807bdb28 KCLTypeHolder::ReplaceBLIGHTIndex +807bdb5c KCLTypeHolder::ReplaceKCLVariant +807bdb90 KCLTypeHolder::ReplaceWheelDepth +807bdbc4 KCLTypeHolder::ReplaceTrickableBit +807bdbf8 KCLTypeHolder::ReplaceRejectBit +807bdc2c KCLTypeHolder::ReplaceSoftBit +807bdc5c KCLController::__ct +807bdd7c 0x807bdd7c +807bddbc KCLController::__dt +807bddfc 0x807bddfc +807bdf54 0x807bdf54 +807be030 KCLController::FindTrianglesList +807be12c KCLController::FindTrianglesListAndProcessNewRadius +807be3c4 0x807be3c4 +807bf4c0 0x807bf4c0 +807c01e4 0x807c01e4 +807c0884 KCLController::IsCollidingImpl +807c0f00 KCLController::IsCollidingFromCurCopyPosition +807c1514 KCLController::IsCollidingImplInfY +807c1b0c 0x807c1b0c +807c1bb4 KCLController::GetTrianglesList +807c1c8c 0x807c1c8c +807c1de8 KCLController::GetTrianglesListReuse +807c1f80 0x807c1f80 +807c1fac 0x807c1fac +807c21f4 0x807c21f4 +807c2410 KCLController::IsColliding +807c243c 0x807c243c +807c24c0 KCLController::Update +807c25cc 0x807c25cc +807c2630 0x807c2630 +807c2660 CollisionInfo::Init +807c26ac 0x807c26ac +807c2824 KCLManager::CreateInstance +807c2884 KCLManager::DestroyInstance +807c28d8 KCLManager::Load +807c2928 0x807c2928 +807c293c KCLManager::Update +807c29e4 0x807c29e4 +807c2a04 0x807c2a04 +807c2a60 0x807c2a60 +807c2bd8 KCLManager::IsCollidingFillInfoNoTerrainInfo +807c2da0 0x807c2da0 +807c2f18 KCLManager::IsCollidingFillInfoAddEntryNoTerrainInfo +807c30e0 0x807c30e0 +807c3258 KCLManager::IsCollidingFillInfo +807c3554 0x807c3554 +807c36cc KCLManager::IsCollidingFillInfoAddEntry +807c39c8 KCLManager::IsCollidingNoTerrainInfoNoTriangleCheck +807c3b5c KCLManager::IsCollidingAddEntryNoTerrainInfoNoTriangleCheck +807c3cf0 KCLManager::IsColliding +807c3e84 KCLManager::IsCollidingAddEntryNoTriangleCheck +807c4018 0x807c4018 +807c41a4 0x807c41a4 +807c4330 0x807c4330 +807c44bc 0x807c44bc +807c4648 KCLManager::IsCollidingNoTerrainInfo +807c47f0 KCLManager::IsCollidingAddEntryNoTerrainInfo +807c4998 0x807c4998 +807c4b40 KCLManager::IsCollidingAddEntry +807c4ce8 ObjectKCLController::__ct +807c4d6c ObjectKCLController::__dt +807c4dc8 ObjectKCLController::Update +807c4e4c 0x807c4e4c +807c4e7c 0x807c4e7c +807c4eac 0x807c4eac +807c506c 0x807c506c +807c522c 0x807c522c +807c53a4 0x807c53a4 +807c551c ObjectKCLController::CalcCollisionNoTerrainInfoNoTriangleCheck +807c56f8 ObjectKCLController::CalcCollisionAddEntryNoTerrainInfoNoTriangleCheck +807c58d4 ObjectKCLController::CalcCollision +807c5a68 ObjectKCLController::CalcCollisionAddEntryNoTriangleCheck +807c5bfc 0x807c5bfc +807c5dd4 0x807c5dd4 +807c5fac 0x807c5fac +807c613c 0x807c613c +807c62cc ObjectKCLController::CalcCollisionNoTerrainInfo +807c64c0 ObjectKCLController::CalcCollisionAddEntryNoTerrainInfo +807c66b4 0x807c66b4 +807c6860 ObjectKCLController::CalcCollisionAddEntry +807c6a0c 0x807c6a0c +807c6a14 0x807c6a14 +807c6a20 0x807c6a20 +807c6a38 0x807c6a38 +807c6a44 0x807c6a44 +807c6a98 0x807c6a44_switch +807c6a9c 0x807c6a44_caseD_0 +807c6ad8 0x807c6a44_caseD_5 +807c6b34 0x807c6a44_caseD_4 +807c6b70 0x807c6a44_caseD_1 +807c6bc8 0x807c6a44_caseD_8 +807c6c04 0x807c6a44_caseD_9 +807c6d1c 0x807c6d1c +807c6e64 0x807c6e64 +807c6f0c 0x807c6f0c +807c6f3c 0x807c6f3c +807c708c DriverControllerBones::__ct +807c70b4 0x807c70b4 +807c70f4 DriverControllerBones::__dt +807c7134 DriverControllerBones::SetBoneName +807c7158 DriverControllerBones::SetNode +807c71d8 DriverMgr::InitiateDriverControllerBoneNames +807c7358 0x807c7358 +807c7364 DriverController::__ct +807c77e4 0x807c77e4 +807c77e8 0x807c77e8 +807c7828 DriverController::LoadModels +807c7e7c DriverController::LoadBones +807c8624 0x807c8624 +807c8758 DriverController::Load +807c962c 0x807c962c +807c9840 0x807c9840 +807c9844 0x807c9844 +807ca024 0x807ca024 +807ca068 0x807ca068 +807ca0b8 0x807ca0b8 +807ca6e4 0x807ca6e4 +807caaa4 0x807caaa4 +807cab8c 0x807cab8c +807cad68 0x807cad68 +807cae80 0x807cae80 +807cae8c 0x807cae8c +807cb0c8 DriverController::Init +807cb130 DriverControllerBike::Init +807cb198 0x807cb198 +807cb34c 0x807cb34c +807cb360 DriverController::Update +807cb530 0x807cb530 +807cbd18 0x807cbd18 +807cbd20 0x807cbd20 +807cbd24 0x807cbd24 +807cbeb8 0x807cbeb8 +807cc018 PlayerModel_setAnimation +807cc170 0x807cc170 +807cc174 0x807cc174 +807cce78 0x807cce78 +807cce88 0x807cce88 +807cce98 0x807cce98 +807ccea8 0x807ccea8 +807cceb8 0x807cceb8 +807ccec8 0x807ccec8 +807cced8 0x807cced8 +807ccee8 0x807ccee8 +807ccef8 0x807ccef8 +807ccf08 0x807ccf08 +807ccf18 0x807ccf18 +807ccf28 0x807ccf28 +807cd32c 0x807cd32c +807cda74 0x807cda74 +807cdbf8 0x807cdbf8 +807cdc20 0x807cdc20 +807cdca4 0x807cdca4 +807cdccc DriverControllerBike::__ct +807cdd08 DriverController::__dt +807cddb8 0x807cddb8 +807cdf14 0x807cdf14 +807ce190 0x807ce190 +807ce270 0x807ce270 +807ce31c 0x807ce31c +807ce38c 0x807ce38c +807ce5a4 0x807ce5a4 +807ce6d0 0x807ce6d0 +807cec48 0x807cec48 +807cecd4 0x807cecd4 +807cf510 0x807cf510 +807cf920 0x807cf920 +807cf960 0x807cf960 +807cf9f0 0x807cf9f0 +807d0554 0x807d0554 +807d0744 0x807d0744 +807d07f0 0x807d07f0 +807d0898 0x807d0898 +807d0938 0x807d0938 +807d094c 0x807d094c +807d0b5c MiiDriverModel::UpdateParams +807d0d68 0x807d0d68 +807d0e70 0x807d0e70 +807d0f78 0x807d0f78 +807d14b4 0x807d14b4 +807d1504 0x807d1504 +807d171c 0x807d171c +807d18ec 0x807d18ec +807d19c8 0x807d19c8 +807d1a60 0x807d1a60 +807d1bdc 0x807d1bdc +807d1bf8 0x807d1bf8 +807d1c14 0x807d1c14 +807d1c40 0x807d1c40 +807d1c5c 0x807d1c5c +807d1c78 0x807d1c78 +807d1c94 0x807d1c94 +807d1cb0 0x807d1cb0 +807d1ccc 0x807d1ccc +807d1ce8 0x807d1ce8 +807d1d04 0x807d1d04 +807d1e34 0x807d1e34 +807d1f80 0x807d1f80 +807d201c 0x807d201c +807d2038 0x807d2038 +807d2064 0x807d2064 +807d20a0 0x807d20a0 +807d20bc 0x807d20bc +807d20e0 0x807d20e0 +807d2114 0x807d2114 +807d2130 0x807d2130 +807d214c 0x807d214c +807d2168 0x807d2168 +807d218c 0x807d218c +807d21a8 0x807d21a8 +807d21c4 0x807d21c4 +807d21f8 DriverController::CreateModelDirectors +807d2784 0x807d2784 +807d2798 0x807d2798 +807d2940 0x807d2940 +807d2aa8 0x807d2aa8 +807d2b2c 0x807d2b2c +807d2b68 0x807d2b68 +807d2bb4 0x807d2bb4 +807d2da8 0x807d2da8 +807d2ddc 0x807d2ddc +807d2de4 0x807d2de4 +807d2dec 0x807d2dec +807d2e78 0x807d2e78 +807d2f04 0x807d2f04 +807d2fac 0x807d2fac +807d3054 0x807d3054 +807d30a4 0x807d30a4 +807d30f4 0x807d30f4 +807d3100 0x807d3100 +807d3188 0x807d3188 +807d3300 0x807d3188_switch +807d3304 0x807d3188_caseD_1a +807d3314 0x807d3188_caseD_13 +807d3330 0x807d3188_caseD_16 +807d3358 0x807d3188_caseD_14 +807d338c 0x807d338c +807d3458 0x807d3458 +807d37b4 0x807d37b4 +807d37d0 0x807d37d0 +807d381c 0x807d381c +807d3868 0x807d3868 +807d396c 0x807d396c +807d3d58 0x807d3d58 +807d3e68 0x807d3e68 +807d3f10 0x807d3f10 +807d3f24 0x807d3f24 +807d3f58 DriverControllerBike::__dt +807d400c 0x807d400c +807d42e8 DriverLimbs::__ct +807d45e8 DriverCalcWorldCB::__dt +807d4628 0x807d4628 +807d46a4 0x807d46a4 +807d4700 0x807d4700 +807d4904 0x807d4904 +807d492c 0x807d492c +807d4a40 0x807d4a40 +807d4a4c 0x807d4a4c +807d4aa0 0x807d4aa0 +807d4af4 0x807d4af4 +807d4b20 0x807d4b20 +807d4d08 0x807d4d08 +807d5680 0x807d5680 +807d5760 DriverLimbs::ExecCallbackA +807d5944 0x807d5944 +807d5a98 DriverLimbs::ExecCallbackB +807d5b38 0x807d5b38 +807d5e90 0x807d5e90 +807d63e0 DriverLimbs::ExecCallbackC +807d65bc 0x807d65bc +807d6844 0x807d6844 +807d69e8 DriverLimbs::CalcLeftArm +807d6b28 DriverLimbs::CalcRightArm +807d6cac DriverLimbs::CalcLeftLeg +807d6e60 0x807d6e60 +807d6edc 0x807d6edc +807d6f58 DriverLimbs::CalcRightLeg +807d710c 0x807d710c +807d7458 0x807d7458 +807d7588 0x807d7588 +807d76e8 0x807d76e8 +807d77d4 0x807d77d4 +807d798c 0x807d798c +807d8004 0x807d8004 +807d82e4 0x807d82e4 +807d8604 0x807d8604 +807d8688 0x807d8688 +807d870c 0x807d870c +807d8790 0x807d8790 +807d89f4 0x807d89f4 +807d8e30 0x807d8e30 +807d8f88 0x807d8f88 +807d9038 0x807d9038 +807d90e8 0x807d90e8 +807d9134 0x807d9134 +807d91cc 0x807d91cc +807d9688 0x807d9688 +807d9734 0x807d9734 +807d973c 0x807d973c +807d974c DriverLimbs::ExecCallbackC(thunk) +807d9754 DriverLimbs::ExecCallbackB(thunk) +807d975c DriverLimbs::ExecCallbackA(thunk) +807d9764 DriverLimbs::__dt(thunk) +807d976c DriverController::GetBoneMatId +807d9798 DriverCalcWorldCB::__ct +807d99ec DriverCalcWorldCB::ExecCallbackB +807d9b0c DriverCalcWorldCB::ExecCallbackC +807d9b80 DriverCalcWorldCB::ExecCallbackC(thunk) +807d9b88 DriverCalcWorldCB::ExecCallbackB(thunk) +807d9b90 DriverCalcWorldCB::__dt(thunk) +807d9b98 TicoModel::__ct +807d9d64 TicoModel::Update +807da40c 0x807da40c +807da578 TicoModel::UpdateKartPosition +807da5c0 ToadetteHair::__ct(ResFile, ModelDirector) +807da8f4 ToadetteHair::__ct(DriverController) +807dac50 0x807dac50 +807dad04 0x807dad04 +807db004 0x807db004 +807db008 0x807db008 +807db028 ToadetteHair::ToggleLock +807db03c ToadetteHair::CalcWorldCB::ExecCallbackC +807dbc24 ToadetteHair::__dt +807dbc7c ToadetteHair::CalcWorldCB::__dt +807dbcbc ToadetteHair::CalcWorldCB::ExecCallbackC(thunk) +807dbcc4 ToadetteHair::CalcWorldCB::__dt(thunk) +807dbccc 0x807dbccc +807dbd80 MiiHeadsModel::__ct(Mii) +807dbe0c MiiHeadsModel::__ct(miiIdx, r9) +807dbec8 0x807dbec8 +807dbf7c 0x807dbf7c +807dbfbc MiiHeadsModel::__ct(Mii, r9) +807dc048 MiiHeadsModel::Init +807dc2ac 0x807dc2ac +807dc2c0 MiiHeadsModel::UpdateParams +807dc2d8 0x807dc2d8 +807dc2f0 0x807dc2f0 +807dc3e8 MiiHeadsModel::SetExpression +807dc43c MiiHeadsModel::SetExpression_switch +807dc440 MiiHeadsModel::SetExpression_caseD_5 +807dc48c MiiHeadsModel::SetExpression_caseD_6 +807dc4d8 MiiHeadsModel::SetExpression_caseD_7 +807dc524 MiiHeadsModel::SetExpression_caseD_4 +807dc570 MiiHeadsModel::SetExpression_caseD_2 +807dc5bc MiiHeadsModel::SetExpression_caseD_1 +807dc608 MiiHeadsModel::SetExpression_caseD_0 +807dc654 MiiHeadsModel::SetExpression_caseD_3 +807dc6c8 0x807dc6c8 +807dc7e4 0x807dc7e4 +807dc860 MiiHeadsModel::__dt +807dc8b8 0x807dc8b8 +807dc8c8 0x807dc8c8 +807dc950 0x807dc950 +807dca40 0x807dca40 +807dcb30 0x807dcb30 +807dcc20 0x807dcc20 +807dcc60 0x807dcc60 +807dcca0 0x807dcca0 +807dcce0 0x807dcce0 +807dcd20 0x807dcd20 +807dcd60 0x807dcd60 +807dcda0 0x807dcda0 +807dce78 0x807dce78 +807dcf50 0x807dcf50 +807dd028 0x807dd028 +807dd100 0x807dd100 +807dd1d8 0x807dd1d8 +807dd2b0 0x807dd2b0 +807dd5b8 0x807dd5b8 +807dd8c0 0x807dd8c0 +807ddbc8 0x807ddbc8 +807dded0 0x807dded0 +807de1d8 0x807de1d8 +807de4e0 0x807de4e0 +807de640 lim_sprintf +807de6e8 0x807de6e8 +807de7a8 0x807de7a8 +807de820 0x807de820 +807de858 0x807de858 +807de8b8 0x807de8b8 +807de934 0x807de934 +807deb24 0x807deb24 +807debcc 0x807debcc +807ded14 PSMTXRotRad +807dee8c 0x807dee8c +807df004 0x807df004 +807df17c 0x807df17c +807df260 0x807df260 +807df3c4 0x807df3c4 +807df430 0x807df430 +807df56c 0x807df56c +807df690 0x807df690 +807df7b4 0x807df7b4 +807df808 MinimapMgr::Disposer__dt +807df908 MinimapMgr::CreateInstance +807dfa04 MinimapMgr::DestroyInstance +807dfad0 MinimapMgr::__ct +807dfba0 0x807dfba0 +807dfba4 MinimapMgr::Load +807dfea4 0x807dfea4 +807dfee4 MinimapMgr::__dt +807dffa4 MinimapMgr::TryDraw +807e0028 MinimapMgr::Draw +807e057c MinimapMgr::ApplyAREAType5 +807e08dc 0x807e08dc +807e08e0 0x807e08e0 +807e08e4 MinimapMgr::Convert3DPosToPercent +807e093c CtrlMenuBattleCupSelectCup::GetClassName +807e0948 CtrlMenuBattleCupSelectStage::GetClassName +807e0958 CtrlMenuBattleCupSelectCup::Load +807e0b50 CtrlMenuBattleCupSelectCup::OnInit +807e0c90 CtrlMenuBattleCupSelectCup::OnCupButtonSelect +807e0d60 CtrlMenuBattleCupSelectCup::OnCupButtonClick +807e0dc0 CtrlMenuBattleCupSelectStage::Load +807e0ee4 CtrlMenuBattleCupSelectStage::UpdateStageList +807e0fd0 CtrlMenuBattleCupSelectStage::InitSelf +807e10d4 CtrlMenuBattleCupSelectStage::OnUpdate +807e1248 CtrlMenuBattleCupSelectStage::GetRuntimeTypeInfo +807e1254 CtrlMenuBattleCupSelectCup::GetRuntimeTypeInfo +807e1260 CtrlMenuBattleCup::_sinit_ +807e127c CtrlMenuBattleStageSelectCupSub::GetClassName +807e1288 CtrlMenuBattleStageSelectCup::GetClassName +807e1298 CtrlMenuBattleStageSelectStage::GetClassName +807e12a8 StageButton::GetClassName +807e12b8 CtrlMenuBattleStageSelectCup::Load +807e1444 CtrlMenuBattleStageSelectCup::InitSelf +807e1548 CtrlMenuBattleStageSelectCupSub::SetRelativePosition +807e15cc CtrlMenuBattleStageSelectCupSub::InitSelf +807e1668 CtrlMenuBattleStageSelectCupSub::OnUpdate +807e1d78 Pages::BattleStageSelect::GetManipulatorManager +807e1d80 CtrlMenuBattleStageSelectStage::Load +807e1ed0 CtrlMenuBattleStageSelectStage::InitSelf +807e22b4 CtrlMenuBattleStageSelectStage::OnStageButtonClick +807e230c CtrlMenuBattleStageSelectStage::OnStageButtonSelect +807e2508 CtrlMenuBattleStageSelectStage::GetRuntimeTypeInfo +807e2514 StageButton::GetRuntimeTypeInfo +807e2520 CtrlMenuMovieButton::GetMovieCount +807e2528 CtrlMenuBattleStageSelectCup::GetRuntimeTypeInfo +807e2534 CtrlMenuBattleStageSelectCupSub::GetRuntimeTypeInfo +807e2540 CtrlMenuBattleStage_cpp_sinit_ +807e256c CtrlMenuMovieButton::OnUpdate(PushButton) +807e2574 CtrlMenuMovieButton::InitSelf(PushButton) +807e257c CtrlMenuMovieButton::OnLoad(PushButton) +807e2584 StageButton::__dt(PushButton) +807e258c StageButton::GetClassName(PushButton) +807e2594 StageButton::GetRuntimeTypeInfo(PushButton) +807e259c CtrlMenuCharacterSelect::GetClassName +807e25a8 CtrlMenuCharacterSelect::Load +807e2808 CtrlMenuCharacterSelect::ButtonDriver::__ct +807e2844 CtrlMenuCharacterSelect::ButtonDriver::__dt +807e289c CtrlMenuCharacterSelect::GetUnlockedStatus +807e2928 CtrlMenuCharacterSelect::LoadButton +807e2d18 CtrlMenuCharacterSelect::InitSelf +807e30ac CtrlMenuCharacterSelect::OnUpdate +807e35b0 CtrlMenuCharacterSelect::GetButtonDriver +807e35fc CtrlMenuCharacterSelect::OnButtonClick +807e36f4 CtrlMenuCharacterSelect::OnButtonSelect +807e37d4 CtrlMenuCharacterSelect::OnButtonDeselect +807e3880 CtrlMenuCharacterSelect::OnMiiButtonClick +807e39cc CtrlMenuCharacterSelect::OnMiiButtonSelect +807e3aac CtrlMenuCharacterSelect::OnMiiButtonDeselect +807e3b58 CtrlMenuCharacterSelect::OnMultiplayerMiiSelected +807e3bec CtrlMenuCharacterSelect::GetUnlockedCharacterCountByWeight +807e3cac CtrlMenuCharacterSelect::GetCorrectButtonDriverId +807e3e00 CtrlMenuCharacterSelect::ButtonDriver::GetClassName +807e3e10 CtrlMenuCharacterSelect::ButtonDriver::InitSelf +807e3eac CtrlMenuCharacterSelect::ButtonDriver::SetPositionAnim +807e3efc CtrlMenuCharacterSelect::ButtonDriver::ResetButtonColours +807e4130 CtrlMenuCharacterSelect::ButtonDriver::SetButtonColours +807e43d4 CtrlMenuCharacterSelect::GetRuntimeTypeInfo +807e43e0 CtrlMenuCharacterSelect::ButtonDriver::GetRuntimeTypeInfo +807e43ec CtrlMenuCharacterSelect_cpp_sinit_ +807e4410 CtrlMenuCourseSelectCupSub::GetClassName +807e441c CtrlMenuCourseSelectCup::GetClassName +807e442c CtrlMenuCourseSelectCourse::GetClassName +807e443c CourseButton::GetClassName +807e444c CtrlMenuCourseSelectCup::Load +807e45c0 CtrlMenuCourseSelectCup::InitSelf +807e4744 CtrlMenuCourseSelectCupSub::SetRelativePosition +807e47c8 CtrlMenuCourseSelectCupSub::InitSelf +807e4864 CtrlMenuCourseSelectCupSub::OnUpdate +807e4f74 Pages::CourseSelect::GetManipulatorManager +807e4f7c CtrlMenuCourseSelectCourse::Load +807e5118 CtrlMenuCourseSelectCourse::InitSelf +807e5434 CtrlMenuCourseSelectCourse::OnCourseButtonClick +807e548c CtrlMenuCourseSelectCourse::OnCourseButtonSelect +807e55f8 CtrlMenuCourseSelectCourse::GetRuntimeTypeInfo +807e5604 CourseButton::GetRuntimeTypeInfo +807e5610 CtrlMenuCourseSelectCup::GetRuntimeTypeInfo +807e561c CtrlMenuCourseSelectCupSub::GetRuntimeTypeInfo +807e5628 CtrlMenuCoursecpp_sinit_ +807e5654 CourseButton::__dt(PushButton) +807e565c 0x807e565c +807e5664 CourseButton::GetRuntimeTypeInfo(PushButton) +807e566c CtrlMenuCupSelectCup::GetClassName +807e5678 CtrlMenuCupSelectCourse::GetClassName +807e5688 CtrlMenuCupSelectCup::Load +807e5894 CtrlMenuCupSelectCup::InitSelf +807e5c58 CtrlMenuCupSelectCup::OnCupButtonSelect +807e5d68 CtrlMenuCupSelectCup::OnCupButtonClick +807e5dc8 CtrlMenuCupSelectCup::IsCupUnlocked +807e5ecc CtrlMenuCupSelectCourse::Load +807e5ff0 CtrlMenuCupSelectCourse::UpdateTrackList +807e60dc CtrlMenuCupSelectCourse::InitSelf +807e61e0 CtrlMenuCupSelectCourse::OnUpdate +807e63d4 CtrlMenuCupSelectCourse::GetRuntimeTypeInfo +807e63e0 CtrlMenuCupSelectCup::GetRuntimeTypeInfo +807e63ec CtrlMenuCupcpp_sinit_ +807e6408 0x807e6408 +807e6414 0x807e6414 +807e6424 0x807e6424 +807e666c 0x807e666c +807e681c 0x807e681c +807e693c 0x807e693c +807e699c 0x807e699c +807e6b24 0x807e6b24 +807e6c10 0x807e6c10 +807e6d30 0x807e6d30 +807e6d34 0x807e6d34 +807e6d40 0x807e6d40 +807e6d4c 0x807e6d4c +807e6d68 CtrlMenuMRStageSelectLevelSub::GetClassName +807e6d74 CtrlMenuMRStageSelectLevel::GetClassName +807e6d84 CtrlMenuMRStageSelectStage::GetClassName +807e6d94 MRStageButton::GetClassName +807e6da4 MissionStageSelectLevel::OnInit +807e6f70 0x807e6f70 +807e7090 0x807e7090 +807e70e8 0x807e70e8 +807e7144 CtrlMenuMRStageSelectLevelSub::OnUpdate +807e73a0 0x807e73a0 +807e73a8 MissionStageSelectStage::OnInit +807e75dc 0x807e75dc +807e77b4 0x807e77b4 +807e780c 0x807e780c +807e79b8 0x807e79b8 +807e79c4 0x807e79c4 +807e79d0 0x807e79d0 +807e79dc CtrlMenuMRStageSelectLevelSub::GetRuntimeTypeInfo +807e79e8 0x807e79e8 +807e7a14 0x807e7a14 +807e7a1c 0x807e7a1c +807e7a24 0x807e7a24 +807e7a2c CtrlMenuMachineGraph::GetClassName +807e7a38 CtrlMenuMachineGraph::Load +807e7d88 MachineAbility::__ct +807e7dc4 MachineAbility::__dt +807e7e1c CtrlMenuMachineGraph::OnUpdate +807e7e20 CtrlMenuMachineGraph::OnLoad +807e7f44 MachineAbility::InitSelf +807e8118 CtrlMenuMachineGraph::GetRuntimeTypeInfo +807e8124 CtrlMenuMachineGraph_cpp_sinit +807e8138 CtrlMenuMovieHandler::Toggle +807e8188 CtrlMenuMovieHandler::IsMirror +807e82d0 CtrlMenuMovieButton::GetPaneNames +807e82dc CtrlMenuMovieLayout::GetPaneNames +807e82e8 CtrlMenuSingleTopMovieButton::GetPaneNames +807e82f4 CtrlMenuMovieButton::GetClassName +807e8300 CtrlMenuMovieButton::OnLoad +807e84a8 MovieScale::__ct +807e84ac CtrlMenuMovieButton::InitSelf +807e8698 CtrlMenuMovieButton::OnUpdate +807e879c CtrlMenuSingleTopMovieButton::GetClassName +807e87ac CtrlMenuSingleTopMovieButton::OnLoad +807e8954 CtrlMenuSingleTopMovieButton::GetMovieCount +807e895c CtrlMenuSingleTopMovieButton::InitSelf +807e8b48 CtrlMenuSingleTopMovieButton::OnUpdate +807e8c4c CtrlMenuMovieLayout::GetClassName +807e8c5c CtrlMenuMovieLayout::OnLoad +807e8e04 CtrlMenuMovieLayout::GetMovieCount +807e8e0c CtrlMenuMovieLayout::InitSelf +807e8ff8 CtrlMenuMovieLayout::OnUpdate +807e90fc CtrlMenuMovieLayout::GetRuntimeTypeInfo +807e9108 CtrlMenuSingleTopMovieButton::GetRuntimeTypeInfo +807e9114 CtrlMenuSingleTopMovieButton::__dt +807e9170 CtrlMenuMovieButton::GetRuntimeTypeInfo +807e917c CtrlMenuMovie_cpp_sinit_ +807e91a4 CtrlMenuMovieButton::__dt(PushButton) +807e91ac CtrlMenuMovieButton::GetClassName(PushButton) +807e91b4 CtrlMenuMovieButton::GetRuntimeTypeInfo(PushButton) +807e91bc CtrlMenuSingleTopMovieButton::OnUpdate(PushButton) +807e91c4 CtrlMenuSingleTopMovieButton::InitSelf(PushButton) +807e91cc CtrlMenuSingleTopMovieButton::OnLoad(PushButton) +807e91d4 CtrlMenuSingleTopMovieButton::__dt(PushButton) +807e91dc CtrlMenuSingleTopMovieButton::GetClassName(PushButton) +807e91e4 CtrlMenuSingleTopMovieButton::GetRuntimeTypeInfo(PushButton) +807e91ec CtrlMenuMovieLayout::OnUpdate(LayoutUIControl) +807e91f4 CtrlMenuMovieLayout::InitSelf(LayoutUIControl) +807e91fc CtrlMenuMovieLayout::__dt(LayoutUIControl) +807e9204 CtrlMenuMovieLayout::GetClassName(LayoutUIControl) +807e920c CtrlMenuMovieLayout::GetRuntimeTypeInfo(LayoutUIControl) +807e9214 CtrlMenuBackButton::GetClassName +807e9220 CtrlMenuDummyBack::GetClassName +807e9230 CtrlMenuPageTitleText::GetClassName +807e9240 CtrlMenuInstructionText::GetClassName +807e9250 CtrlMenuObiTop::GetClassName +807e9260 CtrlMenuObiBottom::GetClassName +807e9270 CtrlMenuBackButton::OnUpdate +807e93b8 CtrlMenuBackButton::OnInit +807e9410 CtrlMenuBackButton::SetPositionAnim +807e947c CtrlMenuPageTitleTextText::Load +807e9610 CtrlMenuBackButton::SetPlayerBitfield +807e9620 CtrlMenuDummyBack::Load +807e9724 CtrlMenuDummyBack::OnUpdate +807e987c CtrlMenuPageTitleText::SetMessage +807e9898 CtrlMenuPageTitleText::SetPositionAnim +807e9904 CtrlMenuInstructionText::Load +807e999c CtrlMenuInstructionText::SetPositionAnim +807e9a38 CtrlMenuInstructionText::SetMessage +807e9a68 CtrlMenuObiTop::SetPositionAnim +807e9ac0 0x807e9ac0 +807e9b18 0x807e9b18 +807e9b70 0x807e9b70 +807e9b7c CtrlMenuObiTop::__dt +807e9bd4 CtrlMenuObiTop::GetRuntimeTypeInfo +807e9be0 0x807e9be0 +807e9bec CtrlMenuPageTitleText::GetRuntimeTypeInfo +807e9bf8 CtrlMenuDummyBack::GetRuntimeTypeInfo +807e9c04 CtrlMenuBackButton::GetRuntimeTypeInfo +807e9c10 CtrlMenuText_cpp_sinit_ +807e9c44 CtrlMenuPressStart::GetClassName +807e9c50 CtrlMenuPressStart::InitSelf +807e9ccc CtrlMenuPressStart::OnUpdate +807e9cd0 CtrlMenuPressStart::Load +807e9d90 CtrlMenuPressStart::GetRuntimeTypeInfo +807e9d9c CtrlMenuPressStart_cpp_sinit_ +807e9db0 CtrlMenuTeamConfirmTeam::GetClassName +807e9dc0 CtrlMenuTeamConfirmTeam::Load +807e9f5c CtrlMenuTeamConfirmTeam::InitSelf +807ea240 CtrlMenuTeamConfirmTeam::GetRuntimeTypeInfo +807ea24c CtrlMenuTeamConfirmTeam_cpp_sinit_ +807ea270 CtrlRace2DMap::GetClassName +807ea27c CtrlRace2DMap::Load +807ea89c CtrlRace2DMapCharacer::__ct +807ea8d8 CtrlRaceBase::__dt +807ea930 CtrlRace2DMapObject::__dt +807ea98c CtrlRace2DMapCharacter::__dt +807ea9ec CtrlRace2DMapBossObj::__ct +807eaa28 CtrlRace2DMapBossObj::__dt +807eaa88 CtrlRace2DMapObject::SetMapPosition +807eaaa4 CtrlRace2DMapObject::SetMapRotation +807eaac0 CtrlRace2DMap::InitSelf +807eab3c CtrlRace2DMap::OnUpdate +807eab4c zz_807eab4c +807eac1c CtrlRace2DMapObject::GetClassName +807eac2c CtrlRace2DMapObject::InitSelf +807eac7c CtrlRace2DMapObject::GetPaneName +807eac88 CtrlRace2DMapObject::OnUpdate +807eacfc CtrlRace2DMapObject::CalculatePosition +807eadf0 CtrlRace2DMapCharacter::GetClassName +807eae00 CtrlRace2DMapCharacer::Load +807eaecc CtrlRace2DMapCharacter::InitSelf +807eb250 CtrlRace2DMapCharacer::OnUpdate +807eb3cc CtrlRace2DMapCharacter::CalculatePosition +807eb9f8 CtrlRace2DMapBossObj::GetClassName +807eba08 CtrlRace2DMapBossObj::Load +807eba60 CtrlRace2DMapBossObj::InitSelf +807ebd50 CtrlRace2DMapBossObj::OnUpdate +807ebe40 CtrlRaceBase::GetPane +807ebe48 CtrlRace2DMapBossObj::GetPaneName +807ebe54 CtrlRace2DMapBossObj::GetRuntimeTypeInfo +807ebe60 CtrlRace2DMapCharacter::GetPaneName +807ebe6c CtrlRace2DMapCharacter::GetRuntimeTypeInfo +807ebe78 CtrlRace2DMapObject::GetRuntimeTypeInfo +807ebe84 CtrlRace2DMap::__dt +807ebee0 CtrlRace2DMap::GetRuntimeTypeInfo +807ebeec CtrlRace2DMap_cpp_sinit_ +807ebf14 CtrlRaceBase::GetClassName +807ebf20 CtrlRaceBase::HudSlotColorEnable +807ec20c CtrlRaceBase::SetPanePosition +807ec30c CtrlRaceBase::setPausingPhase +807ec3b0 CtrlRaceBase::IsInactive +807ec3c4 CtrlRaceBase::HasStarted +807ec3e0 CtrlRaceBase::UpdatePausePosition +807ec6e8 CtrlRaceBase::InitSelf +807ec7f8 CtrlRaceBase::GetPlayerId +807ec8ec CtrlRaceBase::GetRuntimeTypeInfo +807ec8f8 0x807ec8f8 +807ec90c CtrlRaceBattleAddPoint::GetClassName +807ec918 CtrlRaceBattleAddPoint::Load +807ec9ac CtrlRaceBattleAddPoint::InitSelf +807eca8c CtrlRaceBattleAddPoint::OnUpdate +807ecd88 CtrlRaceBattlePoint::GetClassName +807ecd98 CtrlRaceBattlePoint::SetPanePosition +807ecf5c 0x807ecf5c +807ecfac CtrlRaceBattlePoint::SetPositionAnim +807ed098 CtrlRaceBattlePoint::Init +807ed134 CtrlRaceBattlePoint::OnUpdate +807ed1f0 CtrlRaceBattlePoint::Load +807ed258 CtrlRaceBattleTotalPoint::GetClassName +807ed268 CtrlRaceBattleTotalPoint::Init +807ed364 CtrlRaceBattleTotalPoint::OnUpdate +807ed9e8 CtrlRaceBattleTotalPoint::Load +807edb84 CtrlRaceBattleTotalPoint::__dt +807edbe0 CtrlRaceBattleTotalPoint::GetRuntimeTypeInfo +807edbec CtrlRaceBattlePoint::__dt +807edc48 CtrlRaceBattlePoint::GetRuntimeTypeInfo +807edc54 CtrlRaceBattleAddPoint::GetRuntimeTypeInfo +807edc60 CtrlRaceBattleAddPoint::__dt +807edcbc CtrlRaceBattlePoint_cpp_sinit_ +807edd98 CtrlRaceBattleSetPoint::GetClassName +807edda4 CtrlRaceBattleSetPoint::Load +807edeb8 CtrlRaceBattleSetPoint::InitSelf +807ee1d8 CtrlRaceBattleSetPoint::__dt +807ee230 CtrlRaceBattleSetPoint::GetRuntimeTypeInfo +807ee23c CtrlRaceBattleSetPoint_cpp_sinit_ +807ee250 CtrlRaceCount::GetClassName +807ee25c CtrlRaceCount::Load +807ee30c CtrlRaceCount::Init +807ee358 CtrlRaceCount::OnUpdate +807ee39c CtrlRaceCount::IsFadingIn +807ee3d4 CtrlRaceCount::FadeIn +807ee40c CtrlRaceCount::__dt +807ee468 CtrlRaceCount::GetRuntimeTypeInfo +807ee474 CtrlRaceCount_cpp_sinit +807ee488 CtrlRaceGhostDiffTime::GetClassName +807ee494 CtrlRaceGhostDiffTime::Init +807ee548 CtrlRaceGhostDiffTime::OnUpdate +807ee7c0 CtrlRaceGhostDiffTime::AddTimers +807ee860 CtrlRaceGhostDiffTime::SubtractTimers +807ee8e0 CtrlRaceGhostDiffTime::Load +807ee990 CtrlRaceGhostDiffTime::__dt +807eea14 CtrlRaceGhostDiffTime::GetRuntimeTypeInfo +807eea20 CtrlRaceGhostDiffTime_cpp_sinit +807eea34 CtrlRaceItemWindow::GetClassName +807eea40 CtrlRaceItemWindow::Init +807eee0c CtrlRaceItemWindow::IsInactive +807eeed4 CtrlRaceItemWindow::HasStarted +807eef64 CtrlRaceItemWindow::OnUpdate +807ef4c8 CtrlRaceItemWindow::Load +807ef530 CtrlRaceItemWindow::AnimateItemIcon +807ef660 CtrlRaceItemWindow::__dt +807ef6bc CtrlRaceItemWindow::GetRuntimeTypeInfo +807ef6c8 CtrlRaceItemWindow_cpp_sinit_ +807ef6dc CtrlRaceLap::GetClassName +807ef6e8 CtrlRaceLap::Init +807ef810 CtrlRaceLap::OnUpdate +807ef8d0 CtrlRaceLap::Load +807ef97c CtrlRaceLap::__dt +807ef9d8 CtrlRaceLap::GetRuntimeTypeInfo +807ef9e4 CtrlRaceLap_cpp_sinit_ +807ef9f4 CtrlRaceLive::getClassName +807efa00 CtrlRaceLive::initLayout +807efa70 screenElement_setupPauseMovement +807efa74 0x807efa74 +807efa78 CtrlRaceLiveMessage::getClassName +807efa88 CtrlRaceLiveMessage::initLayout +807efadc CtrlRaceLiveMessage::onInit +807efb90 CtrlRaceLiveMessage::onUpdate +807efc20 CtrlRaceLiveMessage::__dt +807efc7c CtrlRaceLiveMessage::getInstance +807efc88 CtrlRaceLive::__dt +807efce4 0x807efce4 +807efcf0 __sinit_CtrlRaceLive_cpp +807efd0c CtrlRaceNameBalloon:GetClassName +807efd18 CtrlRaceNameBalloon::Load +807efec8 CtrlRaceNameBalloon::InitSelf +807f0044 CtrlRaceNameBalloon::UpdateInfo +807f06e8 CtrlRaceNameBalloon::OnUpdate +807f0d30 0x807f0d30 +807f0ed4 0x807f0ed4 +807f0f98 0x807f0f98 +807f1094 CtrlRaceNameBalloon::UpdatePosition +807f132c RaceBalloons::Update +807f1b8c 0x807f1b8c +807f1bf0 0x807f1bf0 +807f1c34 RaceBalloons::AddNameBalloon +807f1c50 RaceBalloons::AddItemBalloon +807f1c58 RaceBalloons::__ct +807f1d44 RaceBalloons::Init +807f1da4 0x807f1da4 +807f1db4 RaceBalloons::OnItemObjTargeting +807f1f24 0x807f1f24 +807f1f40 CtrlRaceItemBalloon::GetClassName +807f1f50 CtrlRaceItemBalloon::Load +807f2118 CtrlRaceItemBalloon::InitSelf +807f22bc CtrlRaceItemBalloon::OnUpdate +807f2b74 0x807f2b74 +807f2c84 0x807f2c84 +807f34ec CtrlRaceItemBalloon::UpdateIcon +807f368c CtrlRaceItemBalloon::UpdatePositionForPlayer +807f3d68 0x807f3d68 +807f4550 0x807f4550 +807f4680 0x807f4680 +807f46c0 0x807f46c0 +807f47bc CtrlRaceItemBalloon::__dt +807f4814 CtrlRaceItemBalloon::GetRuntimeTypeInfo +807f4820 CtrlRaceNameBalloon::__dt +807f4878 CtrlRaceNameBalloon::GetRuntimeTypeInfo +807f4884 0x807f4884 +807f48f0 CtrlRaceRankNum::GetClassName +807f48fc CtrlRaceRankNum::Init +807f4a08 CtrlRaceRankNum::OnUpdate +807f4bb4 CtrlRaceRankNum::Load +807f4d40 CtrlRaceRankNum::IsInactive +807f4de0 CtrlRaceRankNum::__dt +807f4e3c CtrlRaceRankNum::GetRuntimeTypeInfo +807f4e48 CtrlRaceRankNum_cpp_sinit_ +807f4e5c CtrlRaceResult::GetClassName +807f4e68 CtrlRaceResult::Load +807f4fcc 0x807f4fcc +807f51d4 0x807f51d4 +807f52f4 CtrlRaceResult::FillName +807f5440 0x807f5440 +807f544c 0x807f544c +807f56d4 CtrlRaceResult::FillScore +807f579c 0x807f579c +807f595c CtrlRaceResult::FillFinishTime +807f5a4c CtrlRaceResult::Init +807f5a50 CtrlRaceResult::OnUpdate +807f5d24 CtrlRaceResult::807f5d24 +807f5e2c 0x807f5e2c +807f5f20 CtrlRaceResult::GetBRCTRName +807f5f9c CtrlRaceResult::CanEnd +807f5fec CtrlRaceResult::Fill +807f6490 CtrlRaceResult::__dt +807f64e8 CtrlRaceResult::GetRuntimeTypeInfo +807f64f4 CtrlRaceResult_cpp_sinit_ +807f6510 CtrlRaceResultTeam::GetClassName +807f651c CtrlRaceResultTeam::Load +807f69e4 CtrlRaceResultTeam::InitSelf +807f6fb8 CtrlRaceResultTeam::OnUpdate +807f7224 CtrlRaceResultTeam::IsResultAnimDone +807f73c0 CtrlRaceResultTeam::ComparePlayers +807f7458 CtrlResultAddPoint::GetClassName +807f7468 CtrlResultAddPoint::Load +807f7590 CtrlResultAddPoint::InitSelf +807f75e4 CtrlResultAddPoint::OnUpdate +807f7600 0x807f7600 +807f7654 CtrlResultAddPoint::GetRuntimeTypeInfo +807f7660 CtrlRaceResultTeam::GetRuntimeTypeInfo +807f766c CtrlResultAddPoint::__dt +807f76c8 CtrlRaceResultTeam_cpp_sinit_ +807f76ec CtrlRaceScore::GetClassName +807f76f8 CtrlRaceScore::InitSelf +807f784c CtrlRaceScore::OnUpdate +807f7914 CtrlRaceScore::Load +807f7970 CtrlRaceScore::SetMessage +807f7b48 CtrlRaceScore::__dt +807f7ba4 CtrlRaceScore::GetRuntimeTypeInfo +807f7bb0 CtrlRaceScore_cpp_sinit_ +807f7bc4 CtrlRaceTime::GetClassName +807f7bd0 CtrlRaceTime::__ct +807f7c6c CtrlRaceTime::InitSelf +807f7e64 CtrlRaceTime::GetPlayerMaxLap +807f7e80 CtrlRaceTime::GetPlayerId +807f7eb4 CtrlRaceTime::GetRacedata +807f7ec0 CtrlRaceTime::OnUpdate +807f8084 0x807f8084 +807f826c CtrlRaceTime::GetParentPageId +807f8274 CtrlRaceTime::GetParentPage +807f8280 CtrlRaceTime::IsPaused +807f8288 CtrlRaceTime::GetCurSection +807f8298 CtrlRaceTime::GetSectionId +807f82ac CtrlRaceTime::FillTimerGlobal +807f82bc CtrlRaceTime::HasPlayerFinished +807f82dc CtrlRaceTime::FillLapSplitTimer +807f82f4 CtrlRaceTime::IsBattle +807f8320 CtrlRaceTime::CheckTimer +807f8360 CtrlRaceTime::IsTimerInvalid +807f8380 CtrlRaceTime::ResetTimer +807f83a4 CtrlRaceTime::IsOnlineVS +807f83c4 CtrlRaceTime::IsModeFlag4 +807f83d0 CtrlRaceTime::IsPlayerLast +807f83f0 CtrlRaceTime::FillFinishTimeTimer +807f842c CtrlRaceTime::Load +807f84c0 CtrlRaceTime::SetTimer +807f8768 CtrlRaceTime::SetPositionAnim +807f8840 CtrlRaceTime::DisableFlashingAnimation +807f8878 CtrlRaceTime::EnableFlashingAnimation +807f88b0 CtrlRaceTime::Func_0x5c +807f88b8 CtrlRaceTime::Func_0x58 +807f88c0 CtrlRaceTime::OnDefocus +807f88e8 CtrlRaceTime::OnFocus +807f890c CtrlRaceTime::__dt +807f8968 CtrlRaceTime::GetRuntimeTypeInfo +807f8974 CtrlRaceTime_cpp_sinit_ +807f8988 CtrlRaceWifiStartMessage::GetClassName +807f8994 CtrlRaceWifiStartMessage::Load +807f8ba4 CtrlRaceWifiStartMessage::HasStarted +807f8bd8 CtrlRaceWifiStartMessage::IsInactive +807f8be8 CtrlRaceWifiStartMessage::SetPanePosition +807f8c34 CtrlRaceWifiStartMessage::OnUpdate +807f8c44 CtrlRaceWifiFinishMessage::GetClassName +807f8c54 CtrlRaceWifiFinishMessage::Load +807f8d08 CtrlRaceWifiFinishMessage::HasStarted +807f8d88 CtrlRaceWifiFinishMessage::IsInactive +807f8d90 CtrlRaceWifiFinishMessage::SetPanePosition +807f8f8c CtrlRaceWifiFinishMessage::OnUpdate +807f8f9c CtrlRaceWaitSymbol::GetClassName +807f8fac CtrlRaceWaitSymbol::Load +807f9068 CtrlRaceWaitSymbol::HasStarted +807f909c CtrlRaceWaitSymbol::IsInactive +807f90ac CtrlRaceWaitSymbol::SetPanePosition +807f90f8 CtrlRaceWaitSymbol::OnUpdate +807f9108 CtrlRaceWaitSymbol::GetPane +807f9110 CtrlRaceWaitSymbol::__dt +807f916c CtrlRaceWaitSymbol::GetRuntimeTypeInfo +807f9178 CtrlRaceWifiFinishMessage::GetPane +807f9180 CtrlRaceWifiFinishMessage::__dt +807f91dc CtrlRaceWifiFinishMessage::GetRuntimeTypeInfo +807f91e8 CtrlRaceWifiStartMessage::GetPane +807f91f0 CtrlRaceWifiStartMessage::__dt +807f924c CtrlRaceWifiStartMessage::GetRuntimeTypeInfo +807f9258 CtrlRaceWifi_cpp_sinit_ +807f9278 GeoHitTableHolder::__ct +807f9348 GeoHitTableHolder::__dt +807f9388 Objects::Airblock::__ct +807f9424 Objects::Airblock::__ct(Vec3s) +807f94cc Objects::Airblock::OnStart +807f957c ObjectCollisionPolyhedra::GetRadius +807f9584 Objects::Airblock::LoadCollision +807f96a4 Objects::Airblock::UpdateCollision +807f9770 Objects::Airblock::vf_0xb4 +807f983c Objects::Airblock::GetCollisionDiameter +807f9864 Objects::Airblock::IsCollidingNoTerrainInfo +807f9868 Objects::Airblock::IsCollidingAddEntryNoTerrainInfo +807f986c Objects::Airblock::vf_0xfc +807f9870 Objects::Airblock::IsCollidingAddEntry +807f9874 Objects::Airblock::IsCollidingNoTerrainInfoNoTriangleCheck(no radius) +807f9884 Objects::Airblock::IsCollidingAddEntryNoTerrainInfoNoTriangleCheck(no radius) +807f9894 Objects::Airblock::IsColliding(no radius) +807f98a4 Objects::Airblock::IsCollidingNoTriangleCheck(no radius) +807f98b4 Objects::Airblock::IsCollidingNoTerrainInfoNoTriangleCheck +807f98b8 Objects::Airblock::IsCollidingAddEntryNoTerrainInfoNoTriangleCheck +807f98bc Objects::Airblock::IsColliding +807f98c0 Objects::Airblock::IsCollidingNoTriangleCheck +807f98c4 Objects::Airblock::vf_0xc0 +807f98d4 Objects::Airblock::vf_0xc4 +807f98e4 Objects::Airblock::vf_0xc8 +807f98f4 Objects::Airblock::vf_0xcc +807f9904 Objects::Airblock::Update +807f9908 Objects::Airblock::GetPropertiesBitfield +807f9910 Objects::Airblock::__dt +807f9950 Objects::Airblock::IsCollidingNoTerrainInfoImpl +807f9c14 Objects::Airblock::IsCollidingAddEntryNoTerrainInfoImplImpl +807f9ef8 0x807f9ef8 +807fa51c Objects::Airblock::IsCollidingAddEntryImpl +807fab58 Objects::Aurora::__ct +807fabc4 Objects::Aurora::OnStart +807fac84 Objects::Aurora::Update +807faf10 0x807faf10 +807faffc 0x807faffc +807fb060 0x807fb060 +807fb33c Objects::Aurora::CalcOtherEntityDistance +807fb39c Objects::Aurora::UpdateOtherEntityPosWithRot +807fb520 Objects::Aurora::vf_0x11c +807fb528 Objects::Aurora::vf_0x108 +807fb534 Objects::Aurora::AreItemsAllowed +807fb53c Objects::Aurora::IsCollidingNoTerrainInfo +807fb540 Objects::Aurora::IsCollidingAddEntryNoTerrainInfo +807fb544 Objects::Aurora::vf_0xfc +807fb548 Objects::Aurora::IsCollidingAddEntry +807fb54c Objects::Aurora::IsCollidingNoTerrainInfoNoTriangleCheck(no radius) +807fb55c Objects::Aurora::IsCollidingAddEntryNoTerrainInfoNoTriangleCheck(no radius) +807fb56c Objects::Aurora::IsColliding(no radius) +807fb57c Objects::Aurora::IsCollidingNoTriangleCheck(no radius) +807fb58c Objects::Aurora::IsCollidingNoTerrainInfoNoTriangleCheck +807fb590 Objects::Aurora::IsCollidingAddEntryNoTerrainInfoNoTriangleCheck +807fb594 Objects::Aurora::IsColliding +807fb598 Objects::Aurora::IsCollidingNoTriangleCheck +807fb59c Objects::Aurora::vf_0xc0 +807fb5ac Objects::Aurora::vf_0xc4 +807fb5bc Objects::Aurora::vf_0xc8 +807fb5cc Objects::Aurora::vf_0xc8 +807fb5dc Objects::Aurora::GetCollisionDiameter +807fb5f0 Objects::Aurora::LoadAnimations +807fb67c Objects::Aurora::vf_0xb4 +807fb680 Objects::Aurora::UpdateCollision +807fb684 Objects::Aurora::LoadCollision +807fb688 Objects::Aurora::GetPropertiesBitfield +807fb690 Objects::Aurora::__dt +807fb6d0 Objects::Aurora::IsCollidingNoTerrainInfoImpl +807fb8b0 Objects::Aurora::IsCollidingAddEntryNoTerrainInfoImpl +807fbac0 0x807fbac0 +807fbe6c Objects::Aurora::IsCollidingAddEntryImpl +807fc248 0x807fc248 +807fc294 Objects::Belt::vf_0x120 +807fc578 Objects::BeltEasy::__ct +807fc5ec Objects::Belt::__dt +807fc62c Objects::BeltEasy::vf_0x124 +807fc6c8 Objects::BeltEasy::vf_0x128 +807fc764 Objects::BeltCrossing::__ct +807fc7d8 Objects::BeltCrossing::vf_0x124 +807fc874 Objects::BeltCrossing::vf_0x128 +807fc90c Objects::BeltCurveA::__ct +807fc9d4 Objects::BeltCurveA::vf_0x124 +807fcca4 Objects::BeltCurveA::vf_0x128 +807fccfc Objects::BeltCurveA::LoadModels +807fcde8 Objects::BeltCurveA::LoadAnimations +807fd0e8 Objects::BeltCurveA::UpdateModelMatrix +807fd1ac Objects::BeltCurveA::OnStart +807fd1b4 Objects::BeltCurveA::Update +807fd38c Objects::BeltCurveA::vf_0x12c +807fd5a0 0x807fd5a0 +807fd66c 0x807fd66c +807fd6a4 Objects::Belt::vf_0x11c +807fd6ac Objects::Belt::UpdateOtherEntityPosWithRot +807fd6b0 Objects::Belt::UpdateOtherEntityPos +807fd6b4 Objects::Belt::CalcOtherEntityDistance +807fd6b8 Objects::Belt::vf_0x108 +807fd6c4 Objects::Belt::AreItemsAllowed +807fd6cc Objects::Belt::IsCollidingNoTerrainInfo +807fd6d4 Objects::Belt::IsCollidingAddEntryNoTerrainInfo +807fd6dc Objects::Belt::vf_0xfc +807fd6e4 Objects::Belt::IsCollidingAddEntry +807fd6f4 Objects::Belt::IsCollidingNoTerrainInfoNoTriangleCheck(no radius) +807fd6fc Objects::Belt::IsCollidingAddEntryNoTerrainInfoNoTriangleCheck(no radius) +807fd704 Objects::Belt::IsColliding(no radius) +807fd70c Objects::Belt::IsCollidingAddEntryNoTriangleCheck(no radius) +807fd728 Objects::Belt::IsCollidingNoTerrainInfoNoTriangleCheck +807fd730 Objects::Belt::IsCollidingAddEntryNoTerrainInfoNoTriangleCheck +807fd738 Objects::Belt::IsColliding +807fd740 Objects::Belt::vf_0xc0 +807fd750 0x807fd750 +807fd758 Objects::Belt::vf_0xc4 +807fd760 Objects::Belt::vf_0xc8 +807fd768 Objects::Belt::vf_0xcc +807fd784 Objects::Belt::GetCollisionDiameter +807fd790 Objects::Belt::vf_0xb4 +807fd794 Objects::Belt::UpdateCollision +807fd798 Objects::Belt::LoadCollision +807fd79c Objects::Belt::GetPropertiesBitfield +807fd7a4 Objects::BeltCurveA::SetObjInfoCalcBit +807fd7b4 Objects::BeltCurveA::GetScnObjDrawOptionsIdx +807fd7bc Objects::BeltCurveA::__dt +807fd7fc Objects::Belt::SetObjInfoCalcBit +807fd80c Objects::Belt::vf_0x12c +807fd860 Objects::Belt::LoadAnimations +807fd8a0 Objects::BeltCrossing::GetScnObjDrawOptionsIdx +807fd8a8 Objects::BeltCrossing::__dt +807fd8e8 Objects::BeltEasy::GetScnObjDrawOptionsIdx +807fd8f0 Objects::BeltEasy::__dt +807fd930 Objects::Belt::GetScnObjDrawOptionsIdx +807fd938 0x807fd938 +807fda18 0x807fda18 +807fdae4 0x807fdae4 +807fdc50 0x807fdc50 +807fde5c 0x807fde5c +807fdfb4 0x807fdfb4 +807fdfdc 0x807fdfdc +807fe03c 0x807fe03c +807fe2cc 0x807fe2cc +807fe364 0x807fe364 +807fe4fc 0x807fe4fc +807fe52c 0x807fe52c +807fe534 0x807fe534 +807fe5e8 0x807fe5e8 +807fe5f0 0x807fe5f0 +807fe630 0x807fe630 +807fe658 Objects::Crane::__ct +807fe7ec Objects::Crane::Update +807fea58 ObjectExternKCL::vf_0x11c +807fea60 ObjectExternKCL::CalcOtherEntityDistance +807feac0 ObjectExternKCL::GetTransformationMatrix +807feaf0 Objects::Crane::GetPeriod +807feb20 Objects::Crane::GetPropertiesBitfield +807feb28 Objects::Crane::__dt +807feb68 0x807feb68 +807fec30 0x807fec30 +807fed5c 0x807fed5c +807fed80 0x807fed80 +807fefd8 0x807fefd8 +807ff17c 0x807ff17c +807ff59c 0x807ff59c +807ff83c 0x807ff83c +807ff90c 0x807ff90c +807ff980 0x807ff980 +807ff98c 0x807ff98c +807ffa24 0x807ffa24 +807ffa2c 0x807ffa2c +807ffa34 0x807ffa34 +807ffa74 0x807ffa74 +807ffa80 0x807ffa80 +807ffa88 0x807ffa88 +807ffa94 0x807ffa94 +807ffaa0 0x807ffaa0 +807ffae0 0x807ffae0 +807ffb20 0x807ffb20 +807ffdd4 0x807ffdd4 +80800000 0x80800000 +80800100 0x80800100 +808007d8 0x808007d8 +808008fc 0x808008fc +80800a10 0x80800a10 +80800abc 0x80800abc +80800fbc 0x80800fbc +808011cc 0x808011cc +808014ac 0x808014ac +8080178c 0x8080178c +808019f8 0x808019f8 +80801bb4 0x80801bb4 +808027a4 0x808027a4 +808029ac 0x808029ac +80802c70 0x80802c70 +80802c78 0x80802c78 +80802d00 0x80802d00 +80802d0c 0x80802d0c +80802d18 0x80802d18 +80802d20 0x80802d20 +80802d60 0x80802d60 +80802d68 0x80802d68 +80802d6c 0x80802d6c +80802d70 0x80802d70 +80802d80 0x80802d80 +80802d84 0x80802d84 +80802d90 0x80802d90 +80802d98 0x80802d98 +80802da0 0x80802da0 +80802ee0 0x80802ee0 +80803020 0x80803020 +80803160 0x80803160 +80803170 0x80803170 +80803264 0x80803264 +80803358 0x80803358 +8080344c 0x8080344c +80803540 0x80803540 +80803680 0x80803680 +808037c0 0x808037c0 +80803900 0x80803900 +80803910 0x80803910 +80803a04 0x80803a04 +80803af8 0x80803af8 +80803bec 0x80803bec +80803ce0 0x80803ce0 +80803cec 0x80803cec +80803cf0 0x80803cf0 +80803cf8 0x80803cf8 +80803d00 0x80803d00 +80803d40 Objects::bblock::__ct +80803da8 Objects::VolcanoPiece::__dt +80803de8 Objects::bblock::LoadModels +80803f6c Objects::VolcanoPiece::GetSubFileName +80803f74 Objects::bblock::OnStart +80803fcc Objects::bblock::UpdateModelMatrix +80804054 Objects::bblock::LoadAnimations +80804108 Objects::bblock::LoadCollision +808043c0 Objects::bblock::GetBRRESName +808043cc Objects::bblock::SetCollisionType +80804418 Objects::bblock::Init_eAnm +808044c0 Objects::VolcanoPiece::UpdateKCL +808045a8 Objects::VolcanoPiece::SetCollisionType +808045ac Objects::VolcanoPiece::IsCollidingNoTerrainInfo +80804730 Objects::VolcanoPiece::IsCollidingAddEntryNoTerrainInfo +808048b4 Objects::VolcanoPiece::vf_0xfc +80804a38 Objects::VolcanoPiece::IsCollidingAddEntry +80804a48 Objects::VolcanoPiece::IsCollidingNoTerrainInfoNoTriangleCheck(no radius) +80804b90 Objects::VolcanoPiece::IsCollidingAddEntryNoTerrainInfoNoTriangleCheck(no radius) +80804cd8 Objects::VolcanoPiece::IsColliding(no radius) +80804e20 Objects::VolcanoPiece::IsCollidingAddEntryNoTriangleCheck(no radius) +80804f68 Objects::VolcanoPiece::IsCollidingNoTerrainInfoNoTriangleCheck +808050ec Objects::VolcanoPiece::IsCollidingAddEntryNoTerrainInfoNoTriangleCheck +80805270 Objects::VolcanoPiece::IsColliding +808053f4 Objects::VolcanoPiece::IsCollidingAddEntryNoTriangleCheck +80805404 Objects::VolcanoPiece::vf_0xc0 +8080554c Objects::VolcanoPiece::vf_0xc4 +80805694 Objects::VolcanoPiece::vf_0xc8 +808057dc Objects::VolcanoPiece::vf_0xcc +80805924 Objects::VolcanoPiece::UpdateDiffPosVector +8080595c Objects::VolcanoPiece::GetTransformationMatrix +80805974 Objects::VolcanoPiece::GetPropertiesBitfield +8080597c Objects::bblock::UpdateModelScale +80805a0c Objects::bblock::__dt +80805a4c Objects::DKturibashiGCc::__ct +80805ad8 Objects::DKturibashiGCc::OnStart +80805c24 Objects::DKturibashiGCc::Update +80805e64 0x80805e64 +80805f44 0x80805f44 +808060d4 Objects::DKturibashiGCc::UpdateOtherEntityPos +80806268 Objects::DKturibashiGCc::UpdateOtherEntityPosWithRot +8080642c Objects::DKturibashiGCc::vf_0x11c +80806434 Objects::DKturibashiGCc::vf_0x108 +80806440 Objects::DKturibashiGCc::AreItemsAllowed +80806448 Objects::DKturibashiGCc::IsCollidingAddEntryNoTerrainInfo +8080644c Objects::DKturibashiGCc::IsCollidingAddEntryNoTerrainInfo +80806450 Objects::DKturibashiGCc::vf_0xfc +80806454 Objects::DKturibashiGCc::IsCollidingAddEntry +80806458 Objects::DKturibashiGCc::IsCollidingNoTerrainInfo(no radius) +80806468 Objects::DKturibashiGCc::IsCollidingAddEntryNoTerrainInfo(no radius) +80806478 Objects::DKturibashiGCc::vf_0xec(no radius) +80806488 Objects::DKturibashiGCc::IsCollidingAddEntry(no radius) +80806498 Objects::DKturibashiGCc::IsCollidingNoTerrainInfoNoTriangleCheck +8080649c Objects::DKturibashiGCc::IsCollidingAddEntryNoTerrainInfoNoTriangleCheck +808064a0 Objects::DKturibashiGCc::IsColliding +808064a4 Objects::DKturibashiGCc::IsCollidingNoTriangleCheck +808064a8 Objects::DKturibashiGCc::vf_0xc0 +808064b8 Objects::DKturibashiGCc::vf_0xc4 +808064c8 Objects::DKturibashiGCc::vf_0xc8 +808064d8 Objects::DKturibashiGCc::vf_0xcc +808064e8 Objects::DKturibashiGCc::GetCollisionDiameter +808064fc Objects::DKturibashiGCc::LoadAnimations +80806500 Objects::DKturibashiGCc::vf_0xb4 +80806504 Objects::DKturibashiGCc::UpdateCollision +80806508 0x80806508 +8080650c Objects::DKturibashiGCc::GetPropertiesBitfield +80806514 Objects::DKturibashiGCc::__dt +80806554 0x80806554 +808068a0 0x808068a0 +80806c24 0x80806c24 +80807110 Objects::DKturibashiGCc::IsCollidingImpl +8080761c Objects::Kinoko::__ct +808077ec Objects::Kinoko::OnStart +8080782c Objects::Kinoko::Update +80807950 Objects::Kinoko_ud::__ct +80807a14 Objects::Kinoko::__dt +80807a54 Objects::Kinoko_ud::UpdateSpecific +80807b7c Objects::Kinoko_bend::__ct +80807c98 Objects::Kinoko_bend::UpdateSpecific +80807d84 Objects::Kinoko::vf_0xb4 +80807d88 Objects::Kinoko::SetYScale +80807d8c Objects::Kinoko::GetSubFileName +80807dac Objects::Kinoko::GetPropertiesBitfield +80807db4 Objects::Kinoko_bend::__dt +80807df4 0x80807df4 +80807df8 Objects::Kinoko_ud::SetYScale +80807dfc Objects::Kinoko_ud::GetSubFileName +80807e1c Objects::Kinoko_ud::__dt +80807e5c Objects::Kinoko_kuki::vf_0xec +80807e60 Objects::Kinoko_kuki::vf_0xf0 +80807e64 Objects::Kinoko_kuki::GetSubFileName +80807e84 Objects::Kinoko_kuki::UpdateModelScale +80807e88 Objects::Kinoko_kuki::GetPropertiesBitfield +80807e90 Objects::Kinoko_kuki::__dt +80807ed0 ObjAmi_construct +80807f40 0x80807f40 +80807ff4 0x80807ff4 +80808080 0x80808080 +80808220 0x80808220 +80808308 0x80808308 +80808578 0x80808578 +808085bc 0x808085bc +8080861c 0x8080861c +80808784 0x80808784 +8080878c 0x8080878c +80808798 0x80808798 +808087a0 0x808087a0 +808087a4 0x808087a4 +808087a8 0x808087a8 +808087ac 0x808087ac +808087b0 0x808087b0 +808087c0 0x808087c0 +808087d0 0x808087d0 +808087e0 0x808087e0 +808087f0 0x808087f0 +808087f4 0x808087f4 +808087f8 0x808087f8 +808087fc 0x808087fc +80808800 0x80808800 +80808810 0x80808810 +80808820 0x80808820 +80808830 0x80808830 +80808840 0x80808840 +8080884c 0x8080884c +80808850 0x80808850 +80808854 0x80808854 +80808858 0x80808858 +80808860 0x80808860 +808088a0 0x808088a0 +80808a8c 0x80808a8c +80808ca8 0x80808ca8 +80809060 0x80809060 +80809448 Objects::TownBridgeDS::__ct +808095b8 Objects::TownBridgeDS::LoadCollision +80809774 Objects::TownBridgeDS::Update +80809cdc 0x80809cdc +80809fc4 0x80809fc4 +8080a01c 0x8080a01c +8080a0f0 Objects::TownBridgeDS::CalcOtherEntityDistance +8080a4ac Objects::TownBridgeDS::UpdateOtherEntityPosWithRot +8080a8b4 Objects::TownBridgeDS::vf_0x11c +8080a8bc Objects::TownBridgeDS::vf_0x108 +8080a8c8 Objects::TownBridgeDS::AreItemsAllowed +8080a8d0 Objects::TownBridgeDS::GetPeriod +8080a8dc Objects::TownBridgeDS::LoadAnimations +8080a964 Objects::TownBridgeDS::GetTransformationMatrix +8080acd8 Objects::TownBridgeDS::GetPropertiesBitfield +8080ace0 Objects::TownBridgeDS::__dt +8080ad20 0x8080ad20 +8080b0d8 obj_obakeblockSFC::__ct +8080b244 0x8080b244 +8080b318 0x8080b318 +8080b564 0x8080b564 +8080b578 0x8080b578 +8080bb28 0x8080bb28 +8080bc64 0x8080bc64 +8080bdc0 Object::GetCollisionDiameter +8080bdcc 0x8080bdcc +8080bdd4 0x8080bdd4 +8080bdd8 0x8080bdd8 +8080bddc 0x8080bddc +8080bde0 0x8080bde0 +8080bde4 0x8080bde4 +8080bde8 0x8080bde8 +8080bdec 0x8080bdec +8080bdf0 0x8080bdf0 +8080bdf4 0x8080bdf4 +8080be04 0x8080be04 +8080be14 0x8080be14 +8080be24 0x8080be24 +8080be34 0x8080be34 +8080be38 0x8080be38 +8080be3c 0x8080be3c +8080be40 0x8080be40 +8080be44 0x8080be44 +8080be54 0x8080be54 +8080be64 0x8080be64 +8080be74 0x8080be74 +8080be84 0x8080be84 +8080be90 0x8080be90 +8080be94 0x8080be94 +8080be98 0x8080be98 +8080be9c 0x8080be9c +8080bea4 0x8080bea4 +8080bee4 0x8080bee4 +8080c414 ObjectCollisionPolyhedra::SetRadius +8080c41c 0x8080c41c +8080c980 0x8080c980 +8080d12c 0x8080d12c +8080d8fc 0x8080d8fc +8080d970 0x8080d970 +8080d9a8 0x8080d9a8 +8080d9b4 0x8080d9b4 +8080d9c0 0x8080d9c0 +8080d9cc 0x8080d9cc +8080da30 0x8080da30 +8080da94 0x8080da94 +8080db08 0x8080db08 +8080dd74 0x8080dd74 +8080ded0 obj_quicksand::__ct +8080e0e8 0x8080e0e8 +8080e64c 0x8080e64c +8080edac 0x8080edac +8080f04c 0x8080f04c +8080f23c 0x8080f23c +8080f840 0x8080f840 +8080fe44 0x8080fe44 +80810000 0x80810000 +80810148 0x80810148 +80810170 0x80810170 +80810558 0x80810558 +80810560 0x80810560 +8081056c 0x8081056c +80810574 0x80810574 +80810578 0x80810578 +8081057c 0x8081057c +80810580 0x80810580 +80810584 0x80810584 +80810594 0x80810594 +808105a4 0x808105a4 +808105b4 0x808105b4 +808105c4 0x808105c4 +808105c8 0x808105c8 +808105cc 0x808105cc +808105d0 0x808105d0 +808105d4 0x808105d4 +808105e4 0x808105e4 +808105f4 0x808105f4 +80810604 0x80810604 +80810614 0x80810614 +80810628 0x80810628 +80810690 0x80810690 +8081069c 0x8081069c +808106a8 0x808106a8 +808106ac 0x808106ac +808106b0 0x808106b0 +808106b4 0x808106b4 +808106bc 0x808106bc +808106fc 0x808106fc +80810708 0x80810708 +80810714 0x80810714 +8081071c 0x8081071c +80810724 0x80810724 +80810780 0x80810780 +80810bbc 0x80810bbc +80811024 0x80811024 +80811720 0x80811720 +80811e48 0x80811e48 +80811f88 0x80811f88 +80812180 0x80812180 +80812188 0x80812188 +808122fc 0x808122fc +80812384 0x80812384 +8081247c 0x8081247c +80812848 0x80812848 +80812850 0x80812850 +80812b80 0x80812b80 +80812e48 0x80812e48 +808131f8 0x808131f8 +808135f4 0x808135f4 +808136ec 0x808136ec +80813ae4 0x80813ae4 +80813af0 0x80813af0 +80813b80 0x80813b80 +80813b8c 0x80813b8c +80813b94 0x80813b94 +80813bd4 Objects::TwistedWay::__ct +80813c40 Objects::TwistedWay::OnStart +80813cfc Objects::TwistedWay::Update +80813f40 0x80813f40 +80814020 0x80814020 +80814130 0x80814130 +80814270 0x80814270 +80814428 Objects::TwistedWay::CalcOtherEntityDistance +808145d8 Objects::TwistedWay::UpdateOtherEntityPosWithRot +808147a8 0x808147a8 +8081483c Objects::TwistedWay::vf_0x11c +80814844 Objects::TwistedWay::vf_0x108 +80814850 Objects::TwistedWay::AreItemsAllowed +80814858 Objects::TwistedWay::IsCollidingNoTerrainInfo +8081485c Objects::TwistedWay::IsCollidingAddEntryNoTerrainInfo +80814860 Objects::TwistedWay::vf_0xfc +80814864 Objects::TwistedWay::IsCollidingAddEntry +80814868 Objects::TwistedWay::IsCollidingNoTerrainInfoNoTriangleCheck(no radius) +80814878 Objects::TwistedWay::IsCollidingAddEntryNoTerrainInfoNoTriangleCheck(no radius) +80814888 Objects::TwistedWay::IsColliding(no radius) +80814898 Objects::TwistedWay::IsCollidingAddEntryNoTriangleCheck(no radius) +808148a4 0x808148a4 +808148a8 Objects::TwistedWay::IsCollidingNoTerrainInfoNoTriangleCheck +808148ac Objects::TwistedWay::IsCollidingAddEntryNoTerrainInfoNoTriangleCheck +808148b0 Objects::TwistedWay::IsColliding +808148b4 Objects::TwistedWay::IsCollidingAddEntryNoTriangleCheck +808148b8 Objects::TwistedWay::vf_0xc0 +808148c8 Objects::TwistedWay::vf_0xc4 +808148d8 Objects::TwistedWay::vf_0xc8 +808148e8 Objects::TwistedWay::vf_0xcc +808148f8 Objects::TwistedWay::GetCollisionDiameter +80814900 Objects::TwistedWay::LoadAnimations +80814904 Objects::TwistedWay::vf_0xb4 +80814908 Objects::TwistedWay::UpdateCollision +8081490c Objects::TwistedWay::LoadCollision +80814910 Objects::TwistedWay::GetPropertiesBitfield +80814918 Objects::TwistedWay::__dt +80814958 0x80814958 +80814ea8 0x80814ea8 +80815444 0x80815444 +80815d64 Objects::TwistedWay::IsCollidingImpl +808166d0 obj_venice_hasi::__ct +808168ac 0x808168ac +808169e0 0x808169e0 +80816ae4 0x80816ae4 +80816f38 0x80816f38 +8081719c 0x8081719c +80817308 0x80817308 +808173cc 0x808173cc +80817578 0x80817578 +808176e0 0x808176e0 +80817894 0x80817894 +8081789c 0x8081789c +808178a0 0x808178a0 +808178a4 0x808178a4 +808178b0 0x808178b0 +808178b8 0x808178b8 +808178c4 0x808178c4 +808178d4 0x808178d4 +808178dc 0x808178dc +808178e4 0x808178e4 +80817924 ObjSaku_construct +808179cc 0x808179cc +80817c60 0x80817c60 +80817da0 0x80817da0 +80817da8 0x80817da8 +80817de8 Objects::VolcanoPiece::__ct +80817f6c Objects::VolcanoPiece::LoadCollision +80818334 Objects::VolcanoPiece::UpdateCollisionPosition +80818674 Objects::VolcanoPiece::SetYScale +808187b4 Objects::VolcanoPiece::GetTransformationMatrixImpl +80818fcc 0x80818fcc +80819028 0x80819028 +80819114 0x80819114 +808193fc Objects::VolcanoPiece::Init_eAnm +80819400 Objects::VolcanoPiece::Update +808199a8 Objects::VolcanoPiece::IsCollidingAddEntryNoTriangleCheckImpl +80819da0 Objects::VolcanoPiece::IsCollidingImpl +8081a198 Objects::VolcanoRock1::__ct +8081a370 Objects::VolcanoRock1::Update +8081a414 Objects::VolcanoRock1::CalcTranslation +8081a5d0 Objects::VolcanoRock1::GetPeriod +8081a60c Objects::VolcanoRock1::GetTransformationMatrix +8081a668 Objects::VolcanoRock1::GetSubFileName +8081a688 Objects::VolcanoRock1::GetPropertiesBitfield +8081a690 Objects::VolcanoRock1::__dt +8081a6d0 ObjectKCL::__ct +8081a714 ObjectKCL::__ct(Vec3s, objId) +8081a758 ObjectKCL::__ct(Vec3s, name) +8081a79c ObjectKCL::Init +8081a85c ObjectKCL::RegisterEntity +8081a8d0 ObjectKCL::UpdateModel +8081a980 ObjectExternKCL::__ct +8081a9c8 0x8081a9c8 +8081aa10 0x8081aa10 +8081aa58 ObjectExternKCL::LoadCollision +8081ab4c ObjectExternKCL::InitCollision +8081ad6c ObjectExternKCL::UpdateCollisionPosition +8081af28 ObjectExternKCL::SetYScale +8081afb4 ObjectExternKCL::IsCollidingAddEntryNoTriangleCheckImpl +8081b16c ObjectExternKCL::IsCollidingImpl +8081b324 ObjectsKCLMgr::__ct +8081b380 ObjectsKCLMgr::__dt +8081b428 ObjectsKCLMgr::CreateInstance +8081b4b0 ObjectsKCLMgr::DestroyInstance +8081b500 ObjectsKCLMgr::Init +8081b5a8 0x8081b5a8 +8081b618 ObjectsKCLMgr::Update +8081b6c8 ObjectsKCLMgr::AddObject +8081b7cc ObjectsKCLMgr::UpdateKCL +8081b870 0x8081b870 +8081b940 0x8081b940 +8081ba10 0x8081ba10 +8081bb54 0x8081bb54 +8081bc98 ObjectsKCLMgr::IsCollidingNoTerrainInfoNoTriangleCheck +8081bd70 ObjectsKCLMgr::IsCollidingAddEntryNoTerrainInfoNoTriangleCheck +8081be48 ObjectsKCLMgr::IsColliding +8081bfa0 ObjectsKCLMgr::IsCollidingAddEntryNoTriangleCheck +8081c0f8 0x8081c0f8 +8081c1e8 0x8081c1e8 +8081c2d8 0x8081c2d8 +8081c43c 0x8081c43c +8081c5a0 ObjectsKCLMgr::IsCollidingNoTerrainInfo +8081c6b4 ObjectsKCLMgr::IsCollidingAddEntryNoTerrainInfo +8081c7c8 0x8081c7c8 +8081c958 ObjectsKCLMgr::IsCollidingAddEntry +8081cae8 0x8081cae8 +8081cb70 GetKartWeightClass +8081cd3c GetCharacterWeightClass +8081ceb0 0x8081ceb0 +8081cef4 CharacterIdToKartIdByIdx +8081cf70 0x8081cf70 +8081cfac 0x8081cfac +8081cfb4 IsKartUnlocked +8081d020 IsCharacterUnlocked +8081d028 0x8081d028 +8081d05c 0x8081d05c +8081d090 KartObjectCollision::Init +8081d0f8 KartObjectCollision::Init_switch +8081d0fc KartObjectCollision::Init_case1 +8081d16c KartObjectCollision::Init_case0 +8081d1b8 KartObjectCollision::Init_case2 +8081d22c KartObjectCollision::Init_case3 +8081d29c KartObjectCollision::Init_case6 +8081d2fc KartObjectCollision::Init_case9 +8081d358 KartObjectCollision::Init_casec +8081d3d4 KartObjectCollision::Init_casef +8081d430 KartObjectCollision::Init_case4 +8081d4b0 KartObjectCollision::Init_case7 +8081d524 KartObjectCollision::Init_casea +8081d58c KartObjectCollision::Init_cased +8081d610 KartObjectCollision::Init_case10 +8081d690 KartObjectCollision::Init_case5 +8081d718 KartObjectCollision::Init_case8 +8081d798 KartObjectCollision::Init_caseb +8081d818 KartObjectCollision::Init_casee +8081d89c KartObjectCollision::Init_case11 +8081d93c KartObjectCollision::Init_case12 +8081d998 KartObjectCollision::Init_case13 +8081d9f4 KartObjectCollision::Init_case14 +8081da54 KartObjectCollision::Init_case15 +8081dab4 KartObjectCollision::Init_case18 +8081db14 KartObjectCollision::Init_case1b +8081db70 KartObjectCollision::Init_case1e +8081dbcc KartObjectCollision::Init_case21 +8081dc2c KartObjectCollision::Init_case16 +8081dc8c KartObjectCollision::Init_case19 +8081dce8 KartObjectCollision::Init_case1c +8081dd44 KartObjectCollision::Init_case1f +8081dda4 KartObjectCollision::Init_case22 +8081de04 KartObjectCollision::Init_case17 +8081de64 KartObjectCollision::Init_case1a +8081dedc KartObjectCollision::Init_case1d +8081df3c KartObjectCollision::Init_case20 +8081df9c KartObjectCollision::Init_case23 +8081e0cc KartObjectCollision::__ct +8081e0e4 KartObjectCollision::__dt +8081e170 KartObjectCollision::Update +8081e254 ObjectCollisionPolyhedra::GetDefaultDiameter +8081e25c 0x8081e25c +8081e284 MenuModelBRRESHandle::__ct +8081e29c MenuModelBRRESHandle::__dt +8081e2dc 0x8081e2dc +8081e358 MenuModelBRRESHandle::BindDriverBRRES +8081e548 MenuModelBRRESHandle::BindKartBRRES +8081e658 MenuModelBRRESHandle::BindBackBRRES +8081e74c DriverModelAnmCalculator::__dt +8081e78c MenuModelBRRESHandle::LoadDriverModel +8081eac4 MenuModelBRRESHandle::LinkDriverAnim +8081edc0 MenuModelBRRESHandle::LoadKartModel +8081ef14 MenuModelBRRESHandle::LoadBackModel +8081efdc MenuModelBRRESHandle::MdlExists +8081efec ObjectCollidable::__ct(GOBJ) +8081f028 ObjectCollidable::::__ct(Vec3s, objId) +8081f064 ObjectCollidable::::__ct(Vec3s, name) +8081f0a0 ObjectCollidable::Init +8081f170 ObjectCollidable::RegisterManagedObject +8081f180 ObjectCollidable::RegisterEntity +8081f224 ObjectCollidable::LoadCollision +8081f548 ObjectCollidable::OnNormalCollision +8081f5d8 ObjectCollidable::OnWallCollision +8081f66c ObjectCollidable::BeforeKartCollision +8081f778 ObjectCollidable::BeforeItemCollision +8081f7c8 ObjectCollidable::UpdateCollision +8081f828 Object::__ct +8081f9bc Object::__ct(Vec3s, objId) +8081fb04 Object::__ct(Vec3s, name) +8081fc68 Object::OnStart +8081fcec Object::LoadModels +8081fd00 Object::LoadModels(r4) +8081fd10 Object::LoadGraphics +80820360 Object::LoadSound +808204b8 Object::LinkSound +808204fc Object::StartSoundLimited +8082051c Object::HoldSoundLimited +8082053c Object::StartNewSoundLimited +8082055c Object::StartSound +8082057c Object::HoldSound +8082059c Object::StopAllSound +808205bc Object::StopSound +808205dc Object::LoadShadow +8082072c Object::LoadAnimations +80820874 0x80820874 +80820980 Object::LoadRoute +80820a90 Object::LoadAnimationsByType +80820eb8 Object::LinkAnimations +80821070 Object::LoadClipInfo +80821640 Object::UpdateMatrix +808216c0 Object::ToggleVisible +80821738 0x80821738 +808217b0 0x808217b0 +808217b8 Object::UpdateModel +808218b0 Object::CalcYAxisTransMtx +80821910 0x80821910 +80821974 Object::UpdateShadow +80821a9c Object::UpdateModelMatrix +80821b68 0x80821b68 +80821db8 Object::vf_0x88 +80821dd8 Object::vf_0x8c +80821dec Object::DisableCollision +80821e00 Object::EnableCollision +80821e14 ObjectsMgr::CreateObject +80826940 0x80826940 +80826e8c ObjectsMgr::CreateAllObjects +80827a68 Objects::Kinoko_nm::GetPeriod +80827a74 Objects::Kinoko_nm::GetSubFileName +80827a94 Objects::Kinoko_nm::GetPropertiesBitfield +80827a9c Objects::Kinoko_nm::__dt +80827adc 0x80827adc +80827aec 0x80827aec +80827bcc 0x80827bcc +80827c0c 0x80827c0c +80827c14 0x80827c14 +80827c78 0x80827c78 +80827cb8 0x80827cb8 +80827cc8 0x80827cc8 +80827d08 0x80827d08 +80827d10 0x80827d10 +80827d50 0x80827d50 +80827d58 0x80827d58 +80827d98 0x80827d98 +80827dd8 0x80827dd8 +80827e18 0x80827e18 +80827e58 0x80827e58 +80827f74 0x80827f74 +808280e4 0x808280e4 +80828158 0x80828158 +8082840c 0x8082840c +80828428 0x80828428 +80828440 0x80828440 +80828564 0x80828564 +80828568 0x80828568 +80828570 0x80828570 +808285b0 Objects::Itembox::__ct +8082876c Objects::Itembox::__ct(position) +80828860 Objects::Itembox::Update +80828cb8 Objects::Itembox::OnCollision +80828f44 Objects::F_Itembox::__ct +80828f98 Objects::F_Itembox::OnStart +80829074 Objects::F_Itembox::Update +80829208 Objects::S_Itembox::__ct +80829280 Objects::S_Itembox::OnStart +8082945c Objects::S_Itembox::Update +80829584 Objects::S_Itembox::__dt +808295c4 Objects::F_Itembox::__dt +80829604 0x80829604 +8082963c Objects::Heyho2::__ct +808296f8 Objects::Heyho2::OnStart +80829764 Objects::Heyho2::Lift::__ct +808298dc Objects::Heyho2::Lift::OnStart +80829aa4 Objects::Heyho2::Lift::Update +80829c98 Objects::Heyho2::Lift::UpdateModelMatrix +80829d68 Objects::Heyho2::Rider::__ct +80829ed4 Objects::Heyho2::Rider::OnStart +8082a00c Objects::Heyho2::Rider::LoadAnimations +8082a010 Objects::Heyho2::Rider::Update +8082a134 Objects::Heyho2::LoadRoute +8082a138 Objects::Heyho2::Update +8082a13c Objects::Heyho2::LoadModels +8082a140 Objects::Heyho2::LoadAnimations +8082a144 Objects::Heyho2::GetPropertiesBitfield +8082a14c Objects::Heyho2::__dt +8082a18c Objects::Heyho2::Lift::GetBRRESName +8082a198 Objects::Heyho2::Lift::GetSubFileName +8082a1b8 Objects::Heyho2::Lift::GetPropertiesBitfield +8082a1c0 Objects::Heyho2::Lift::vf_0xa8 +8082a1c8 Objects::Heyho2::Lift::__dt +8082a208 Objects::Heyho2::Rider::LoadRoute +8082a20c Objects::Heyho2::Rider::vf_0xa8 +8082a214 Objects::Heyho2::Rider::GetBRRESName +8082a220 Objects::Heyho2::Rider::GetSubFileName +8082a26c Objects::Heyho2::Rider::GetPropertiesBitfield +8082a274 0x8082a274 +8082a2b4 ObjectsMgr::Start +8082a38c ObjectsMgr::__ct +8082a654 ObjectCollisionManager::__dt +8082a694 ObjectsMgr::__dt +8082a784 ObjectsMgr::CreateInstance +8082a824 ObjectsMgr::DestroyInstance +8082a884 0x8082a884 +8082a8f4 ObjectsMgr::Update +8082aac0 0x8082aac0 +8082ab04 ObjectsMgr::CheckCollision +8082adbc ObjectsMgr::HandleItemCollision +8082afec 0x8082afec +8082b0e8 ObjectsMgr::AddObject +8082b294 0x8082b294 +8082b3a0 ObjectsMgr::RegisterManagedObject +8082b3a8 ObjectsMgr::GetManagedObject +8082b3b8 ObjectsMgr::GetManagedObjectsCount +8082b3d4 ObjectsMgr::GetPseaVeniceHeightDiff +8082b3ec ObjectsMgr::GetPseaVeniceHeight +8082b400 ObjectsMgr::GetPseaVeniceHeightMinus260 +8082b420 0x8082b420 +8082b42c 0x8082b42c +8082b4e0 ObjectCollisionManager::GetPlayerItemState +8082b524 ObjectCollisionManager::Get_0x1c +8082b52c 0x8082b52c +8082b530 0x8082b530 +8082b534 ObjectCollisionManager::GetPosition +8082b53c GeoHitTableKartObjHolder::__dt +8082b57c GeoHitTableKartHolder::__dt +8082b5bc GeoHitTableObjHolder::__dt +8082b5fc GeoHitTableItemHolder::__dt +8082b63c Objects::MiiStatue::__ct +8082b708 0x8082b708 +8082b748 0x8082b748 +8082b7e8 0x8082b7e8 +8082b888 0x8082b888 +8082b928 0x8082b928 +8082ba18 Objects::MiiStatue::__dt +8082bae0 Objects::MiiStatue::OnStart +8082bce8 Objects::MiiStatue::Update +8082bd68 0x8082bd68 +8082be0c Objects::MiiStatue::GetPropertiesBitfield +8082be14 0x8082be14 +8082c10c ObjFlowHolder::__ct +8082c178 ObjFlowHolder::GetObjIDByName +8082c1f4 ObjFlowHolder::__dt +8082c234 Objects::Psea::__ct +8082c634 Objects::Psea::Update +8082c884 Objects::Psea::LoadModels +8082c888 Objects::Psea::GetPropertiesBitfield +8082c890 Objects::Psea::__dt +8082c8d0 Objects::Psea5spc::GetScnObjDrawOptionsIdx +8082c8d8 Objects::Psea5spc::GetName +8082c8e4 Objects::Psea5spc::GetSubFileName +8082c8f0 Objects::Psea5spc::GetPropertiesBitfield +8082c8f8 Objects::Psea5spc::__dt +8082c938 Objects::Psea4tex::GetScnObjDrawOptionsIdx +8082c940 Objects::Psea4tex::GetName +8082c94c Objects::Psea4tex::GetSubFileName +8082c958 Objects::Psea4tex::GetPropertiesBitfield +8082c960 Objects::Psea4tex::__dt +8082c9a0 Objects::Psea3nami::GetScnObjDrawOptionsIdx +8082c9a8 Objects::Psea3nami::GetName +8082c9b4 Objects::Psea3nami::GetSubFileName +8082c9c0 Objects::Psea3nami::GetPropertiesBitfield +8082c9c8 Objects::Psea3nami::__dt +8082ca08 Objects::Psea2dark::GetScnObjDrawOptionsIdx +8082ca10 Objects::Psea2dark::GetName +8082ca1c Objects::Psea2dark::GetSubFileName +8082ca28 Objects::Psea2dark::GetPropertiesBitfield +8082ca30 Objects::Psea2dark::__dt +8082ca70 Objects::Psea1sand::GetScnObjDrawOptionsIdx +8082ca78 Objects::Psea1sand::GetName +8082ca84 Objects::Psea1sand::GetSubFileName +8082ca90 Objects::Psea1sand::GetPropertiesBitfield +8082ca98 Objects::Psea1sand::__dt +8082cad8 Objects::Pylon01::_ct +8082cc64 Objects::Pylon01::GetScnObjDrawOptionsIdx +8082cc7c Objects::Pylon01::GetBRRESName +8082ccb4 Objects::Pylon01::LoadAnimations +8082cd60 Objects::Pylon01::OnStart +8082d044 Objects::Pylon01::Update +8082dbec Objects::Pylon01::OnCollision +8082dfd8 Objects::Pylon01::OnItemCollision +8082e100 0x8082e100 +8082e3f0 0x8082e3f0 +8082e4f4 Objects::Pylon01::OnWallCollision +8082e4f8 Objects::Pylon01::GetPropertiesBitfield +8082e500 Objects::Pylon01::__dt +8082e540 Objects::WLscreenGC::__ct +8082e608 Objects::WLscreenGC::__dt +8082e6a8 Objects::WLscreenGC::LoadModels +8082e84c Objects::WLscreenGC::OnStart +8082e850 Objects::WLscreenGC::Update +8082e854 obj_venice_nami::__ct +8082e9f4 0x8082e9f4 +8082ebe8 0x8082ebe8 +8082ec9c 0x8082ec9c +8082ef80 obj_venice_nami_update +8082f148 0x8082f148 +8082f318 0x8082f318 +8082f3ac 0x8082f3ac +8082f3b8 0x8082f3b8 +8082f3c0 0x8082f3c0 +8082f3c8 0x8082f3c8 +8082f408 BackModelMgr::__ct +8082f4d4 BackModelMgr::__dt +8082f514 BackModelMgr::Start +8082f5f8 BackModelMgr::Update +8082f780 BackModelMgr::SetBackModel +8082f84c BackModelMgr::ToggleVisible +8082f860 MenuDriverModel::__ct +8082f8b8 MenuVehicleDisplayModel::__dt +8082f8f8 MenuDriverModel::Init +8082fa90 0x8082fa90 +8082fb78 MenuDriverModel::SwitchState +8082ff0c MenuDriverModel::Update +80830000 0x80830000 +808300a0 0x808300a0 +80830134 0x80830134 +80830180 MenuDriverModelMgr::__ct +80830708 MenuDriverModel::__dt +80830748 0x80830748 +808307b0 MenuDriverModel::Update +80830a5c MenuModel::ToggleVisible +80830a80 MenuDriverModel::Draw +80830c3c MenuDriverModelMgr::TogglePlayerModel +80830c64 MenuDriverModelMgr::PrepareDriverOnKartAnms +80830d00 MenuDriverModel::SetPlayerCharacter +8083104c 0x8083104c +80831100 0x80831100 +8083144c 0x8083144c +80831534 0x80831534 +80831724 0x80831724 +80831af4 0x80831af4 +80831bd4 MenuDriverModelMgr::__dt +80831c14 MenuModel::Update +80831c18 MenuModel::Init +80831c1c DriverModelAnmCalculator::Determine +80831d20 DriverModelAnmCalculator::GetPrefix +80831e1c 0x80831e1c +80831e50 0x80831e50 +80831e58 0x80831e58 +80831eb0 0x80831eb0 +80831f08 0x80831f08 +80831f60 MenuKartModel::__ct +80831f84 MenuKartModel::__dt +80831fc4 MenuKartModel::Load +80832240 MenuKartModel::Init +8083275c MenuKartModel::Update +80832ae8 MenuKartModelMgr::__ct +80832cbc 0x80832cbc +80832cc0 0x80832cc0 +80832d00 MenuKartModelMgr::Load +80832f98 0x80832f98 +8083300c MenuKartModelMgr::Update +808331c4 MenuKartModelMgr::Draw +80833290 MenuKartModelMgr::SetKartModel +808332e8 MenuKartModelMgr::Reset +808333ac MenuKartModelMgr::ToggleVisible +80833444 0x80833444 +80833474 MenuKartModelMgr::GetMenuKartModel +808334a0 MenuKartModelMgr::__dt +808334e0 0x808334e0 +80833510 0x80833510 +80833638 GetCupNameMsgId +80833668 getTrackBmgId +80833704 0x80833704 +80833734 getArenaBmgId +80833764 GetPositionBMGId +8083376c getOrangeOverlayBmgId +80833774 0x80833774 +808337d4 0x80833774_switch +808337d8 0x80833774_caseD_18 +808337ec 0x80833774_caseD_1a +80833800 0x80833774_caseD_1c +8083381c GetMissionLevelNameMsgId +80833838 0x80833838 +80833840 ObjectCollisionCuboid::__ct +80833b00 ObjectCollisionCuboid::SetPositionAndScale +80833eec ObjectCollisionCuboid::SetPositionAndScale(translation) +808342e8 0x808342e8 +808342f0 ObjectCollisionCuboid::__dt +80834348 ObjectCollision::ProcessCollision +80834fb4 0x80834fb4 +8083504c 0x8083504c +808350e4 0x808350e4 +80835304 0x80835304 +80835650 0x80835650 +808357e4 0x808357e4 +808358cc 0x808358cc +808359a4 0x808359a4 +80835a8c 0x80835a8c +80835f34 0x80835f34 +80836034 0x80836034 +80836068 ObjectCollisionCylinder::__ct +8083618c ObjectCollisionCylinder::Getposition +808361f0 ObjectCollisionCylinder::SetPositionAndScale +80836334 ObjectCollisionCylinder::SetPositionAndScale(translation) +80836490 0x80836490 +80836498 ObjectCollisionCylinder::GetRadius +808364a0 ObjectCollisionCylinder::__dt +808364e0 ObjectCollisionPolyhedra::__ct +808365a8 ObjectCollisionPolyhedra::__dt +80836628 ObjectCollisionPolyhedra::GetPosition +808366d0 ObjectCollisionPolyhedra::SetPositionAndScale +808367c4 ObjectCollisionPolyhedra::SetPositionAndScale(translation) +808368d0 ObjectCollisionSphere::__ct +80836920 ObjectCollisionSphere::GetPosition +80836998 ObjectCollisionSphere::SetPositionAndScale +80836a50 ObjectCollisionSphere::SetPositionAndScale(translation) +80836b54 ObjectCollisionSphere::GetRadius +80836b5c ObjectCollisionSphere::__dt +80836b9c Pages::Menu::OnInit +80837164 Pages::Menu::CreateExternalControl +8083716c Pages::Menu::SetButtonHandlers +80837170 Pages::Menu::OnActivate +808372e0 Pages::Menu::GetTextInfo +808372e8 Pages::Menu::OnControlsInitialized +8083752c Pages::Menu::SelectButton +808375ec Pages::Menu::OnExternalButtonSelect +808375f0 Pages::Menu::SelectExternObject +8083761c Pages::Menu::LoadNextPageWithDelayById +80837720 Pages::Menu::LoadNextPageById +80837830 Pages::Menu::LoadPrevPageWithDelayById +80837934 Pages::Menu::LoadPrevPageById +80837a44 Pages::Menu::LoadPrevPageWithDelay +80837b4c Pages::Menu::LoadPrevPage +80837c5c Pages::Menu::ChangeSectionWithDelayById +80837d1c Pages::Menu::AddPageLayer +80837d44 Pages::Menu::OnResume +80837e40 Pages::Menu::ChangeSectionById +80837f04 Pages::Menu::ForceChangeSectionWithDelayById +80837f20 Pages::Menu::ForceChangeSectionById +80837f80 Pages::Menu::LoadMessageBoxPageWithDelay +8083813c Pages::Menu::LoadMessageBoxPage +80838300 Pages::Menu::LoadMessageBoxTransparentPageThenChangeSectionWithDelay +808384cc Pages::Menu::LoadMessageBoxTransparentPageThenChangeSection +808386a0 Pages::Menu::LoadMessageBoxTransparentPage +808387ac Pages::Menu::ChangePrevSection +808387f8 Pages::Menu::OnMoviesActivate +80838884 Pages::Menu::LoadMovies +808388a0 Pages::Menu::UnloadMovies +808388b4 Pages::Menu::IsMultiplayer +808388f0 Pages::Menu::AreAllPlayersActive +808389d4 Pages::Menu::BeforeEntranceAnimations +80838a70 Pages::Menu::GetNextPage +80838a78 Pages::Menu::BeforeExitAnimations +80838b14 Pages::Menu::OnStartPress +80838b18 MenuPage_cpp_sinit_ +80838b2c Pages::MenuBG::GetPageById +80838c3c Pages::MenuBG::CreateControl +80838da0 Pages::MenuBG::AfterControlUpdate +80838da4 Pages::MenuBG::GetPlayerBifield +80838dac Pages::MenuBG::GetActivePlayerBitfield +80838db4 Pages::MenuBG::GetManipulatorManager +80838dbc Pages::MenuBG::BeforeEntranceAnimations +80838dc0 Pages::MenuBG::__ct +80838e40 Pages::MenuBG::GetRuntimeTypeInfo +80838e4c MenuBGPage_cpp_sinit_ +80838e60 Pages::BattleCupSelect::GetPageById +80838f70 Pages::BattleCupSelect::OnInit +80838fe8 Pages::BattleCupSelect::CreateExternalControl +80838ff0 Pages::BattleCupSelect::CreateControl +80839084 Pages::BattleCupSelect::OnActivate +808393f4 Pages::BattleCupSelect::AfterControlUpdate +80839550 Pages::BattleCupSelect::UpdateText +8083955c Pages::BattleCupSelect::LoadNextPage +80839694 Pages::BattleCupSelect::OnButtonClick +80839904 Pages::BattleCupSelect::OnExternalButtonSelect +80839908 Pages::BattleCupSelect::OnButtonDeselect +8083990c Pages::BattleCupSelect::OnBackPress +80839b7c Pages::BattleCupSelect::OnResume +80839d94 Pages::BattleCupSelect::OnDeactivate +80839d98 Pages::BattleCupSelect::BeforeEntranceAnimations +80839df4 Pages::BattleCupSelect::BeforeExitAnimations +80839e50 Pages::BattleCupSelect::OnTimeout +80839f4c Pages::BattleCupSelect::GetPlayerBitfield +80839f54 Pages::BattleCupSelect::GetActivePlayerBitfield +80839f5c Pages::BattleCupSelect::SetButtonHandlers +80839fb8 Pages::BattleCupSelect::GetManipulatorManager +80839fc0 Pages::BattleCupSelect::__dt +8083a0b4 Pages::BattleCupSelect::GetRuntimeTypeInfo +8083a0c0 BattleCupSelectPage_sinit_ +8083a0d4 Pages::BattleKartSelect::GetPageById +8083a1e4 Pages::BattleKartSelect::OnInit +8083a1f8 Pages::BattleKartSelect::CreateExternalControl +8083a2e0 Pages::BattleKartSelect::SetButtonHandlers +8083a33c Pages::BattleKartSelect::CreateControl +8083a458 Pages::BattleKartSelect::OnActivate +8083a78c Pages::BattleKartSelect::AfterControlUpdate +8083a918 Pages::BattleKartSelect::OnButtonClick +8083aaa4 Pages::BattleKartSelect::OnExternalButtonSelect +8083ab78 Pages::BattleKartSelect::OnButtonDeselect +8083ab80 Pages::BattleKartSelect::OnBackPress +8083ac54 Pages::BattleKartSelect::Func_8083ac54 +8083ad54 Pages::BattleKartSelect::Func_8083ad54 +8083af3c Pages::BattleKartSelect::BeforeEntranceAnimations +8083af9c Pages::BattleKartSelect::BeforeExitAnimations +8083affc Pages::BattleKartSelect::GetPlayerBitfield +8083b004 Pages::BattleKartSelect::GetLocalPlayerCount +8083b00c Pages::BattleKartSelect::GetManipulatorManager +8083b014 Pages::BattleKartSelect::__dt +8083b0c0 Pages::BattleKartSelect::GetRuntimeTypeInfo +8083b0cc BattleKartSelectPagecpp_sinit_ +8083b0e0 Pages::BattleModeSelect::GetPageById +8083b1f0 Pages::BattleModeSelect::OnInit +8083b244 Pages::BattleModeSelect::GetManipulatorManager +8083b24c Pages::BattleModeSelect::CreateExternalControl +8083b3f8 Pages::BattleModeSelect::CreateControl +8083b400 Pages::BattleModeSelect::OnActivate +8083b7d8 Pages::BattleModeSelect::OnActivate_switch +8083b7dc Pages::BattleModeSelect::OnActivate_case_0 +8083b7f4 Pages::BattleModeSelect::OnActivate_case_1 +8083b80c Pages::BattleModeSelect::OnActivate_case_2 +8083b824 Pages::BattleModeSelect::OnActivate_case_3 +8083b83c Pages::BattleModeSelect::OnActivate_case_4 +8083b854 Pages::BattleModeSelect::OnActivate_case_5 +8083b86c Pages::BattleModeSelect::OnActivate_case_6 +8083b884 Pages::BattleModeSelect::OnActivate_case_7 +8083b8cc Pages::BattleModeSelect::AfterControlUpdate +8083b8d0 Pages::BattleModeSelect::OnButtonClick +8083b9ac Pages::BattleModeSelect::OnExternalButtonSelect +8083b9e4 Pages::BattleModeSelect::OnButtonDeselect +8083b9e8 Pages::BattleModeSelect::OnBackPress +8083b9f4 Pages::BattleModeSelect::OnDeactivate +8083ba40 Pages::BattleModeSelect::GetPlayerBitfield +8083ba48 Pages::BattleModeSelect::GetActivePlayerBitfield +8083ba50 Pages::BattleModeSelect::SetButtonHandlers +8083baac Pages::BattleModeSelect::__dt +8083bb2c Pages::BattleModeSelect::GetRuntimeTypeInfo +8083bb38 BattleModeSelectPage_cpp_sinit_ +8083bb4c Pages::BattleSettings::GetPageById +8083bc5c Pages::BattleSettings::OnInit +8083bf50 Pages::BattleSettings::CreateExternalControl +8083bfe0 Pages::BattleSettings::CreateControl +8083c3a4 Pages::BattleSettings::OnActivate +8083c504 Pages::BattleSettings::AfterControlUpdate +8083c508 Pages::BattleSettings::OnButtonClick +8083c50c Pages::BattleSettings::OnExternalButtonSelect +8083c538 Pages::BattleSettings::OnButtonDeselect +8083c53c Pages::BattleSettings::OnBackPress +8083c580 Pages::BattleSettings::OnRadioClick +8083c6ec Pages::BattleSettings::OnRadioChange +8083c7cc Pages::BattleSettings::OnUpDownClick +8083c838 Pages::BattleSettings::OnUpDownSelect +8083c848 Pages::BattleSettings::OnUpDownChange +8083c8c0 Pages::BattleSettings::OnTextChange +8083c93c Pages::BattleSettings::GetPlayerBitfield +8083c944 Pages::BattleSettings::GetActivePlayerBitfield +8083c94c Pages::BattleSettings::SetButtonHandlers +8083c9a8 Pages::BattleSettings::GetManipulatorManager +8083c9b0 Pages::BattleSettings::__dt +8083ca78 Pages::BattleSettings::GetRuntimeTypeInfo +8083ca84 Pages::BattleSettings::TriggerUpDown3APtmf +8083cab4 Pages::BattleSettings::TriggerRadioPtmf +8083cae4 Pages::BattleSettings::TriggerTextPtmf +8083cb14 Pages::BattleSettings::TriggerUpDown2APtmf +8083cb44 BattleSettingsPage_cpp_sinit_ +8083cb58 Pages::BattleStageSelect::GetPageById +8083cc68 Pages::BattleStageSelect::OnInit +8083ccb8 Pages::BattleStageSelect::CreateExternalControl +8083ccc0 Pages::BattleStageSelect::CreateControl +8083cd54 Pages::BattleStageSelect::OnActivate +8083cea8 Pages::BattleStageSelect::BeforeEntranceAnimations +8083cec8 Pages::BattleStageSelect::AfterControlUpdate +8083cfe4 Pages::BattleStageSelect::UpdateBottomText +8083cfe8 Pages::BattleStageSelect::LoadNextPage +8083d11c Pages::BattleStageSelect::OnButtonClick +8083d154 Pages::BattleStageSelect::OnExternalButtonSelect +8083d158 Pages::BattleStageSelect::OnButtonDeselect +8083d15c Pages::BattleStageSelect::OnBackPress +8083d194 Pages::BattleStageSelect::BeforeExitAnimations +8083d1f0 Pages::BattleStageSelect::OnDeactivate +8083d1f4 Pages::BattleStageSelect::OnTimeout +8083d2c8 Pages::BattleStageSelect::GetPlayerBitfield +8083d2d0 Pages::BattleStageSelect::GetActivePlayerBitfield +8083d2d8 Pages::BattleStageSelect::SetButtonHandlers +8083d334 Pages::BattleStageSelect::__dt +8083d420 Pages::BattleStageSelect::GetRuntimeTypeInfo +8083d42c BattleStageSelectPage_cpp_sinit_ +8083d440 CharaName::GetClassName +8083d44c Pages::CharacterSelect::GetPageById +8083d55c Pages::CharacterSelect::OnInit +8083d840 CharaName::__ct +8083d87c Pages::CharacterSelect::CreateExternalControl +8083d884 Pages::CharacterSelect::CreateControl +8083da78 Pages::CharacterSelect::OnActivate +8083ddb4 Pages::CharacterSelect::GetManipulatorManager +8083ddbc Pages::CharacterSelect::AfterControlUpdate +8083dfa8 Pages::CharacterSelect::OnButtonDriverClick +8083e5f4 Pages::CharacterSelect::OnButtonDriverSelect +8083e740 Pages::CharacterSelect::OnButtonClick +8083e8d8 Pages::CharacterSelect::OnExternalButtonSelect +8083e924 Pages::CharacterSelect::OnButtonDeselect +8083e928 Pages::CharacterSelect::OnBackPress +8083ec28 Pages::CharacterSelect::8083ec28 +8083ee1c Pages::CharacterSelect::8083ee1c +8083eebc Pages::CharacterSelect::OnResume +8083ef3c Pages::CharacterSelect::OnDispose +8083ef4c Pages::CharacterSelect::BeforeEntranceAnimations +8083eff4 Pages::CharacterSelect::BeforeExitAnimations +8083f09c CharaName::InitSelf +8083f260 Pages::CharacterSelect::GetPlayerBitfield +8083f268 Pages::CharacterSelect::GetActivePlayerBitfield +8083f270 Pages::CharacterSelect::SetButtonHandlers +8083f2cc Pages::CharacterSelect::__dt +8083f35c Pages::CharacterSelect::GetRuntimeTypeInfo +8083f368 CharaName::GetRuntimeTypeInfo +8083f374 CharaSelectcpp_sinit_ +8083f398 Pages::GPClassSelect::GetPageById +8083f4a8 Pages::GPClassSelect::OnInit +8083f578 Pages::GPClassSelect::GetManipulatorManager +8083f580 Pages::GPClassSelect::CreateExternalControl +8083f7b0 Pages::GPClassSelect::CreateControl +8083f838 Pages::GPClassSelect::OnActivate +8083f914 Pages::GPClassSelect::AfterControlUpdate +8083f988 Pages::GPClassSelect::OnButtonClick +8083fb2c Pages::GPClassSelect::OnExternalButtonSelect +8083fbb4 Pages::GPClassSelect::OnButtonDeselect +8083fbb8 Pages::GPClassSelect::OnBackPress +8083fbc4 Pages::GPClassSelect::OnDeactivate +8083fbc8 Pages::GPClassSelect::OnStartPress +808400cc Pages::GPClassSelect::GetPlayerBitfield +808400d4 Pages::GPClassSelect::GetActivePlayerBitfield +808400dc Pages::GPClassSelect::SetButtonHandlers +80840138 Pages::GPClassSelect::__dt +808401c4 Pages::GPClassSelect::GetRuntimeTypeInfo +808401d0 GPClassSelectPage_cpp_sinit_ +808401e4 Pages::CourseSelect::GetPageById +808402f4 Pages::CourseSelect::OnInit +80840390 Pages::CourseSelect::CreateExternalControl +80840398 Pages::CourseSelect::CreateControl +808404a0 Pages::CourseSelect::OnActivate +808405c0 Pages::CourseSelect::BeforeEntranceAnimations +8084060c Pages::CourseSelect::AfterControlUpdate +80840728 Pages::CourseSelect::UpdateBottomText +80840830 Pages::CourseSelect::LoadNextPage +80840a44 Pages::CourseSelect::OnButtonClick +80840aac Pages::CourseSelect::OnExternalButtonSelect +80840ab0 Pages::CourseSelect::OnButtonDeselect +80840ab4 Pages::CourseSelect::OnBackPress +80840b1c Pages::CourseSelect::BeforeExitAnimations +80840b78 Pages::CourseSelect::OnDeactivate +80840ba0 Pages::CourseSelect::OnTimeout +80840c74 Pages::CourseSelect::GetPlayerBitfield +80840c7c Pages::CourseSelect::GetActivePlayerBitfield +80840c84 Pages::CourseSelect::SetButtonHandlers +80840ce0 Pages::CourseSelect::__dt +80840dcc Pages::CourseSelect::GetRuntimeTypeInfo +80840dd8 Pages::CourseSelect::_sinit_ +80840dec Pages::CupSelect::GetPageById +80840efc Pages::CupSelect::OnInit +80841000 Pages::CupSelect::CreateExternalControl +80841008 Pages::CupSelect::CreateControl +80841110 Pages::CupSelect::OnActivate +808414b0 Pages::CupSelect::GetManipulatorManager +808414b8 Pages::CupSelect::AfterControlUpdate +80841614 Pages::CupSelect::UpdateText +80841744 Pages::CupSelect::LoadNextPage +808418e8 Pages::CupSelect::OnButtonClick +80841b4c Pages::CupSelect::OnExternalButtonSelect +80841b50 Pages::CupSelect::OnButtonDeselect +80841b54 Pages::CupSelect::OnBackPress +80841de4 Pages::CupSelect::OnResume +80841ff8 Pages::CupSelect::OnDeactivate +80842020 Pages::CupSelect::BeforeEntranceAnimations +8084207c Pages::CupSelect::BeforeExitAnimations +808420d8 Pages::CupSelect::OnTimeout +808421d4 Pages::CupSelect::GetPlayerBitfield +808421dc Pages::CupSelect::GetActivePlayerBitfield +808421e4 Pages::CupSelect::SetButtonHandlers +80842240 Pages::CupSelect::__dt +80842334 Pages::CupSelect::GetRuntimeTypeInfo +80842340 CupSelectPagecpp_sinit_ +80842354 Pages::StartRace::GetPageById +80842464 Pages::StartRace::OnInit +8084247c Pages::StartRace::CreateExternalControl +808425e0 Pages::StartRace::GetActivePlayerBitfield +808425e8 Pages::StartRace::SetButtonHandlers +80842644 Pages::StartRace::CreateControl +808428a0 Pages::StartRace::OnActivate +80842914 Pages::StartRace::AfterControlUpdate +80842948 Pages::StartRace::OnButtonClick +808429a4 Pages::StartRace::OnExternalButtonSelect +808429a8 Pages::StartRace::OnButtonDeselect +808429ac Pages::StartRace::OnBackPress +808429c8 Pages::StartRace::GetPlayerBitfield +808429d0 Pages::StartRace::GetManipulatorManager +808429d8 Pages::StartRace::__dt +80842a58 Pages::StartRace::GetRuntimeTypeInfo +80842a64 StartRacePage_cpp_sinit_ +80842a78 0x80842a78 +80842b88 0x80842b88 +80842dac 0x80842dac +80842e50 0x80842e50 +80842ec0 0x80842ec0 +80842fac 0x80842fac +80842fb4 0x80842fb4 +80842fb8 0x80842fb8 +80843384 0x80843384 +80843398 0x80843398 +8084339c 0x8084339c +80843684 0x80843684 +8084368c 0x8084368c +808436e8 0x808436e8 +808436f0 0x808436f0 +80843794 0x80843794 +808437a0 0x808437a0 +808437b4 0x808437b4 +808438c4 0x808438c4 +808438fc 0x808438fc +80843904 0x80843904 +80843998 0x80843998 +808439e0 0x808439e0 +808439e4 0x808439e4 +808439f0 0x808439f0 +80843a4c 0x80843a4c +80843a60 0x80843a60 +80843a64 0x80843a64 +80843a68 0x80843a68 +80843a74 0x80843a74 +80843ac0 0x80843ac0 +80843b14 0x80843b14 +80843b68 0x80843b68 +80843b70 0x80843b70 +80843b78 0x80843b78 +80843bd4 0x80843bd4 +80843bdc 0x80843bdc +80843cd0 0x80843cd0 +80843cdc 0x80843cdc +80843cf0 0x80843cf0 +80843e00 0x80843e00 +80843e70 0x80843e70 +80843efc 0x80843efc +80843f6c 0x80843f6c +80844050 0x80844050 +80844054 0x80844054 +80844138 0x80844138 +80844170 0x80844170 +80844174 Pages::Menu::UnloadVideoSource +80844180 0x80844180 +80844184 0x80844184 +8084418c 0x8084418c +80844194 0x80844194 +808441f0 0x808441f0 +808441f8 0x808441f8 +80844298 0x80844298 +808442a4 0x808442a4 +808442b8 0x808442b8 +808443c8 0x808443c8 +808443dc 0x808443dc +808443e4 0x808443e4 +80844478 Pages::Menu::OnActivate +8084447c 0x8084447c +8084449c 0x8084449c +808444a0 0x808444a0 +808444a4 0x808444a4 +80844500 0x80844500 +8084455c 0x8084455c +80844560 0x80844560 +80844564 0x80844564 +808445b4 0x808445b4 +80844608 0x80844608 +80844654 0x80844654 +8084465c 0x8084465c +80844664 0x80844664 +808446c0 0x808446c0 +808447ac 0x808447ac +808447b8 0x808447b8 +808447cc Pages::KartSelect::GetPageById +808448dc ButtonMachine::GetClassName +808448e8 Pages::KartSelect::GetVehicleID +80844960 Pages::KartSelect::GetUnlockedVehiclesCountByWeightAndType +808449dc Pages::KartSelect::GetUnlockedVehiclesCountByWeight +80844abc Pages::KartSelect::GetMinUnlockedVehiclesCount +80844d68 Pages::KartSelect::OnInit +80845148 Pages::KartSelect::CreateExternalControl +80845150 Pages::KartSelect::CreateControl +808454b4 Pages::KartSelect::SetButtonHandlers +80845510 Pages::KartSelect::OnActivate +80846a1c Pages::KartSelect::GetManipulatorManager +80846a24 Pages::KartSelect::GetLocalPlayerCount +80846a2c Pages::KartSelect::AfterControlUpdate +80846c1c Pages::KartSelect::OnButtonClick +80846ea4 Pages::KartSelect::OnExternalButtonSelect +80846fbc Pages::KartSelect::OnButtonSelect +80846fc0 Pages::KartSelect::OnBackPress +80847094 Pages::KartSelect::GetExternalControl +808471c4 Pages::KartSelect::GetButtonMachineById +80847344 Pages::KartSelect::InitButtonMachine +8084745c Pages::KartSelect::8084745c +80847590 Pages::KartSelect::80847590 +80847630 Pages::KartSelect::BeforeEntranceAnimations +80847690 Pages::KartSelect::BeforeExitAnimations +808476f0 ButtonMachine::OnUpdate +80847810 ButtonMachine::InitSelf +808478f4 zz_8478f4 +80847bec Pages::KartSelect::GetPlayerBitfield +80847bf4 Pages::KartSelect::__dt +80847c90 Pages::KartSelect::GetRuntimeTypeInfo +80847c9c ButtonMachine::GetRuntimeTypeInfo +80847ca8 ButtonMachine::__dt +80847d00 KartSelect_cpp_sinit +80847d24 Pages::MiiSelect::GetPageById +80847e34 MiiName::GetClassName +80847e40 Pages::MiiSelect::OnINit +80848044 Pages::MiiSelect::GetManipulatorManager +8084804c Pages::MiiSelect::CreateExternalControl +808481cc Pages::MiiSelect::GetActivePlayerBitfield +808481d4 Pages::MiiSelect::SetButtonHandlers +80848230 Pages::MiiSelect::CreateControl +80848568 Pages::MiiSelect::OnActivate +808487e8 Pages::MiiSelect::AfterControlUpdate +808487ec Pages::MiiSelect::OnButtonClick +80848ae8 Pages::MiiSelect::OnExternalButtonSelect +80848b58 Pages::MiiSelect::OnButtonDeselect +80848b60 Pages::MiiSelect::OnBackPress +80848bd4 Pages::MiiSelect::OnDeactivate +80848be8 Pages::MiiSelect::OnRightArrowPress +80848d40 Pages::MiiSelect::OnLeftArrowPress +80848e44 Pages::MiiSelect::OnTabButtonClick +80848f94 Pages::MiiSelect::SetMiiIcons +80849234 Pages::MiiSelect::GetCurTabMiiCount +80849284 MiiName::InitSelf +80849448 Pages::MiiSelect::GetPlayerBitfield +80849450 Pages::MiiSelect::__dt +808494f8 Pages::MiiSelect::GetRuntimeTypeInfo +80849504 MiiName::GetRuntimeTypeInfo +80849510 Pages::MiiSelect::TriggerTabPtmf +80849540 Pages::MiiSelect::TriggerSheetPtmf +80849570 MiiSelect_cpp_sinit_ +80849594 Pages::MultiKartSelect::GetPageById +808496a4 Pages::MultiKartSelect::OnInit +80849940 Pages::MultiKartSelect::CreateExternalControl +80849948 Pages::MultiKartSelect::CreateControl +80849c24 Pages::MultiKartSelect::OnActivate +80849d80 Pages::MultiKartSelect::AfterControlUpdate +80849e64 Pages::MultiKartSelect::GetManipulatorManager +80849e6c Pages::MultiKartSelect::OnButtonClick +80849f60 Pages::MultiKartSelect::OnExternalButtonSelect +80849f64 Pages::MultiKartSelect::OnButtonDeselect +80849f68 Pages::MultiKartSelect::OnBackPress +8084a100 Pages::MultiKartSelect::OnUpDownClick +8084a2a4 Pages::MultiKartSelect::OnUpDownChange +8084a2a8 Pages::MultiKartSelect::OnTextChange +8084a3f8 Pages::MultiKartSelect::FillPossibleKarts +8084a5c4 0x8084a5c4 +8084a668 Pages::MultiKartSelect::BeforeEntranceAnimations +8084a710 Pages::MultiKartSelect::BeforeExitAnimations +8084a7b8 Pages::MultiKartSelect::GetPlayerBitfield +8084a7c0 Pages::MultiKartSelect::GetActivePlayerBitfield +8084a7c8 Pages::MultiKartSelect::SetButtonHandlers +8084a824 Pages::MultiKartSelect::__dt +8084a904 Pages::MultiKartSelect::GetRuntimeTypeInfo +8084a910 TriggerPtmf +8084a940 TriggerPtmf +8084a970 TriggerPtmf +8084a9a0 MultiKartSelectPage_cpp_sinit_ +8084a9b4 Pages::MultiDriftSelect::GetPageById +8084aac4 Pages::MultiDriftSelect::OnInit +8084af7c Pages::MultiDriftSelect::GetManipulatorManager +8084af84 Pages::MultiDriftSelect::GetActivePlayerBitfield +8084af8c Pages::MultiDriftSelect::GetPlayerBitfield +8084af94 Pages::MultiDriftSelect::SetButtonHandlers +8084aff0 Pages::MultiDriftSelect::OnControlsInitialized +8084aff4 Pages::MultiDriftSelect::CreateExternalControl +8084b150 Pages::MultiDriftSelect::CreateControl +8084b280 Pages::MultiDriftSelect::OnActivate +8084b388 Pages::MultiDriftSelect::AfterControlUpdate +8084b500 Pages::MultiDriftSelect::OnButtonClick +8084b7ec Pages::MultiDriftSelect::OnExternalButtonSelect +8084b7f0 Pages::MultiDriftSelect::OnButtonDeselect +8084b7f4 Pages::MultiDriftSelect::OnBackPress +8084b8c4 Pages::MultiDriftSelect::OnResume +8084b984 Pages::MultiDriftSelect::__dt +8084ba04 Pages::MultiDriftSelect::GetRuntimeTypeInfo +8084ba10 MultiDriftSelectPage_cpp_sinit_ +8084ba24 Pages::MultiTeamSelect::GetPageById +8084bb34 Pages::MultiTeamSelect::OnInit +8084bffc Pages::MultiTeamSelect::GetManipulatorManager +8084c004 Pages::MultiTeamSelect::GetActivePlayerBitfield +8084c00c Pages::MultiTeamSelect::GetPlayerBifield +8084c014 Pages::MultiTeamSelect::SetButtonHandlers +8084c070 Pages::MultiTeamSelect::OnControlsInitialized +8084c074 Pages::MultiTeamSelect::BeforeEntranceAnimations +8084c11c Pages::MultiTeamSelect::BeforeExitAnimations +8084c1c4 Pages::MultiTeamSelect::CreateExternalControl +8084c314 Pages::MultiTeamSelect::CreateControl +8084c444 Pages::MultiTeamSelect::OnActivate +8084c714 Pages::MultiTeamSelect::AfterControlUpdate +8084c718 Pages::MultiTeamSelect::OnButtonClick +8084cc1c Pages::MultiTeamSelect::OnExternalButtonSelect +8084cc20 Pages::MultiTeamSelect::OnButtonDeselect +8084cc24 Pages::MultiTeamSelect::OnBackPress +8084cd30 Pages::MultiTeamSelect::OnResume +8084ce1c Pages::MultiTeamSelect::AssignCPUTeams +8084cff4 Pages::MultiTeamSelect::IsDifferentTeams +8084d050 Pages::MultiTeamSelect::__dt +8084d0d0 Pages::MultiTeamSelect::GetRuntimeTypeInfo +8084d0dc MultiTeamSelectPage_cpp_sinit_ +8084d0f0 Pages::MultiPlayer::GetPageById +8084d200 Pages::MultiPlayer::OnINit +8084d260 Pages::MultiPlayer::CreateExternalControl +8084d3a8 Pages::MultiPlayer::CreateControl +8084d3b0 Pages::MultiPlayer::OnActivate +8084d4d8 Pages::MultiPlayer::AfterControlUpdate +8084d4dc Pages::MultiPlayer::OnButtonClick +8084d6fc Pages::MultiPlayer::OnExternalButtonSelect +8084db7c Pages::MultiPlayer::OnButtonDeselect +8084db80 Pages::MultiPlayer::OnBackPress +8084dbd4 Pages::MultiPlayer::OnDeactivate +8084dbd8 Pages::MultiPlayer::GetPlayerBifield +8084dbe0 Pages::MultiPlayer::GetActivePlayerBitfield +8084dbe8 Pages::MultiPlayer::SetButtonHandlers +8084dc44 Pages::MultiPlayer::GetManipulatorManager +8084dc4c Pages::MultiPlayer::__dt +8084dccc Pages::MultiPlayer::GetRuntimeTypeInfo +8084dcd8 MultiPlayerPage_cpp_sinit_ +8084dcec DriftSelectPage:GetPageById +8084ddfc Pages::DriftSelect::OnInit +8084dee0 Pages::DriftSelect::GetManipulatorManager +8084dee8 Pages::DriftSelect::CreateExternalControl +8084e134 Pages::DriftSelect::CreateControl +8084e13c Pages::DriftSelect::OnActivate +8084e25c Pages::DriftSelect::AfterControlUpdate +8084e388 Pages::DriftSelect::OnButtonClick +8084e6bc Pages::DriftSelect::OnExternalButtonSelect +8084e6f4 Pages::DriftSelect::OnButtonDeselect +8084e6f8 Pages::DriftSelect::OnBackPress +8084e704 Pages::DriftSelect::OnDeactivate +8084e750 Pages::DriftSelect::GetPlayerBitfield +8084e758 Pages::DriftSelect::GetActivePlayerBitfield +8084e760 Pages::DriftSelect::SetButtonHandlers +8084e7bc Pages::DriftSelect::__dt +8084e83c Pages::DriftSelect::GetRuntimeTypeInfo +8084e848 DriftSelectPage_cpp_sinit_ +8084e85c Pages::TopMenuOverlay::GetPageById +8084e96c Pages::TopMenuOverlay::CreateControl +8084ed4c Pages::TopMenuOverlay::vf_0x98 +8084ed54 Pages::TopMenuOverlay::GetPlayerBitfield +8084ed5c Pages::TopMenuOverlay::GetActivePlayerBitfield +8084ed64 Pages::TopMenuOverlay::GetManipulatorManager +8084ed6c Pages::TopMenuOverlay::__dt +8084edec Pages::TopMenuOverlay::GetRuntimeTypeInfo +8084edf8 TopMenuOverlayPagecpp_sinit_ +8084ee0c Pages::SinglePlayer::GetPageById +8084ef1c Pages::SinglePlayer::OnInit +8084efbc Pages::SinglePlayer::GetManipulatorManager +8084efc4 Pages::SinglePlayer::CreateExternalControl +8084f108 Pages::SinglePlayer::CreateControl +8084f110 Pages::SinglePlayer::OnActivate +8084f294 Pages::SinglePlayer::AfterControlUpdate +8084f298 Pages::SinglePlayer::OnButtonClick +8084f688 Pages::SinglePlayer::OnExternalButtonSelect +8084fb34 Pages::SinglePlayer::OnButtonDeselect +8084fb38 Pages::SinglePlayer::OnBackPress +8084fb8c Pages::SinglePlayer::OnDeactivate +8084fb90 Pages::SinglePlayer::GetPlayerBitfield +8084fb98 Pages::SinglePlayer::GetActivePlayerBitfield +8084fba0 Pages::SinglePlayer::SetButtonHandlers +8084fbfc Pages::SinglePlayer::__dt +8084fc7c Pages::SinglePlayer::GetRuntimeTypeInfo +8084fc88 SinglePlayerPage_cpp_sinit_ +8084fc9c Pages::VSTeamsView::GetPageById +8084fdac Pages::VSTeamsView::OnInit +8084fdb0 Pages::VSTeamsView::CreateExternalControl +8084fe40 Pages::VSTeamsView::CreateControl +8084fea8 Pages::VSTeamsView::OnActivate +8084feac Pages::VSTeamsView::AfterControlUpdate +8084feb0 Pages::VSTeamsView::OnButtonClick +8084ff80 Pages::VSTeamsView::OnExternalButtonSelect +8084ffb8 Pages::VSTeamsView::OnButtonDeselect +8084ffbc Pages::VSTeamsView::OnBackPress +8084ffc8 Pages::VSTeamsView::AssignTeams +808501a0 Pages::VSTeamsView::GetPlayerBitfield +808501a8 Pages::VSTeamsView::GetActivePlayerBitfield +808501b0 Pages::VSTeamsView::SetButtonHandlers +8085020c Pages::VSTeamsView::GetManipulatorManager +80850214 Pages::VSTeamsView::__dt +808502ac Pages::VSTeamsView::GetRuntimeTypeInfo +808502b8 VSTeamsViewPage_cpp_sinit_ +808502cc Pages::MainMenu::GetPageById +808503dc Pages::MainMenu::OnInit +808504dc Pages::MainMenu::GetManipulatorManager +808504e4 Pages::MainMenu::CreateExternalControl +80850580 Pages::MainMenu::CreateControl +80850d54 Pages::MainMenu::SetButtonHandlers +80850db0 Pages::MainMenu::OnActivate +80851108 Pages::MainMenu::AfterControlUpdate +8085110c Pages::MainMenu::OnButtonClick +8085151c Pages::MainMenu::OnExternalButtonSelect +808515a0 Pages::MainMenu::OnButtonDeselect +808516ac Pages::MainMenu::OnBackPress +808516bc Pages::MainMenu::ResetSectionParamsAndRacedataScenario1 +80851b88 Pages::MainMenu::OnDeactivate +80851b8c Pages::MainMenu::ShowMessageBox +80851c84 Pages::MainMenu::GetPlayerBitfield +80851c8c Pages::MainMenu::GetActivePlayerBitfield +80851c94 Pages::MainMenu::__dt +80851d2c Pages::MainMenu::GetRuntimeTypeInfo +80851d38 MainMenuPage_cpp_sinit_ +80851d4c 0x80851d4c +80851e5c 0x80851e5c +80851e68 0x80851e68 +80851e70 0x80851e70 +80851f70 0x80851f70 +80851fdc 0x80851fdc +80851fe0 0x80851fe0 +80851ff4 0x80851ff4 +80851ff8 0x80851ff8 +80851ffc Pages::Menu::UnloadVideoSource +80852008 0x80852008 +8085200c 0x8085200c +80852014 0x80852014 +8085201c 0x8085201c +80852078 0x80852078 +80852080 0x80852080 +8085210c 0x8085210c +80852118 0x80852118 +8085212c Pages::VSModeSelect::GetPageById +8085223c Pages::VSModeSelect::OnInit +80852290 Pages::VSModeSelect::GetManipulatorManager +80852298 Pages::VSModeSelect::CreateExternalControl +80852448 Pages::VSModeSelect::CreateControl +80852450 Pages::VSModeSelect::OnActivate +80852908 Pages::VSModeSelect::OnActivate_switch +8085290c Pages::VSModeSelect::OnActivate_case_0 +80852924 Pages::VSModeSelect::OnActivate_case_1 +8085293c Pages::VSModeSelect::OnActivate_case_2 +80852954 Pages::VSModeSelect::OnActivate_case_3 +8085296c Pages::VSModeSelect::OnActivate_case_4 +80852984 Pages::VSModeSelect::OnActivate_case_5 +8085299c Pages::VSModeSelect::OnActivate_case_6 +808529b4 Pages::VSModeSelect::OnActivate_case_7 +808529fc Pages::VSModeSelect::AfterControlUpdate +80852a00 Pages::VSModeSelect::OnButtonClick +80852ae0 Pages::VSModeSelect::OnExternalButtonSelect +80852b18 Pages::VSModeSelect::OnButtonDeselect +80852b1c Pages::VSModeSelect::OnBackPress +80852b28 Pages::VSModeSelect::OnDeactivate +80852b74 Pages::VSModeSelect::GetPlayerBitfield +80852b7c Pages::VSModeSelect::GetActivePlayerBitfield +80852b84 Pages::VSModeSelect::SetButtonHandlers +80852be0 Pages::VSModeSelect::__dt +80852c60 Pages::VSModeSelect::GetRuntimeTypeInfo +80852c6c VSModeSelectPage_cpp_sinit_ +80852c80 Pages::VSSettings::GetPageById +80852d90 Pages::VSSettings::OnInit +80853028 Pages::VSSettings::CreateExternalControl +808530b8 Pages::VSSettings::CreateControl +8085354c Pages::VSSettings::OnActivate +80853650 Pages::VSSettings::AfterControlUpdate +80853654 Pages::VSSettings::OnButtonClick +80853684 Pages::VSSettings::OnExternalButtonSelect +808536b0 Pages::VSSettings::OnButtonDeselect +808536b4 Pages::VSSettings::OnBackPress +80853714 Pages::VSSettings::OnRadioClick +8085389c Pages::VSSettings::OnRadioChange +8085391c Pages::VSSettings::OnUpDownClick +80853988 Pages::VSSettings::OnUpDownSelect +80853998 Pages::VSSettings::OnUpDownChange +80853a10 Pages::VSSettings::OnTextChange +80853a8c Pages::VSSettings::OnDispose +80853a9c Pages::VSSettings::GetPlayerBitfield +80853aa4 Pages::VSSettings::GetLocalPlayerCount +80853aac Pages::VSSettings::SetButtonHandlers +80853b08 Pages::VSSettings::GetManipulatorManager +80853b10 Pages::VSSettings::__dt +80853bd8 Pages::VSSettings::GetRuntimeTypeInfo +80853be4 Pages::VSSettings::TriggerUpDown3APtmf +80853c14 Pages::VSSettings::TriggerRadioPtmf +80853c44 Pages::VSSettings::TriggerTextPtmf +80853c74 Pages::VSSettings::TriggerUpDown2APtmf +80853ca4 VSSettingsPage_cpp_sinit_ +80853cb8 Pages::Unlock::GetPageById +80853dc8 0x80853dc8 +80853e18 Pages::Unlock::OnInit +80853e30 Pages::Unlock::CreateExternalControl +80853ec0 Pages::Unlock::CreateControl +80854104 Pages::Unlock::OnActivate +80854320 Pages::Unlock::AfterControlUpdate +8085447c Pages::Unlock::OnButtonClick +80854748 Pages::Unlock::OnExternalButtonSelect +8085474c Pages::Unlock::OnButtonDeselect +80854750 Pages::Unlock::OnBackPress +80854754 Pages::Unlock::SetTitleBMG +80854810 Pages::Unlock::PrepareControls +80854a80 0x80854a80 +80854ae0 0x80854ae0 +80854b40 Pages::Unlock::SetModels +80854c3c Pages::Unlock::SetModels_switch +80854c40 Pages::Unlock::SetModels_caseD_1b +80854c4c Pages::Unlock::SetModels_caseD_10 +80854c54 Pages::Unlock::SetModels_caseD_d +80854c60 Pages::Unlock::SetModels_caseD_b +80854c6c Pages::Unlock::SetModels_caseD_e +80854c78 Pages::Unlock::SetModels_caseD_11 +80854c84 Pages::Unlock::SetModels_caseD_12 +80854cd8 0x80854cd8 +80854d28 0x80854d28 +80854d64 0x80854d64 +80854e00 Pages::Unlock::SetMessageWindowMessage +80854fa4 0x80854fa4 +808550bc Pages::Unlock::GetPlayerBifield +808550c4 Pages::Unlock::GetActivePlayerBitfield +808550cc Pages::Unlock::SetButtonHandlers +80855128 Pages::Unlock::GetManipulatorManager +80855130 Pages::Unlock::__dt +808551e0 Pages::Unlock::GetRuntimeTypeInfo +808551ec UnlockPage_cpp_sinit_ +80855200 Pages::RaceIntro::OnInit +80855644 Pages::RaceIntro::__dt +808556c4 Pages::RaceIntro::GetRuntimeTypeInfo +808556d0 RaceIntroPage_cpp_sinit_ +808556e4 Pages::TTSplits::__ct +80855854 Pages::TTSplits::Unload +80855864 Pages::TTSplits::__dt +80855924 Pages::TTSplits::OnInit +80855adc Pages::TTSplits::AfterControlUpdate +80855b30 Pages::TTSplits::BeforeEntranceAnimations +8085616c Pages::TTSplits::BeforeExitAnimations +808561dc Pages::TTSplits::GetNextPage +80856228 Pages::TTSplits::GetRuntimeTypeInfo +80856234 Pages::TTSplits::TriggerAction +80856264 Pages::TTSplits::_sinit_ +80856278 Pages::RaceHUD::OnInit +80856664 Pages::RaceHUD::InitActions +808569ac Pages::RaceHUD::OnNextPlayerSwitch +808569b4 Pages::RaceHUD::OnPrevPlayerSwitch +808569bc Pages::RaceHUD::OnPause +80856b74 Pages::RaceHUD::ChangeFocusedPlayer +80856ccc Pages::RaceHUD::GetLocalPlayerCount +80856cec Pages::RaceHUD::BeforeControlUpdate +808572d4 Pages::RaceHUD::HasPlayerRaceEnded +8085732c Pages::RaceHUD::HasRaceEndedReplay +808573ac Pages::RaceHUD::HasRaceEnded +808574b8 Pages::RaceHUD::AfterControlUpdate +80857ac8 0x80857ac8 +80857b08 Pages::RaceHUD::GetCtrlRaceBaseCount +80857cc0 Pages::RaceHUD::InitCtrlRaceBase +80858828 CtrlRaceCount::__ct +80858864 RaceBalloons::__dt +808588cc CtrlRaceNameBalloon::__ct +8085891c CtrlRaceItemBalloon::__ct +8085896c Pages::TournamentHUD::GetEnabledCtrlRaceBases +80858a18 Pages::RaceHUD::OnDeactivate +80858a24 Pages::RaceHUD::OnDispose +80858a34 Pages::RaceHUD::ChangeSectionByReinit +80858a50 Pages::RaceHUD::OnItemObjTargeting +80858ae4 Pages::RaceHUD::UpdateRaceBalloons +80858b88 Pages::TournamentHUD::GetCtrlRaceNameBalloonCount +80858b90 Pages::TournamentHUD::GetPausePageId +80858b98 Pages::TournamentHUD::__dt +80858c04 Pages::TournamentHUD::GetRuntimeTypeInfo +80858c10 Pages::RaceHUD::GetRuntimeTypeInfo +80858c1c TriggerPtmf +80858c4c 0x80858c4c +80858ca4 Pages::RaceMenu::__ct +80858d60 Pages::RaceMenu::OnInit +80859a2c Pages::RaceMenu::OnActivate +80859e98 Pages::RaceMenu::AfterControlUpdate +80859f30 Pages::RaceMenu::BeforeEntranceAnimations +80859f34 Pages::RaceMenu::BeforeExitAnimations +80859fa0 Pages::RaceMenu::OnDeactivate +80859fa4 Pages::RaceMenu::OnDispose +8085a03c 0x8085a03c +8085a098 Pages::RaceMenu::OnStartPress +8085a0f4 Pages::RaceMenu::OnButtonClick +8085a1c0 0x8085a1c0 +8085a208 0x8085a208 +8085a298 0x8085a298 +8085a580 0x8085a580 +8085a5e0 0x8085a5e0 +8085a67c 0x8085a67c +8085a6dc 0x8085a6dc +8085a80c 0x8085a80c +8085a9b0 0x8085a9b0 +8085ac54 0x8085ac54 +8085ade8 0x8085ade8 +8085b024 0x8085b024 +8085b0d8 0x8085b0d8 +8085b18c 0x8085b18c +8085b3bc 0x8085b3bc +8085b468 0x8085b468 +8085b4bc 0x8085b4bc +8085b568 0x8085b568 +8085b5e8 0x8085b5e8 +8085b6d8 0x8085b6d8 +8085b73c 0x8085b73c +8085b7bc 0x8085b7bc +8085b81c 0x8085b81c +8085b8c8 0x8085b8c8 +8085b930 0x8085b930 +8085b9dc 0x8085b9dc +8085ba44 0x8085ba44 +8085baf0 0x8085baf0 +8085bb9c 0x8085bb9c +8085bc10 Pages::RaceMenu::IsFinalRace +8085bd58 Pages::RaceMenu::GetRuntimeTypeInfo +8085bd64 TriggerPtmf +8085bd94 TriggerPtmf +8085bdc4 0x8085bdc4 +8085be44 Pages::Leaderboard::__ct +8085bf94 Pages::Leaderboard::OnActivate +8085c070 0x8085c070 +8085c0c4 Pages::Leaderboard::AfterControlUpdate +8085c16c 0x8085c16c +8085c390 Pages::Leaderboard::GetRuntimeTypeInfo +8085c39c TriggerPtmf +8085c3cc 0x8085c3cc +8085c3e0 Pages::GPVSLeaderboardUpdate::OnInit +8085c4f4 Pages::GPVSLeaderboardUpdate::GetRowCount +8085c504 Pages::GPVSLeaderboardUpdate::CanEnd +8085c5a0 Pages::GPVSLeaderboardUpdate::Func_0x6c +8085c858 Pages::GPVSLeaderboardUpdate::FillRows +8085c974 Pages::GPVSLeaderboardUpdate::GetNextPage +8085c9b0 Pages::GPVSLeaderboardTotal::OnInit +8085cae8 Pages::GPVSLeaderboardTotal::FillRows +8085cc40 Pages::GPVSLeaderboardTotal::GetNextPage +8085cc84 Pages::GPVSLeaderboardTotal::ComparePlayers +8085cd18 Pages::WWLeaderboardUpdate::OnInit +8085ce8c Pages::WWLeaderboardUpdate::FillRows +8085d55c Pages::WWLeaderboardUpdate::GetNextPage +8085d564 Pages::TTLeaderboard::OnInit +8085d784 Pages::TTLeaderboard::GetRowCount +8085d78c Pages::TTLeaderboard::FillRows +8085dc44 Pages::TTLeaderboard::GetNextPage +8085de34 0x8085de34 +8085e3ac 0x8085e3ac +8085e3b4 0x8085e3b4 +8085e3bc 0x8085e3bc +8085e42c 0x8085e42c +8085e438 Pages::TTLeaderboard::__dt +8085e4b8 Pages::TTLeaderboard::GetRuntimeTypeInfo +8085e4c4 Pages::WWLeaderboardUpdate::__dt +8085e534 Pages::WWLeaderboardUpdate::GetRuntimeTypeInfo +8085e540 Pages::GPVSLeaderboardTotal::__dt +8085e5b0 Pages::GPVSLeaderboardTotal::GetRuntimeTypeInfo +8085e5bc Pages::GPVSLeaderboardUpdate::__dt +8085e62c Pages::GPVSLeaderboardUpdate::GetRuntimeTypeInfo +8085e638 0x8085e638 +8085e644 LeaderboardsPagescpp_sinit_ +8085e674 Pages::TeamVSLeaderboardBase::OnInit +8085e7a0 Pages::TeamVSLeaderboardBase::AfterControlUpdate +8085e7fc Pages::TeamVSLeaderboardBase::CanEnd +8085e85c Pages::TeamVSLeaderboardBase::CalcTeamScoreDiff +8085e9e4 Pages::TeamVSLeaderboardTotal::FillRows +8085ea54 Pages::BattleLeaderboardTotal::FillRows +8085ea64 Pages::BattleLeaderboardUpdate::FillRows +8085edb4 Pages::TeamVSLeaderboardTotal::GetNextPage +8085ede4 Pages::BattleLeaderboardUpdate::GetNextPage +8085ee14 Pages::BattleLeaderboardTotal::GetNextPage +8085eeb0 Pages::BattleLeaderboardTotal::__dt +8085ef3c Pages::BattleLeaderboardTotal::GetRuntimeTypeInfo +8085ef48 Pages::BattleLeaderboardUpdate::__dt +8085efd4 Pages::BattleLeaderboardUpdate::GetRuntimeTypeInfo +8085efe0 Pages::TeamVSLeaderboardTotal::__dt +8085f06c Pages::TeamVSLeaderboardTotal::GetRuntimeTypeInfo +8085f078 Pages::TeamVSLeaderboardBase::GetRuntimeTypeInfo +8085f084 TeamLeaderboard_cpp_sinit_ +8085f0ac Pages::LicenseRecords::GetPageById +8085f1bc Pages::LicenseRecords::OnInit +8085f264 Pages::LicenseRecords::CreateExternalControl +8085f310 Pages::LicenseRecords::CreateControl +8085f470 Pages::LicenseRecords::OnActivate +8085f4dc Pages::LicenseRecords::AfterControlUpdate +8085f4e0 0x8085f4e0 +8085f5bc Pages::LicenseRecords::OnExternalButtonSelect +8085f5c0 0x8085f5c0 +8085f5c4 0x8085f5c4 +8085f5ec 0x8085f5ec +8085f934 0x8085f934 +8085fa70 0x8085fa70 +8085fc0c 0x8085fc0c +8085fd24 0x8085fd24 +8085fec0 Pages::LicenseRecords::GetPlayerBitfield +8085fec8 Pages::LicenseRecords::GetActivePlayerBitfield +8085fed0 Pages::LicenseRecords::SetButtonHandlers +8085ff2c Pages::LicenseRecords::GetManipulatorManager +8085ff34 Pages::LicenseRecords::__dt +8085ffb4 Pages::LicenseRecords::GetRuntimeTypeInfo +8085ffc0 LicenseRecordsPagecpp_sinit_ +8085ffd4 RacePauseMgr::CreateInstance +80860034 RacePauseMgr::__ct +8086004c RacePauseMgr::DestroyInstance +8086009c RacePauseMgr::__dt +808600dc RacePauseMgr::RequestPause +80860100 RacePauseMgr::RequestUnpause +80860124 0x80860124 +80860158 0x80860158 +80860160 0x80860160 +80860204 0x80860204 +80860254 0x80860254 +8086025c 0x8086025c +80860264 0x80860264 +8086027c 0x8086027c +808602b0 0x808602b0 +808602b8 0x808602b8 +808602c0 0x808602c0 +808602c8 0x808602c8 +808602d0 0x808602d0 +808602d8 0x808602d8 +808602e0 0x808602e0 +808602e8 0x808602e8 +808602f0 0x808602f0 +808602f8 0x808602f8 +80860300 0x80860300 +80860308 0x80860308 +80860310 0x80860310 +80860318 0x80860318 +80860320 0x80860320 +80860328 0x80860328 +80860330 0x80860330 +80860338 0x80860338 +80860340 0x80860340 +80860348 0x80860348 +80860350 0x80860350 +80860358 0x80860358 +8086038c 0x8086038c +80860394 0x80860394 +8086039c 0x8086039c +808603a4 0x808603a4 +808603ac 0x808603ac +808603b4 0x808603b4 +808603d0 0x808603b4_switch +808603d4 0x808603b4_caseD_1 +808603dc 0x808603b4_caseD_0 +808603e4 0x808603b4_caseD_3 +808603ec 0x808603b4_caseD_a +808603f4 0x808603b4_caseD_10 +808603fc 0x808603b4_caseD_12 +80860404 0x808603b4_caseD_11 +8086040c 0x808603b4_caseD_13 +8086041c 0x8086041c +80860448 0x80860448 +80860450 0x80860450 +80860470 0x80860450_switch +80860474 0x80860450_caseD_1e +8086047c 0x80860450_caseD_21 +80860484 IsOnlineSection +808604c0 IsGhostRaceSection +808604e0 IsTournamentSection +80860500 GetActiveRacedataPlayers +808605fc GetLocalPlayerCount +80860614 0x80860614 +8086068c 0x8086068c +808606a4 GetNextVSTrack +808606d0 0x808606d0 +8086070c 0x8086070c +8086082c 0x8086082c +808608e4 TimerToTextInfo +808609fc IsGameplaySection +80860a28 0x80860a28 +80860a8c IsBattle +80860acc GetCharacterIconPaneName +80860af0 GetItemIconPaneName +80860b54 GetCupIconPaneName +80860b88 0x80860b88 +80860bbc GetBattleCupIconPaneName +80860bf0 0x80860bf0 +80860e08 GetMapIconPaneName +80860f2c 0x80860f2c +80860f94 0x80860f2c_switch +80860f98 0x80860f2c_caseD_0 +80861014 0x80860f2c_caseD_4 +8086107c 0x80860f2c_caseD_8 +808610fc 0x80860f2c_caseD_c +80861164 0x80860f2c_caseD_10 +8086129c 0x80860f2c_caseD_11 +80861310 0x80860f2c_caseD_12 +80861368 0x80860f2c_caseD_13 +808613b0 0x80860f2c_caseD_14 +80861488 0x80860f2c_caseD_15 +808614e4 0x80860f2c_caseD_23 +80861524 0x80860f2c_caseD_16 +80861598 0x80860f2c_caseD_17 +80861680 0x80860f2c_caseD_18 +808616f4 0x80860f2c_caseD_19 +8086175c 0x80860f2c_caseD_24 +808617a4 0x80860f2c_caseD_1a +808618b8 0x80860f2c_caseD_1b +80861938 0x80860f2c_caseD_1c +80861a1c 0x80860f2c_caseD_1d +80861a78 0x80860f2c_caseD_1e +80861ab8 0x80860f2c_caseD_1f +80861b2c 0x80860f2c_caseD_20 +80861ba4 0x80860f2c_caseD_21 +80861c04 0x80860f2c_caseD_22 +80861c50 0x80860f2c_caseD_25 +80861d28 0x80860f2c_caseD_26 +80861d84 0x80860f2c_caseD_27 +80861dcc 0x80860f2c_caseD_28 +80861e40 0x80860f2c_caseD_29 +80861eb4 0x80860f2c_caseD_2a +80861f28 0x80860f2c_caseD_2b +80861f84 0x80860f2c_caseD_3c +80861fcc 0x80860f2c_caseD_2c +80862040 0x80860f2c_caseD_2d +80862128 0x80860f2c_caseD_2e +8086219c 0x80860f2c_caseD_2f +80862210 0x80860f2c_caseD_30 +80862278 0x80860f2c_caseD_31 +808622b8 0x80860f2c_caseD_32 +8086232c 0x80860f2c_caseD_33 +808623a0 0x80860f2c_caseD_34 +80862484 0x80860f2c_caseD_35 +808624f8 0x80860f2c_caseD_36 +8086256c 0x80860f2c_caseD_37 +808625c8 0x80860f2c_caseD_38 +80862608 0x80860f2c_caseD_39 +8086267c 0x80860f2c_caseD_3a +808626d8 0x80860f2c_caseD_3b +80862754 0x80862754 +808627c0 0x808627c0 +8086284c 0x8086284c +808628a4 0x808628a4 +80862988 0x80862988 +808629a4 0x80862988_switch +808629a8 0x80862988_caseD_34 +808629b0 0x80862988_caseD_0 +808629b8 0x80862988_caseD_1 +808629c0 0x80862988_caseD_2 +808629c8 0x80862988_caseD_3 +808629d0 0x80862988_caseD_4 +808629d8 0x80862988_caseD_5 +808629e0 0x80862988_caseD_6 +808629e8 0x80862988_caseD_7 +808629f0 0x80862988_caseD_b +808629f8 0x80862988_caseD_8 +80862a00 0x80862988_caseD_a +80862a08 0x80862988_caseD_9 +80862a10 0x80862988_caseD_c +80862a18 0x80862988_caseD_d +80862a20 0x80862988_caseD_22 +80862a28 0x80862988_caseD_23 +80862a30 0x80862988_caseD_24 +80862a38 0x80862988_caseD_25 +80862a40 0x80862988_caseD_26 +80862a48 0x80862988_caseD_27 +80862a50 0x80862988_caseD_28 +80862a58 0x80862988_caseD_29 +80862a60 0x80862988_caseD_2a +80862a68 0x80862988_caseD_2b +80862a70 0x80862988_caseD_2c +80862a78 0x80862988_caseD_2d +80862a80 0x80862988_caseD_2e +80862a88 0x80862988_caseD_2f +80862a90 0x80862988_caseD_30 +80862a98 0x80862988_caseD_31 +80862aa0 0x80862988_caseD_32 +80862aa8 0x80862988_caseD_33 +80862ab0 0x80862988_caseD_10 +80862ab8 0x80862ab8 +80862ad4 0x80862ab8_switch +80862ad8 0x80862ab8_caseD_0 +80862ae0 0x80862ab8_caseD_1 +80862ae8 0x80862ab8_caseD_2 +80862af0 0x80862ab8_caseD_3 +80862af8 0x80862ab8_caseD_4 +80862b00 0x80862ab8_caseD_5 +80862b08 0x80862ab8_caseD_6 +80862b10 0x80862ab8_caseD_7 +80862b18 0x80862ab8_caseD_8 +80862b20 0x80862ab8_caseD_9 +80862b28 0x80862ab8_caseD_a +80862b30 0x80862ab8_caseD_b +80862b38 0x80862ab8_caseD_c +80862b40 0x80862ab8_caseD_d +80862b48 0x80862ab8_caseD_e +80862b50 0x80862ab8_caseD_f +80862b58 0x80862ab8_caseD_10 +80862b60 0x80862ab8_caseD_11 +80862b68 0x80862ab8_caseD_12 +80862b70 0x80862ab8_caseD_14 +80862b78 0x80862ab8_caseD_15 +80862b80 0x80862ab8_caseD_16 +80862b88 0x80862ab8_caseD_17 +80862b90 0x80862ab8_caseD_18 +80862b98 0x80862ab8_caseD_19 +80862ba0 0x80862ab8_caseD_1a +80862ba8 0x80862ab8_caseD_1c +80862bb0 0x80862ab8_caseD_1d +80862bb8 0x80862ab8_caseD_1f +80862bc0 0x80862ab8_caseD_20 +80862bc8 0x80862ab8_caseD_21 +80862bd0 0x80862ab8_caseD_25 +80862bd8 0x80862ab8_caseD_26 +80862be0 0x80862ab8_caseD_28 +80862be8 0x80862ab8_caseD_29 +80862bf0 0x80862ab8_caseD_2a +80862bf8 0x80862ab8_caseD_2b +80862c00 0x80862ab8_caseD_2c +80862c08 0x80862ab8_caseD_2d +80862c10 0x80862ab8_caseD_2e +80862c18 0x80862ab8_caseD_2f +80862c20 0x80862ab8_caseD_30 +80862c28 0x80862ab8_caseD_32 +80862c30 0x80862ab8_caseD_33 +80862c38 0x80862ab8_caseD_34 +80862c40 0x80862ab8_caseD_35 +80862c48 0x80862ab8_caseD_36 +80862c50 0x80862ab8_caseD_37 +80862c58 0x80862ab8_caseD_39 +80862c60 0x80862ab8_caseD_3a +80862c68 0x80862ab8_caseD_3b +80862c78 0x80862c78 +80862c9c 0x80862c9c +80862cb8 0x80862c9c_switch +80862cbc 0x80862c9c_caseD_0 +80862cc4 0x80862c9c_caseD_1 +80862ccc 0x80862c9c_caseD_2 +80862cd4 0x80862c9c_caseD_3 +80862cdc 0x80862c9c_caseD_4 +80862ce4 0x80862c9c_caseD_5 +80862cec 0x80862c9c_caseD_6 +80862cf4 0x80862c9c_caseD_7 +80862cfc 0x80862c9c_caseD_b +80862d04 0x80862c9c_caseD_8 +80862d0c 0x80862c9c_caseD_a +80862d14 0x80862c9c_caseD_9 +80862d1c 0x80862c9c_caseD_c +80862d24 0x80862c9c_caseD_d +80862d34 0x80862d34 +80862d58 0x80862d58 +80862d78 0x80862d58_switch +80862d7c 0x80862d58_caseD_22 +80862d84 0x80862d58_caseD_23 +80862d8c 0x80862d58_caseD_24 +80862d94 0x80862d58_caseD_25 +80862d9c 0x80862d58_caseD_26 +80862da4 0x80862d58_caseD_27 +80862dac 0x80862d58_caseD_28 +80862db4 0x80862d58_caseD_29 +80862dbc 0x80862d58_caseD_2a +80862dc4 0x80862d58_caseD_2b +80862dcc 0x80862d58_caseD_2c +80862dd4 0x80862d58_caseD_2d +80862ddc 0x80862d58_caseD_2e +80862de4 0x80862d58_caseD_2f +80862dec 0x80862d58_caseD_30 +80862df4 0x80862d58_caseD_31 +80862dfc 0x80862d58_caseD_32 +80862e04 0x80862d58_caseD_33 +80862e0c 0x80862d58_caseD_1e +80862e14 0x80862d58_caseD_1f +80862e1c 0x80862d58_caseD_20 +80862e24 0x80862e24 +80863234 0x80863234 +8086329c 0x8086329c +808632c4 0x808632c4 +808632f0 0x808632c4_switch +808632f4 0x808632c4_caseD_5 +80863300 0x808632c4_caseD_a +8086330c 0x808632c4_caseD_7 +80863318 0x808632c4_caseD_f +80863324 0x808632c4_caseD_12 +80863330 0x808632c4_caseD_14 +8086333c 0x808632c4_caseD_1a +80863348 0x808632c4_caseD_1b +80863358 0x808632c4_caseD_10 +8086335c 0x8086335c +80863384 0x80863384 +808633ac 0x808633ac +808633cc 0x808633ac_switch +808633d0 0x808633ac_caseD_1 +808633dc 0x808633ac_caseD_2 +808633e8 0x808633ac_caseD_4 +808633f4 0x808633ac_caseD_5 +80863400 0x808633ac_caseD_7 +8086340c 0x808633ac_caseD_8 +80863418 0x808633ac_caseD_a +80863424 0x808633ac_caseD_c +80863430 0x808633ac_caseD_e +8086343c 0x808633ac_caseD_f +80863448 0x808633ac_caseD_10 +80863454 0x808633ac_caseD_12 +80863460 0x808633ac_caseD_14 +8086346c 0x808633ac_caseD_1b +80863478 0x808633ac_caseD_1d +80863484 0x808633ac_caseD_1e +80863490 0x808633ac_caseD_1f +80863498 0x808633ac_caseD_0 +8086349c 0x8086349c +808634bc 0x8086349c_switch +808634c0 0x8086349c_caseD_1 +808634cc 0x8086349c_caseD_2 +808634d8 0x8086349c_caseD_5 +808634e4 0x8086349c_caseD_7 +808634f0 0x8086349c_caseD_8 +808634fc 0x8086349c_caseD_a +80863508 0x8086349c_caseD_c +80863514 0x8086349c_caseD_e +80863520 0x8086349c_caseD_f +8086352c 0x8086349c_caseD_10 +80863538 0x8086349c_caseD_12 +80863544 0x8086349c_caseD_14 +80863550 0x8086349c_caseD_1b +8086355c 0x8086349c_caseD_1d +80863568 0x8086349c_caseD_1e +80863574 0x8086349c_caseD_1f +8086357c 0x8086349c_caseD_0 +80863580 0x80863580 +808635a0 0x80863580_switch +808635a4 0x80863580_caseD_a +808635b0 0x80863580_caseD_e +808635bc 0x80863580_caseD_d +808635c8 0x80863580_caseD_4 +808635d4 0x80863580_caseD_7 +808635e0 0x80863580_caseD_f +808635ec 0x80863580_caseD_12 +808635f8 0x80863580_caseD_14 +80863604 0x80863580_caseD_1b +80863610 0x80863580_caseD_1 +80863618 0x80863580_caseD_0 +80863638 0x80863638 +80863660 0x80863660 +80863680 0x80863660_switch +80863684 0x80863660_caseD_2 +80863690 0x80863660_caseD_a +8086369c 0x80863660_caseD_8 +808636a8 0x80863660_caseD_e +808636b4 0x80863660_caseD_d +808636c0 0x80863660_caseD_4 +808636cc 0x80863660_caseD_7 +808636d8 0x80863660_caseD_f +808636e4 0x80863660_caseD_13 +808636f0 0x80863660_caseD_12 +808636fc 0x80863660_caseD_1b +80863708 0x80863660_caseD_1 +80863710 0x80863660_caseD_0 +8086375c 0x8086375c +80863798 0x8086375c_switch +8086379c 0x8086375c_caseD_a +808637a8 0x8086375c_caseD_9 +808637b4 0x8086375c_caseD_c +808637c0 0x8086375c_caseD_e +808637cc 0x8086375c_caseD_4 +808637d8 0x8086375c_caseD_1b +808637e4 0x8086375c_caseD_0 +808637f0 0x8086375c_caseD_1 +80863814 0x8086375c_caseD_10 +80863828 0x80863828 +8086382c 0x8086382c +80863854 0x8086382c_switch +80863858 0x8086382c_caseD_b +80863864 0x8086382c_caseD_a +80863870 0x8086382c_caseD_8 +8086387c 0x8086382c_caseD_e +80863888 0x8086382c_caseD_d +80863894 0x8086382c_caseD_7 +808638a0 0x8086382c_caseD_f +808638ac 0x8086382c_caseD_12 +808638b8 0x8086382c_caseD_14 +808638c4 0x8086382c_caseD_1b +808638d0 0x8086382c_caseD_0 +808638dc 0x8086382c_caseD_1 +808638e4 0x8086382c_caseD_10 +808638e8 0x808638e8 +80863928 Audio::CharacterActor::__ct +808639a8 0x808639a8 +808639e8 Audio::RandomCharacterPicker::__dt +80863a44 Audio::RandomSoundPicker::__dt +80863a9c Audio::CharacterActor::Link +80863eb8 0x80863eb8 +80863ff8 Audio::CharacterActor::Unlink +80864000 Audio::CharacterActor::Update +808646c4 Audio::CharacterActor::HoldSound +808646f0 Audio::CharacterActor::PlayCharacterSoundConditional +80864748 Audio::CharacterActor::PlayCharacterSoundConditional_switch +8086474c Audio::CharacterActor::PlayCharacterSoundConditional_case1a +808647c8 Audio::CharacterActor::PlayCharacterSoundConditional_case1b +80864854 Audio::CharacterActor::PlayCharacterSoundConditional_case16 +80864874 Audio::CharacterActor::PlayCharacterSoundConditional_case17 +808648c4 Audio::CharacterActor::PlayCharacterSoundConditional_caseb +808648d0 Audio::CharacterActor::PlayCharacterSoundConditional_casec +808648ec Audio::CharacterActor::PlayCharacterSoundConditional_case10 +80864914 Audio::CharacterActor::PlayCharacterSound +80864cec 0x80864cec +80865050 0x80865050 +80865138 0x80865138 +80865390 0x80865390 +80865448 Audio::CharacterActor::PlayCharacterCollisionSound +80865480 0x80865480 +80865488 0x80865488 +80865490 0x80865490 +80865498 0x80865498 +808654a8 0x808654a8 +80865518 0x80865518 +80865534 0x80865534 +80865580 0x80865580 +808655b4 0x808655b4 +80865618 Audio::CharacterActor::StartSound +808656c4 0x808656c4 +808656cc 0x808656cc +808658c0 0x808658c0 +80865918 0x80865918 +80865944 0x80865944 +80865964 0x80865964 +80865984 Audio::CharacterActor::StopSound +80865a18 0x80865a18 +80865a98 0x80865a98 +80865bd4 0x80865bd4 +80865de8 0x80865de8 +80865f04 0x80865f04 +80865f38 0x80865f38 +80865f50 0x80865f50 +80865f64 0x80865f64 +80865fcc Jumbotron_StopPlaying +80865fe0 0x80865fe0 +80866110 0x80866110 +80866170 0x80866170 +80866198 0x80866198 +8086622c 0x8086622c +808662e4 0x808662e4 +8086630c 0x8086630c +80866388 Audio::CharacterActor::GetCharacterGroupId +80866404 Audio::CharacterActor::GetCharacterCannonGroupId +808664c4 0x808664c4 +80866628 0x80866628 +80866b1c 0x80866b1c +80866b94 0x80866b94 +80866c08 Audio::CharacterActor::__dt +80866d0c Audio::CharacterActor::__dt(thunk) +80866d14 Audio::CharacterActor::StopSound(thunk) +80866d1c Audio::CharacterActor::HoldSound(thunk) +80866d24 Audio::CharacterActor::StartSound(thunk) +80866d2c Audio::CharacterActor::Update(thunk) +80866d34 Audio::CharacterActor::Unlink(thunk) +80866d3c Audio::CharacterActor::Link(thunk) +80866d44 AudioRandomMgr::Disposer__dt +80866ddc AudioRandomMgr::CreateInstance +80866e64 AudioRandomMgr::DestroyInstance +80866e98 AudioRandomMgr::__dt +80866f4c AudioRandomMgr::RandInt +80866f80 AudioRandomMgr::RandFloat +80866fc0 0x80866fc0 +80867020 0x80867020 +8086708c Audio::RandomSoundPicker::__ct +808670d0 0x808670d0 +80867160 0x80867160 +80867194 0x80867194 +808672f4 0x808672f4 +808673a4 0x808673a4 +80867400 0x80867400 +808674cc 0x808674cc +80867540 0x80867540 +80867550 0x80867550 +80867584 0x80867584 +808675ec 0x808675ec +808676a4 0x808676a4 +808676d8 0x808676d8 +808676e0 Audio::RandomCharacterPicker::__ct +80867724 0x80867724 +80867788 0x80867788 +80867790 0x80867790 +80867850 0x80867850 +80867910 0x80867910 +808679d0 0x808679d0 +80867a90 0x80867a90 +80867b3c 0x80867b3c +80867cb8 0x80867cb8 +80867ddc 0x80867ddc +80867f0c 0x80867f0c +80868004 0x80868004 +80868044 0x80868044 +8086817c 0x8086817c +808683e0 0x808683e0 +80868554 0x80868554 +8086858c 0x8086858c +80868638 0x80868638 +80868774 cAIPad__ct +80868780 0x80868780 +80868794 0x80868794 +80868974 0x80868974 +808689dc 0x808689dc +80868a3c 0x80868a3c +80868abc 0x80868abc +80868c20 0x80868c20 +80868d3c 0x80868d3c +80868d54 0x80868d54 +80868e2c 0x80868e2c +80868f1c 0x80868f1c +80868f34 0x80868f34 +80869024 0x80869024 +80869038 0x80869038 +80869054 0x80869054 +8086906c 0x8086906c +80869084 0x80869084 +80869098 0x80869098 +808690a8 cTeam_GetPlayer +808690b8 0x808690b8 +808690e0 0x808690e0 +808690fc 0x808690fc +80869118 0x80869118 +808691fc 0x808691fc +808692c8 0x808692c8 +80869340 thunk_0x808691fc +80869344 0x80869344 +80869364 0x80869344_switch +80869368 0x80869344_caseD_0 +80869374 0x80869344_caseD_1 +80869380 0x80869344_caseD_2 +8086938c 0x80869344_caseD_3 +808693b8 0x808693b8 +80869488 0x80869488 +8086965c 0x8086965c +808696f8 0x808696f8 +808697a4 0x808697a4 +808697bc 0x808697bc +80869844 0x80869844 +80869884 0x80869884 +80869998 0x80869998 +808699d8 0x808699d8 +80869b2c 0x80869b2c +80869c48 0x80869c48 +80869d20 0x80869d20 +80869df4 0x80869df4 +80869fd0 0x80869fd0 +8086a0dc 0x8086a0dc +8086a254 0x8086a254 +8086a2d4 0x8086a2d4 +8086a318 0x8086a318 +8086a9ec 0x8086a9ec +8086a9f0 0x8086a9f0 +8086aa30 0x8086aa30 +8086aa34 0x8086aa34 +8086aa74 0x8086aa74 +8086ab68 0x8086ab68 +8086aba8 0x8086aba8 +8086ac20 obj_casino_roulette::__ct +8086ace8 obj_casino_roulette_generateBigassLongListOfFloatsForRotation +8086af30 0x8086af30 +8086b1f4 obj_casino_roulette_spinVisually +8086b274 obj_casino_roulette_update +8086b33c obj_casino_roulette_spinEverythingElse +8086b720 obj_casino_roulette_calculateSpin +8086b878 obj_casino_roulette_spinPlayer +8086baac obj_casino_roulette_initDushBoardAnimationSpeed? +8086bb28 0x8086bb28 +8086bb34 0x8086bb34 +8086bbb4 0x8086bbb4 +8086bbbc 0x8086bbbc +8086bbc4 0x8086bbc4 +8086bbd4 0x8086bbd4 +8086bbdc 0x8086bbdc +8086bc1c Objects::WLwallGC::__ct +8086bde4 Objects::WLwallGC::__dt +8086be34 Objects::WLwallGC::OnStart +8086bf08 0x8086bf08 +8086bf30 Objects::WLwallGC::GetTransformationMatrix +8086c098 Objects::WLwallGC::CalcNextPosition +8086c108 Objects::WLwallGC::Update +8086c280 Objects::WLwallGC::CalcOtherEntityDistance +8086c2a8 Objects::WLwallGC::UpdateOtherEntityPos +8086c328 Objects::WLwallGC::IsCollidingAddEntryNoTriangleCheckImpl +8086c5a8 Objects::WLwallGC::IsCollidingImpl +8086c640 Objects::WLwallGC::GetPropertiesBitfield +8086c648 Objects::WLwallGC::GetPeriod +8086c684 Objects::WLwallGC::vf_0x11c +8086c68c 0x8086c68c +8086c988 0x8086c988 +8086c98c 0x8086c98c +8086c9cc 0x8086c9cc +8086ca40 0x8086ca40 +8086cacc 0x8086cacc +8086cb9c 0x8086cb9c +8086d890 0x8086d890 +8086de94 0x8086de94 +8086dea8 0x8086dea8 +8086dec8 0x8086dec8 +8086dee8 0x8086dee8 +8086def0 0x8086def0 +8086e034 0x8086e034 +8086e110 0x8086e110 +8086e218 0x8086e218 +8086e21c 0x8086e21c +8086e224 0x8086e224 +8086e824 0x8086e824 +8086e910 0x8086e910 +8086e9dc 0x8086e9dc +8086eadc 0x8086eadc +8086ec3c 0x8086ec3c +8086ec5c 0x8086ec5c +8086ee50 0x8086ee50 +8086ef34 0x8086ef34 +8086f01c 0x8086f01c +8086f19c 0x8086f19c +8086f23c 0x8086f23c +8086f410 0x8086f410 +8086f70c 0x8086f70c +8086f8ec 0x8086f8ec +80871010 0x80871010 +80871060 0x80871060 +8087147c 0x8087147c +80871684 0x80871684 +80871698 0x80871698 +808718b0 0x808718b0 +80871900 0x80871900 +80871af4 0x80871af4 +80871b44 0x80871b44 +80871bb0 0x80871bb0 +80872208 0x80872208 +80872858 0x80872858 +80872acc 0x80872acc +80872bd4 0x80872bd4 +80872be0 0x80872be0 +80872be8 0x80872be8 +80872bf0 0x80872bf0 +80872bf4 0x80872bf4 +80872bf8 0x80872bf8 +80872bfc 0x80872bfc +80872c00 0x80872c00 +80872c08 0x80872c08 +80872c0c 0x80872c0c +80872c14 0x80872c14 +80872ca4 0x80872ca4 +80872cb4 0x80872cb4 +80872cc8 0x80872cc8 +80873824 0x80873824 +80873850 0x80873850 +8087385c 0x8087385c +808739b0 0x808739b0 +808739b8 0x808739b8 +808739c0 0x808739c0 +808739c8 0x808739c8 +808739d0 0x808739d0 +808739f4 0x808739f4 +80873a04 0x80873a04 +80873a08 0x80873a08 +80873a20 0x80873a20 +80873a40 0x80873a40 +80873b48 0x80873b48 +80873cf4 0x80873cf4 +80873dc0 0x80873dc0 +80873e14 0x80873e14 +80873e2c 0x80873e2c +80873f6c 0x80873f6c +808741d8 0x808741d8 +80874244 0x80874244 +80874300 0x80874300 +808743a0 0x808743a0 +80874570 0x80874570 +8087477c 0x8087477c +80874950 0x80874950 +80874a44 0x80874a44 +80874b34 0x80874b34 +80874c18 0x80874c18 +80874d7c 0x80874d7c +80874d90 0x80874d90 +80874e08 0x80874e08 +80875454 0x80875454 +808755c4 0x808755c4 +8087567c 0x8087567c +80875688 0x80875688 +80875740 GoalOverlay_SceneCreated +8087574c 0x8087574c +80875aac 0x80875aac +80875d70 0x80875d70 +808766f0 0x808766f0 +80876710 0x80876710 +80876964 0x80876964 +808773f4 0x808773f4 +80877408 0x80877408 +80877574 0x80877574 +80877974 0x80877974 +80877b48 0x80877b48 +80877be0 0x80877be0 +808781d0 0x808781d0 +80878324 0x80878324 +80878494 0x80878494 +8087889c 0x8087889c +80879654 0x80879654 +80879698 0x80879698 +80879720 0x80879720 +80879c14 0x80879c14 +80879da0 0x80879da0 +8087a138 0x8087a138 +8087a3ac 0x8087a3ac +8087a4dc 0x8087a4dc +8087a70c 0x8087a70c +8087a718 0x8087a718 +8087aff4 0x8087aff4 +8087aff8 0x8087aff8 +8087b198 0x8087b198 +8087b20c 0x8087b20c +8087b224 0x8087b224 +8087b228 0x8087b228 +8087b2ec 0x8087b2ec +8087b2f4 0x8087b2f4 +8087b3dc 0x8087b3dc +8087b46c 0x8087b46c +8087b4d0 0x8087b4d0 +8087ba20 0x8087ba20 +8087ba24 0x8087ba24 +8087ba64 0x8087ba64 +8087bacc 0x8087bacc +8087bfe4 0x8087bfe4 +8087cb04 0x8087cb04 +8087cb44 0x8087cb44 +8087cb4c 0x8087cb4c +8087cb8c 0x8087cb8c +8087cb94 0x8087cb94 +8087cbd4 0x8087cbd4 +8087cbdc 0x8087cbdc +8087cc1c 0x8087cc1c +8087cc5c 0x8087cc5c +8087cc64 0x8087cc64 +8087cca4 0x8087cca4 +8087ccac 0x8087ccac +8087ccec 0x8087ccec +8087ccf4 0x8087ccf4 +8087dbe4 0x8087dbe4 +8087dc48 0x8087dc48 +8087dcac 0x8087dcac +8087dcec 0x8087dcec +8087dd58 0x8087dd58 +8087dd60 0x8087dd60 +8087ddc4 0x8087ddc4 +8087de30 0x8087de30 +8087de9c 0x8087de9c +8087df04 0x8087df04 +8087df44 0x8087df44 +8087dfa8 0x8087dfa8 +8087dfb0 0x8087dfb0 +8087dff0 0x8087dff0 +8087e054 0x8087e054 +8087e05c 0x8087e05c +8087e09c 0x8087e09c +8087e100 0x8087e100 +8087e108 0x8087e108 +8087e148 0x8087e148 +8087e178 0x8087e178 +8087e1b8 0x8087e1b8 +8087e1e8 0x8087e1e8 +8087e228 0x8087e228 +8087e258 0x8087e258 +8087e298 0x8087e298 +8087e304 0x8087e304 +8087e30c 0x8087e30c +8087e34c 0x8087e34c +8087e3b0 0x8087e3b0 +8087e3b8 0x8087e3b8 +8087e3f8 0x8087e3f8 +8087e438 0x8087e438 +8087e468 0x8087e468 +80881314 0x80881314 +8088137c 0x8088137c +808813e4 0x808813e4 +8088145c 0x8088145c +808814c4 0x808814c4 +8088152c 0x8088152c +808815a4 0x808815a4 +8088160c 0x8088160c +80881674 0x80881674 +808816dc 0x808816dc +80881744 0x80881744 +80881798 0x80881798 +808817ec 0x808817ec +80881840 0x80881840 +808818b8 0x808818b8 +80881920 0x80881920 +80881974 0x80881974 +80881990 0x80881990 +80881994 0x80881994 +80882b58 0x80882b58 +80882be8 0x80882be8 +80882cd4 0x80882cd4 +80882d60 0x80882d60 +808830b4 0x808830b4 +80883250 0x80883250 +80883328 0x80883328 +80883428 0x80883428 +8088342c 0x8088342c +80883434 0x80883434 +8088343c 0x8088343c +80883444 0x80883444 +8088344c 0x8088344c +808837b8 0x808837b8 +80883844 0x80883844 +80883b98 0x80883b98 +808861ac 0x808861ac +808862d4 0x808862d4 +80886438 0x80886438 +8088644c 0x8088644c +808864d4 0x808864d4 +808864dc 0x808864dc +808864e4 0x808864e4 +80886574 obj_cruiserR::__ct +808867ac 0x808867ac +80886838 0x80886838 +80886a90 0x80886a90 +80886ab0 0x80886ab0 +80886b58 0x80886b58 +808873d0 0x808873d0 +808879b4 0x808879b4 +808879c8 0x808879c8 +80887a10 0x80887a10 +80887a18 0x80887a18 +80887a20 0x80887a20 +80887a58 0x80887a58 +80887ab4 0x80887ab4 +80887b04 0x80887b04 +80887be4 0x80887be4 +80887d14 0x80887d14 +80887d5c 0x80887d5c +80887d64 0x80887d64 +80887d84 0x80887d84 +80887d8c 0x80887d8c +80887e60 0x80887e60 +80887ea0 0x80887ea0 +80887f6c 0x80887f6c +80887f8c 0x80887f8c +80888464 0x80888464 +80888594 0x80888594 +808885a0 0x808885a0 +808885ac 0x808885ac +808885b0 0x808885b0 +80888604 0x80888604 +80888608 0x80888608 +80888610 0x80888610 +80888614 0x80888614 +80888618 0x80888618 +80888620 0x80888620 +808887c4 0x808887c4 +80888804 0x80888804 +80888acc 0x80888acc +80888b70 0x80888b70 +8088916c 0x8088916c +808892b0 0x808892b0 +808892f4 0x808892f4 +808894b4 0x808894b4 +808894c0 0x808894c0 +808894c8 GetOneTimerLeadGroundContactAnims +808894d4 0x808894d4 +808894d8 0x808894d8 +80889594 0x80889594 +8088959c 0x8088959c +808895a0 0x808895a0 +808895a4 0x808895a4 +808895ac 0x808895ac +80889744 0x80889744 +808897a0 0x808897a0 +808897f0 0x808897f0 +808899a8 0x808899a8 +8088a294 0x8088a294 +8088a2a8 0x8088a2a8 +8088a2f0 0x8088a2f0 +8088a2f8 0x8088a2f8 +8088a318 0x8088a318 +8088a320 0x8088a320 +8088a3b0 obj_Twanwan::__ct +8088a46c obj_Twanwan::__dt +8088a4bc obj_Twanwan_load +8088a8a4 0x8088a8a4 +8088a978 0x8088a978 +8088ab04 0x8088ab04 +8088b87c obj_Twanwan_update +8088bb7c 0x8088bb7c +8088bb94 obj_Twanwan_draw_init +8088bbcc 0x8088bbcc +8088bd08 0x8088bd08 +8088bd4c 0x8088bd4c +8088bdc0 0x8088bdc0 +8088bdc8 0x8088bdc8 +8088be00 0x8088be00 +8088be48 0x8088be48 +8088beb4 0x8088beb4 +8088c4b0 0x8088c4b0 +8088c520 0x8088c520 +8088c52c 0x8088c52c +8088c584 0x8088c584 +8088c5a0 0x8088c5a0 +8088cb74 0x8088cb74 +8088ccc0 0x8088ccc0 +8088ccc4 0x8088ccc4 +8088cd04 0x8088cd04 +8088ce58 0x8088ce58 +8088ce5c 0x8088ce5c +8088ce9c 0x8088ce9c +8088cff0 0x8088cff0 +8088cff4 0x8088cff4 +8088d034 0x8088d034 +8088d100 0x8088d100 +8088d1cc 0x8088d1cc +8088d298 0x8088d298 +8088d324 0x8088d324 +8088d3b0 0x8088d3b0 +8088d438 0x8088d438 +8088d914 0x8088d914 +8088ddfc 0x8088ddfc +8088e2d8 0x8088e2d8 +8088e3e8 0x8088e3e8 +8088e4fc 0x8088e4fc +8088e60c 0x8088e60c +8088e66c 0x8088e66c +8088e6cc 0x8088e6cc +8088e72c 0x8088e72c +8088e780 0x8088e780 +8088e7d4 0x8088e7d4 +8088e828 0x8088e828 +8088ea18 0x8088ea18 +8088ec08 0x8088ec08 +8088edf8 0x8088edf8 +8088ee38 0x8088ee38 +8088ee78 0x8088ee78 +8088eeb8 0x8088eeb8 +8088f038 0x8088f038 +8088f1bc 0x8088f1bc +8088f334 RelEndFuncLink::AddEntry +8088f350 RelEndFuncLink::ExecFuncs +8088f3a4 0x8088f3a4 diff --git a/projects/mkwii/recomp.yml b/projects/mkwii/recomp.yml new file mode 100644 index 0000000..bd8f727 --- /dev/null +++ b/projects/mkwii/recomp.yml @@ -0,0 +1,68 @@ +schema_version: 1 +workspace_root: ../.. + +project: + id: mkwii-rmcp01 + display_name: Mario Kart Wii PAL + game_id: RMCP01 + region: P + base_manifest_format: mkwii-recomp-base-manifest + base_manifest_stem: mkwii_base + +memory: + base: 0x80000000 + size: 0x01A00000 + sda_base: 0x8038CC00 + sda2_base: 0x8038EFA0 + +inputs: + dol: + path: Assets/main.dol + sha256: 80d18895b39c63bd80f457398bfcbb91b7d16ac116a41a88967e954080155b05 + + rel: + path: Assets/StaticR.rel + load_address: 0x805102E0 + sha256: 16d9d146112541fefea701ecb5bc1a496f9d50e4a752fbb5b6778e7c6399f67d + +translation: + entry_points: + - 0x800060A4 + function_map: + path: projects/mkwii/MAP.txt + + #just a precaution, but this should never be needed for MKWii + allow_unsupported_instructions: false + +runtime: + native_registration_root: runtime/src + native_abi_directories: + - runtime/src/hle/gx + +output: + root: generated + functions: functions + runtime_config: RuntimeConfig.h + data_initializer: data_sections_init.cpp + base_manifest: ../build/base/mkwii_base_manifest.json + +profiles: + retro-rewind: + enabled: true + requires_game_id: RMCP01 + requires_dol_sha256: 80d18895b39c63bd80f457398bfcbb91b7d16ac116a41a88967e954080155b05 + code_pul: PulsarPacks/completed/RetroRewind/RetroRewind6/Binaries/Code.pul + mod_root: PulsarPacks/completed/RetroRewind/RetroRewind6 + region: P + module_guest_base: 0x81800000 + module_link_base: 0x803992E0 + output: build/mods/retro_rewind_full_cpp + enable_retro_wfc: true + retro_wfc_payload: http://nas.play.rwfc.net/payload?g=RMCPD00 + retro_wfc_legacy_bootstrap_hook: 0x800ED6E8 + riivolution: + xml: xml/RetroRewind6.xml + options: + - section: Retro Rewind + option: Pack + choice: 1 diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt new file mode 100644 index 0000000..c2b3a1b --- /dev/null +++ b/runtime/CMakeLists.txt @@ -0,0 +1,229 @@ +cmake_minimum_required(VERSION 3.16) +project(mkw_recompiled) + +if(NOT WIN32 OR NOT MINGW OR NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR + NOT CMAKE_SIZEOF_VOID_P EQUAL 8 OR + NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$") + message(FATAL_ERROR "WiiCompiled requires 64-bit LLVM-MinGW Clang on Windows") +endif() +if(NOT CMAKE_BUILD_TYPE STREQUAL "Release") + message(FATAL_ERROR "WiiCompiled only supports Release builds") +endif() + +# Preprocessor definitions that belong to this project's own code (the runtime, +# the translated shards and the product glue) and to nothing else. They are +# applied directory-scoped, immediately after the aurora add_subdirectory() call, +# so every target created afterwards (mkw_runtime_common, the translated shard +# libraries and the products in cmake/PublicProducts.cmake) inherits them while +# aurora-main and its third-party tree stay unaffected. +set(MKW_PROJECT_COMPILE_DEFINITIONS NOMINMAX) + +set(CMAKE_CXX_STANDARD 17) + +# Declared before the vendored libraries so the third-party block below can +# branch on it and so cmake/NativePrebuilt.cmake is included after the names it +# must provide are known. +set(MKW_NATIVE_PREBUILT_DIR "" CACHE PATH + "Precompiled aurora/third-party package to link instead of building aurora-main") +# Maintainer-only counterpart: describe the configured aurora build so the +# package can be harvested from it. +set(MKW_NATIVE_PREBUILT_EXPORT_DIR "" CACHE PATH + "Maintainer-only: write a description of the configured aurora build here") + +# The two Crypto++ configuration macros are load-bearing for ABI: they shape the +# structs and inline code the headers generate, so a first-party TU including +# must see exactly what the archive was compiled with. Both the +# from-source target below and the prebuilt import in cmake/NativePrebuilt.cmake +# consume this one definition. +set(MKW_CRYPTOPP_INTERFACE_DEFINITIONS CRYPTOPP_DISABLE_ASM CRYPTOPP_NO_GLOBAL_BYTE) + +# Riivolution XML is parsed with pugixml, matching the parser used by Dolphin. +# The four upstream files from v1.15 (ee86beb30e4973f5feffe3ce63bfa4fbadf72f38) +# are vendored so builds remain usable with FETCHCONTENT_FULLY_DISCONNECTED. +add_library(mkw_pugixml STATIC third_party/pugixml/pugixml.cpp) +add_library(mkw::pugixml ALIAS mkw_pugixml) +target_include_directories(mkw_pugixml PUBLIC third_party/pugixml) +target_compile_features(mkw_pugixml PUBLIC cxx_std_17) +set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF) + +# Runtime configuration is real TOML, parsed by toml11 rather than a project- +# specific line parser. Keep it header-only and vendored so disconnected release +# builds have exactly the same parser as developer builds. +add_library(mkw_toml11 INTERFACE) +add_library(mkw::toml11 ALIAS mkw_toml11) +target_include_directories(mkw_toml11 SYSTEM INTERFACE third_party/toml11) +target_compile_features(mkw_toml11 INTERFACE cxx_std_17) + +# Wii ES signatures use the standard binary-field sect233r1 curve. Crypto++ +# supplies that curve, SHA-1, ECDSA and an OS-seeded CSPRNG. The complete 8.9.0 +# source distribution is vendored. Assembly and algorithm-specific SIMD units +# are disabled to keep the library portable and independent of the runtime's +# x86-64-v3 policy; the static linker retains only the algorithms we reference. +# +# Crypto++ is by far the most expensive vendored library to compile (~150 +# translation units), and under the shipped fixed toolchain it is bit-for-bit +# identical for every user, so the prebuilt package harvests its archive and +# this block is skipped entirely in that mode (see cmake/NativePrebuilt.cmake). +if(NOT MKW_NATIVE_PREBUILT_DIR) + file(GLOB MKW_CRYPTOPP_SOURCES CONFIGURE_DEPENDS + "${CMAKE_CURRENT_LIST_DIR}/third_party/cryptopp/*.cpp") + list(FILTER MKW_CRYPTOPP_SOURCES EXCLUDE REGEX + "/(adhoc|bench[123]|datatest|dlltest|fipsalgt|fipstest|pch|regtest[1234]|simple|test|validat[0-9]+)\\.cpp$") + list(FILTER MKW_CRYPTOPP_SOURCES EXCLUDE REGEX + "(_avx|_ppc|_simd|_sse)\\.cpp$") + add_library(mkw_cryptopp STATIC ${MKW_CRYPTOPP_SOURCES}) + add_library(mkw::cryptopp ALIAS mkw_cryptopp) + target_include_directories(mkw_cryptopp SYSTEM PUBLIC third_party) + target_compile_features(mkw_cryptopp PUBLIC cxx_std_17) + target_compile_definitions(mkw_cryptopp PUBLIC ${MKW_CRYPTOPP_INTERFACE_DEFINITIONS}) + target_compile_options(mkw_cryptopp PRIVATE -w) + set_target_properties(mkw_cryptopp PROPERTIES UNITY_BUILD OFF) +endif() + +set(MKW_TRANSLATED_COMPILE_JOBS 0 CACHE STRING + "Cap on concurrently compiling translated shard TUs via a Ninja job pool (0 = uncapped). \ +Scheduling only - never affects output bytes, so it is deliberately outside the canonical flag fingerprint.") +set(MKW_CPPWINRT_INCLUDE_DIR "" CACHE PATH "Optional self-contained C++/WinRT include directory") + +# Precompiled aurora + third-party package (see launcher/Prepare-NativePrebuilt.ps1). +# Empty - the default - keeps the from-source behaviour every developer build +# uses. When set, aurora-main, its extern/ tree and the vendored Crypto++ are +# not compiled at all; imported targets with the same names are wired up from +# the package instead, so this project's own code compiles and links exactly +# as before. +set(MKW_AURORA_DIR "${CMAKE_CURRENT_LIST_DIR}/../aurora-main") + +# The translator's explicit float casts are PPC single-precision rounding points. +# Fast-math may erase them and change guest-visible integer conversions. +set(MKW_TRANSLATED_PPC_FP_OPTIONS -fno-fast-math -ffp-contract=off) + +# ---------------------------------------------------------------------- +# Third-party: aurora-main (provides SDL3 + GPU backends) +# ---------------------------------------------------------------------- +if(MKW_NATIVE_PREBUILT_DIR) + # Precompiled aurora + third-party archives. The source trees are still + # present (the runtime includes aurora headers directly), they are simply + # not compiled here. + include("${CMAKE_CURRENT_LIST_DIR}/cmake/NativePrebuilt.cmake") +else() + if(NOT EXISTS ${MKW_AURORA_DIR}/CMakeLists.txt) + message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing") + endif() + set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE) + set(DAWN_ENABLE_D3D12 ON CACHE BOOL "" FORCE) + set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE) + set(TINT_BUILD_HLSL_WRITER ON CACHE BOOL "" FORCE) + set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "" FORCE) + set(DAWN_BUILD_TESTS OFF CACHE BOOL "" FORCE) + set(DAWN_USE_WINDOWS_UI OFF CACHE BOOL "" FORCE) + + # Provide a tiny stub for DXProgrammableCapture when the SDK/PIX headers are + # missing (common on MinGW). Dawn only includes the header; no symbols are + # referenced when PIX isn't present. + set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs") + if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h") + file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR}) + file(WRITE "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h" +"#pragma once\n// Stubbed PIX capture header for Dawn; no functionality when PIX is absent.\n") + endif() + # Deliberately NOT injected project-wide. Only a from-source Dawn build ever + # includes DXProgrammableCapture.h, and this tree consumes Dawn as a prebuilt + # package (AURORA_DAWN_PROVIDER=package), so the old + # include_directories()/add_compile_options(-I...) pair only added a dead + # -I to every one of the ~500 third-party and runtime compile lines. The + # aurora_* targets that do want it get it from the + # target_include_directories() call further down; a from-source Dawn build + # should add it to the Dawn targets the same way. + + # Keep SDL3 lean when aurora is enabled. + set(SDL_SHARED OFF CACHE BOOL "" FORCE) + set(SDL_STATIC ON CACHE BOOL "" FORCE) + set(SDL_TEST OFF CACHE BOOL "" FORCE) + + # MinGW/WSL can choke on SDL precompiled headers; disable PCH for aurora/SDL. + set(CMAKE_DISABLE_PRECOMPILE_HEADERS ON) + # Isolate aurora build products to a subdirectory under the main build dir. + add_subdirectory(${MKW_AURORA_DIR} ${CMAKE_BINARY_DIR}/aurora-build EXCLUDE_FROM_ALL) + set(CMAKE_DISABLE_PRECOMPILE_HEADERS OFF) + + # Keep upstream Aurora split: several sources intentionally use file-local + # names that collide under this repo's aggressive unity build settings. + set(AURORA_TARGETS aurora_core aurora_gx aurora_pad aurora_si aurora_mtx aurora_vi) + foreach(t ${AURORA_TARGETS}) + if(TARGET ${t}) + set_target_properties(${t} PROPERTIES UNITY_BUILD OFF) + target_compile_options(${t} PRIVATE -O3 -ffast-math -w -pipe) + target_include_directories(${t} PRIVATE ${MKW_DX_STUB_DIR}) + # Aurora's own sources include Windows headers and call std::min/max; + # they relied on the old project-wide NOMINMAX that no longer leaks + # into this subtree, so the define is applied per target here. + target_compile_definitions(${t} PRIVATE NOMINMAX) + endif() + endforeach() + + # Frame interpolation filters guest NaN/Inf matrices out of a std::sort + # comparator, which makes its std::isfinite guards load-bearing for memory + # safety, not just match quality. -ffinite-math-only (part of -ffast-math + # above) folds those guards to constants and poisons NaN float arguments + # via nofpclass, so this one file opts back out of the finite-math + # assumption; the file #errors if built without this. Source-scoped + # COMPILE_OPTIONS append after the target options, so this overrides the + # target's -ffast-math for exactly this TU. + if(TARGET aurora_gx) + set_source_files_properties("${MKW_AURORA_DIR}/lib/gx/frame_interpolation.cpp" + TARGET_DIRECTORY aurora_gx + PROPERTIES COMPILE_OPTIONS "-fno-finite-math-only") + endif() + + if(MKW_NATIVE_PREBUILT_EXPORT_DIR) + include("${CMAKE_CURRENT_LIST_DIR}/cmake/NativePrebuiltExport.cmake") + endif() +endif() + +# Everything from here down is this project's own code, so this is where the +# project definitions enter the directory scope. A directory-scoped definition +# seeds the COMPILE_DEFINITIONS of targets as they are created, which means: +# * aurora-main and its third-party tree were added above -> unaffected; +# * mkw_runtime_common, the translated shard libraries and the products in +# cmake/PublicProducts.cmake are all created below (that file is include()d, +# not add_subdirectory()d, so it shares this directory scope) -> they pick +# the definitions up exactly as before. +if(MKW_PROJECT_COMPILE_DEFINITIONS) + add_compile_definitions(${MKW_PROJECT_COMPILE_DEFINITIONS}) +endif() + +# Runtime sources. CONFIGURE_DEPENDS costs one glob re-check per build (measured +# at ~55 ms across the 100+ globs this build already re-verifies for SDL), and it +# buys correctness: without it, a newly added src/*.cpp that is not referenced by +# a registration file is silently never compiled and never errors. The stale-glob +# failure mode is worth far more than the milliseconds. +file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp") +set(MKW_BASE_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/base_product.cpp") +set(MKW_RETRO_REWIND_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/retro_rewind_product.cpp") +# The host ISA guard is the one translation unit that must not receive the +# x86-64-v3 flags, so it gets its own object library instead of riding along in +# mkw_runtime_common. See cmake/PublicProducts.cmake and the file's own header. +set(MKW_CPU_BASELINE_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/host_cpu_baseline.cpp") +list(REMOVE_ITEM SOURCES ${MKW_BASE_PRODUCT_SOURCE} ${MKW_RETRO_REWIND_PRODUCT_SOURCE} + ${MKW_CPU_BASELINE_SOURCE}) + +# The translator emits the complete, content-addressed source graph. Consuming +# this one manifest keeps configure independent of the 28k generated function +# files and of optional Retro Rewind artifacts such as code.map. +set(MKW_TRANSLATED_SHARD_MANIFEST + "${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake" + CACHE FILEPATH "Translator-owned aggregate shard manifest") +if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}") + message(FATAL_ERROR + "Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. " + "Run Translator.Cli emit-build-shards first; see translator/README.md.") +endif() +include("${MKW_TRANSLATED_SHARD_MANIFEST}") +set(MKW_HAVE_RETRO_REWIND ${MKW_HAVE_RETRO_REWIND_SHARDS}) +message(STATUS + "Translator graph: ${MKW_SHARED_BASE_FUNCTION_COUNT}/${MKW_BASE_FUNCTION_COUNT} base functions shared; " + "${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; " + "${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions") + +set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}") +include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake") diff --git a/runtime/assets/dsp/dsp_coef.bin b/runtime/assets/dsp/dsp_coef.bin new file mode 100644 index 0000000..f919013 Binary files /dev/null and b/runtime/assets/dsp/dsp_coef.bin differ diff --git a/runtime/assets/pipeline/initial_pipeline_cache.db b/runtime/assets/pipeline/initial_pipeline_cache.db new file mode 100644 index 0000000..f3856fe Binary files /dev/null and b/runtime/assets/pipeline/initial_pipeline_cache.db differ diff --git a/runtime/assets/wii/README.md b/runtime/assets/wii/README.md new file mode 100644 index 0000000..40672c0 --- /dev/null +++ b/runtime/assets/wii/README.md @@ -0,0 +1,9 @@ +# Wii first-run bootstrap payload + +This directory contains the small `shared2/wc24` first-run tree needed when Mario +Kart Wii is launched directly without an imported Wii NAND. The runtime copies +these files only into a newly managed, per-user NAND and never overwrites an +existing profile. + +The layout and required files follow Dolphin's direct-disc Wii bootstrap: +`Source/Core/Core/WiiRoot.cpp` and `Data/Sys/Wii/shared2/wc24`. diff --git a/runtime/assets/wii/shared2/wc24/mbox/Readme.txt b/runtime/assets/wii/shared2/wc24/mbox/Readme.txt new file mode 100644 index 0000000..6813fc7 --- /dev/null +++ b/runtime/assets/wii/shared2/wc24/mbox/Readme.txt @@ -0,0 +1,4 @@ +The Mailbox here has been shrunk, the real file size: + +WC24RECV.MBX 0x00700000 +WC24SEND.MBX 0x00200000 diff --git a/runtime/assets/wii/shared2/wc24/mbox/wc24recv.ctl b/runtime/assets/wii/shared2/wc24/mbox/wc24recv.ctl new file mode 100644 index 0000000..64c26b5 Binary files /dev/null and b/runtime/assets/wii/shared2/wc24/mbox/wc24recv.ctl differ diff --git a/runtime/assets/wii/shared2/wc24/mbox/wc24recv.mbx b/runtime/assets/wii/shared2/wc24/mbox/wc24recv.mbx new file mode 100644 index 0000000..81cd383 Binary files /dev/null and b/runtime/assets/wii/shared2/wc24/mbox/wc24recv.mbx differ diff --git a/runtime/assets/wii/shared2/wc24/mbox/wc24send.ctl b/runtime/assets/wii/shared2/wc24/mbox/wc24send.ctl new file mode 100644 index 0000000..165daf7 Binary files /dev/null and b/runtime/assets/wii/shared2/wc24/mbox/wc24send.ctl differ diff --git a/runtime/assets/wii/shared2/wc24/mbox/wc24send.mbx b/runtime/assets/wii/shared2/wc24/mbox/wc24send.mbx new file mode 100644 index 0000000..d447447 Binary files /dev/null and b/runtime/assets/wii/shared2/wc24/mbox/wc24send.mbx differ diff --git a/runtime/assets/wii/shared2/wc24/misc.bin b/runtime/assets/wii/shared2/wc24/misc.bin new file mode 100644 index 0000000..b218303 Binary files /dev/null and b/runtime/assets/wii/shared2/wc24/misc.bin differ diff --git a/runtime/assets/wii/shared2/wc24/nwc24dl.bin b/runtime/assets/wii/shared2/wc24/nwc24dl.bin new file mode 100644 index 0000000..b7f8fa3 Binary files /dev/null and b/runtime/assets/wii/shared2/wc24/nwc24dl.bin differ diff --git a/runtime/assets/wii/shared2/wc24/nwc24fl.bin b/runtime/assets/wii/shared2/wc24/nwc24fl.bin new file mode 100644 index 0000000..faa12b0 Binary files /dev/null and b/runtime/assets/wii/shared2/wc24/nwc24fl.bin differ diff --git a/runtime/assets/wii/shared2/wc24/nwc24fls.bin b/runtime/assets/wii/shared2/wc24/nwc24fls.bin new file mode 100644 index 0000000..1ca2191 Binary files /dev/null and b/runtime/assets/wii/shared2/wc24/nwc24fls.bin differ diff --git a/runtime/assets/wii/shared2/wc24/nwc24msg.cbk b/runtime/assets/wii/shared2/wc24/nwc24msg.cbk new file mode 100644 index 0000000..c87fb77 Binary files /dev/null and b/runtime/assets/wii/shared2/wc24/nwc24msg.cbk differ diff --git a/runtime/assets/wii/shared2/wc24/nwc24msg.cfg b/runtime/assets/wii/shared2/wc24/nwc24msg.cfg new file mode 100644 index 0000000..c87fb77 Binary files /dev/null and b/runtime/assets/wii/shared2/wc24/nwc24msg.cfg differ diff --git a/runtime/cmake/NativePrebuilt.cmake b/runtime/cmake/NativePrebuilt.cmake new file mode 100644 index 0000000..f889ecb --- /dev/null +++ b/runtime/cmake/NativePrebuilt.cmake @@ -0,0 +1,117 @@ +# Consume a precompiled aurora + third-party package instead of compiling +# aurora-main and its extern/ tree. +# +# Produced by launcher/Prepare-NativePrebuilt.ps1 with the same toolchain and the +# same canonical flag set the consuming build uses, which is what makes a single +# package valid for every user. Nothing about this project's own code changes: +# the runtime, the translated shards and the products still compile locally and +# still link targets named aurora::gx / aurora::pad / aurora::si / aurora::vi / +# aurora::mtx, so cmake/PublicProducts.cmake needs no prebuilt-specific branch. +# +# The aurora and dependency source trees are still present in the workspace - the +# runtime includes aurora headers directly - so the package records include +# directories as tokens (@AURORA@, @RUNTIME@, @DEPS@, @PKG@) that resolve here +# rather than baking a maintainer machine's paths into a redistributable. + +get_filename_component(MKW_NP_PACKAGE_DIR "${MKW_NATIVE_PREBUILT_DIR}" ABSOLUTE) +if(NOT EXISTS "${MKW_NP_PACKAGE_DIR}/native_prebuilt.cmake") + message(FATAL_ERROR + "MKW_NATIVE_PREBUILT_DIR does not contain native_prebuilt.cmake: ${MKW_NP_PACKAGE_DIR}") +endif() + +# The package ships as /Dependencies/native_prebuilt, next to the +# pinned sources it was built against. +get_filename_component(MKW_NP_DEPS_DIR "${MKW_NP_PACKAGE_DIR}" DIRECTORY) +get_filename_component(MKW_NP_AURORA_DIR "${MKW_AURORA_DIR}" ABSOLUTE) +get_filename_component(MKW_NP_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}" DIRECTORY) + +include("${MKW_NP_PACKAGE_DIR}/native_prebuilt.cmake") + +function(mkw_np_resolve out_var) + set(_resolved "") + foreach(_item IN LISTS ARGN) + string(REPLACE "@PKG@" "${MKW_NP_PACKAGE_DIR}" _item "${_item}") + string(REPLACE "@AURORA@" "${MKW_NP_AURORA_DIR}" _item "${_item}") + string(REPLACE "@RUNTIME@" "${MKW_NP_RUNTIME_DIR}" _item "${_item}") + string(REPLACE "@DEPS@" "${MKW_NP_DEPS_DIR}" _item "${_item}") + list(APPEND _resolved "${_item}") + endforeach() + set(${out_var} "${_resolved}" PARENT_SCOPE) +endfunction() + +mkw_np_resolve(_mkw_np_includes ${MKW_NP_INCLUDE_DIRECTORIES}) +mkw_np_resolve(_mkw_np_links ${MKW_NP_LINK_LIBRARIES}) +mkw_np_resolve(_mkw_np_dawn_config ${MKW_NP_DAWN_CONFIG_DIR}) + +foreach(_dir IN LISTS _mkw_np_includes) + if(NOT IS_DIRECTORY "${_dir}") + message(FATAL_ERROR + "The precompiled package expects an include directory that this workspace " + "does not have: ${_dir}") + endif() +endforeach() + +# Dawn is a prebuilt package on both sides; resolve the same install tree the +# package was built against so its imported target (and therefore +# webgpu_dawn.dll / dxcompiler.dll / dxil.dll) is available exactly as a +# from-source aurora build would provide it. +if(NOT EXISTS "${_mkw_np_dawn_config}/DawnConfig.cmake") + message(FATAL_ERROR "Prebuilt Dawn package is missing DawnConfig.cmake: ${_mkw_np_dawn_config}") +endif() +set(_mkw_np_saved_find_global ${CMAKE_FIND_PACKAGE_TARGETS_GLOBAL}) +set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON) +find_package(Dawn REQUIRED CONFIG PATHS "${_mkw_np_dawn_config}" NO_DEFAULT_PATH) +set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ${_mkw_np_saved_find_global}) +if(NOT TARGET dawn::webgpu_dawn) + message(FATAL_ERROR "Prebuilt Dawn package did not provide dawn::webgpu_dawn") +endif() + +# One interface target carries the whole compile/link surface. The public aurora +# names then reduce to aliases of it, which keeps the link line free of the +# fivefold duplication that giving each of them the full list would produce. +add_library(mkw_native_prebuilt INTERFACE IMPORTED GLOBAL) +set_target_properties(mkw_native_prebuilt PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${_mkw_np_includes}" + INTERFACE_COMPILE_DEFINITIONS "${MKW_NP_COMPILE_DEFINITIONS}" + INTERFACE_COMPILE_OPTIONS "${MKW_NP_COMPILE_OPTIONS}" + INTERFACE_LINK_LIBRARIES "${_mkw_np_links}") + +foreach(_target IN LISTS MKW_NP_AURORA_TARGETS) + add_library(${_target} INTERFACE IMPORTED GLOBAL) + set_target_properties(${_target} PROPERTIES INTERFACE_LINK_LIBRARIES mkw_native_prebuilt) +endforeach() + +# The vendored Crypto++ ships precompiled alongside aurora: under the fixed +# toolchain its archive is identical for every user, so compiling ~150 of its +# translation units locally buys nothing. Only the archive is consumed here - +# the vendored headers still ship in the workspace because first-party TUs +# include them directly (runtime/include/wii_es_crypto.h). +if(NOT MKW_CRYPTOPP_INTERFACE_DEFINITIONS) + message(FATAL_ERROR + "The prebuilt consumer requires MKW_CRYPTOPP_INTERFACE_DEFINITIONS from " + "the top-level CMakeLists.txt; without them first-party TUs would compile " + "against Crypto++ headers configured differently than the archive.") +endif() +file(GLOB _mkw_np_cryptopp_archives "${MKW_NP_PACKAGE_DIR}/lib/libmkw_cryptopp.a") +list(LENGTH _mkw_np_cryptopp_archives _mkw_np_cryptopp_count) +if(NOT _mkw_np_cryptopp_count EQUAL 1) + message(FATAL_ERROR + "The precompiled package must contain exactly one libmkw_cryptopp.a: " + "${MKW_NP_PACKAGE_DIR}") +endif() +if(NOT TARGET mkw_cryptopp) + add_library(mkw_cryptopp STATIC IMPORTED GLOBAL) + set_target_properties(mkw_cryptopp PROPERTIES + IMPORTED_LOCATION "${_mkw_np_cryptopp_archives}" + INTERFACE_INCLUDE_DIRECTORIES "${MKW_NP_RUNTIME_DIR}/third_party" + INTERFACE_COMPILE_DEFINITIONS "${MKW_CRYPTOPP_INTERFACE_DEFINITIONS}") + add_library(mkw::cryptopp ALIAS mkw_cryptopp) +endif() +# The aggregate link list carries the same archive once more (the harvest probe +# linked it, so the probe-vs-control diff includes it). Repeating a static +# archive on the link line is harmless; the second pass resolves no members. + +list(LENGTH _mkw_np_links _mkw_np_link_count) +message(STATUS + "Aurora and third-party libraries come from the precompiled package " + "(${_mkw_np_link_count} link inputs); they are not compiled here") diff --git a/runtime/cmake/NativePrebuiltExport.cmake b/runtime/cmake/NativePrebuiltExport.cmake new file mode 100644 index 0000000..89ffa9b --- /dev/null +++ b/runtime/cmake/NativePrebuiltExport.cmake @@ -0,0 +1,149 @@ +# Maintainer-only: describe the configured aurora + third-party build so it can +# be harvested into a redistributable precompiled package. +# +# Enabled with -DMKW_NATIVE_PREBUILT_EXPORT_DIR=. It never changes what is +# compiled: it adds two never-built probe targets and writes plain text files +# describing (a) which library targets aurora produced and where their build +# outputs land, and (b) the exact compile/link interface a consumer of +# aurora::gx/pad/si/vi/mtx receives. +# +# Everything is taken from the real, fully configured project rather than +# hand-written, so the package cannot describe a graph the build does not have. +# The compile/link interface is read back out of build.ninja by +# launcher/Prepare-NativePrebuilt.ps1: generator expressions cannot be evaluated +# for a specific language by file(GENERATE), and $ collapses to +# nothing outside a link context, so the generated buildsystem is the only place +# where the fully resolved, correctly ordered values exist. + +if(NOT CMAKE_GENERATOR STREQUAL "Ninja") + message(FATAL_ERROR "MKW_NATIVE_PREBUILT_EXPORT_DIR requires the Ninja generator") +endif() + +# Enumerates every target defined anywhere in the configured directory-scope +# tree rooted at _dir (aurora-main plus whatever its providers FetchContent'ed, +# whose sources live under the build tree rather than under aurora-main). The +# walk follows real SUBDIRECTORIES properties instead of parsing files, so it +# cannot disagree with the configure that just ran. The names are candidates +# only - each is re-checked below (TARGET existence, library type) and against +# the link closure mkw_np_probe actually pulled in before anything ships. +function(mkw_collect_buildsystem_targets _root_dir _out_var) + set(_candidates "") + set(_visited "") + set(_pending "${_root_dir}") + while(_pending) + list(POP_FRONT _pending _dir) + get_filename_component(_dir "${_dir}" ABSOLUTE) + if(_dir IN_LIST _visited) + continue() + endif() + list(APPEND _visited "${_dir}") + get_property(_scope_targets DIRECTORY "${_dir}" PROPERTY BUILDSYSTEM_TARGETS) + if(_scope_targets) + list(APPEND _candidates ${_scope_targets}) + endif() + get_property(_children DIRECTORY "${_dir}" PROPERTY SUBDIRECTORIES) + foreach(_child IN LISTS _children) + if(NOT IS_ABSOLUTE "${_child}") + set(_child "${_dir}/${_child}") + endif() + list(APPEND _pending "${_child}") + endforeach() + endwhile() + set(${_out_var} "${_candidates}" PARENT_SCOPE) +endfunction() + +set(_mkw_np_probe_dir "${CMAKE_BINARY_DIR}/native_prebuilt_probe") +file(MAKE_DIRECTORY "${_mkw_np_probe_dir}") +file(WRITE "${_mkw_np_probe_dir}/probe.cpp" + "// Records the compile and link interface aurora's public targets hand to a\n" + "// consumer. Building mkw_np_probe compiles exactly the aurora + third-party\n" + "// closure the game products need, and proves the resulting archives link.\n" + "int main() { return 0; }\n") + +# Control: identical target with no aurora dependency. Subtracting its command +# line from the probe's isolates exactly the aurora-derived contribution. +add_executable(mkw_np_probe_control EXCLUDE_FROM_ALL "${_mkw_np_probe_dir}/probe.cpp") +set_target_properties(mkw_np_probe_control PROPERTIES UNITY_BUILD OFF) +target_compile_features(mkw_np_probe_control PRIVATE cxx_std_20) + +add_executable(mkw_np_probe EXCLUDE_FROM_ALL "${_mkw_np_probe_dir}/probe.cpp") +set_target_properties(mkw_np_probe PROPERTIES UNITY_BUILD OFF) +target_compile_features(mkw_np_probe PRIVATE cxx_std_20) +# Exactly what mkw_runtime_common and the products link (PublicProducts.cmake) +# with one deliberate exception: pugixml stays a locally compiled single +# translation unit, so it must not enter this closure. The runtime's vendored +# Crypto++ does join the harvest - ~150 translation units that are identical +# for every user under the pinned toolchain and flag set. +target_link_libraries(mkw_np_probe PRIVATE + aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx + mkw::cryptopp) + +get_filename_component(_mkw_np_aurora_dir "${MKW_AURORA_DIR}" ABSOLUTE) +mkw_collect_buildsystem_targets("${_mkw_np_aurora_dir}" _mkw_np_all_targets) +# mkw_cryptopp is defined by this runtime project, not under aurora-main, so +# the scan above cannot see it; it is appended explicitly. The type and closure +# checks below still apply to it. +list(APPEND _mkw_np_all_targets "mkw_cryptopp") +if(NOT _mkw_np_all_targets) + message(FATAL_ERROR "No buildsystem targets were found under ${_mkw_np_aurora_dir}") +endif() +list(REMOVE_DUPLICATES _mkw_np_all_targets) +list(SORT _mkw_np_all_targets) + +set(_mkw_np_lib_targets "") +foreach(_t IN LISTS _mkw_np_all_targets) + if(NOT TARGET ${_t}) + continue() + endif() + get_target_property(_imported ${_t} IMPORTED) + if(_imported) + continue() + endif() + get_target_property(_type ${_t} TYPE) + if(_type STREQUAL "STATIC_LIBRARY" OR _type STREQUAL "SHARED_LIBRARY" OR + _type STREQUAL "MODULE_LIBRARY") + list(APPEND _mkw_np_lib_targets ${_t}) + endif() +endforeach() +if(NOT _mkw_np_lib_targets) + message(FATAL_ERROR "aurora produced no library targets to export") +endif() + +set(_mkw_np_target_lines "") +foreach(_t IN LISTS _mkw_np_lib_targets) + string(APPEND _mkw_np_target_lines + "${_t}|$|$|$\n") +endforeach() +file(GENERATE OUTPUT "${MKW_NATIVE_PREBUILT_EXPORT_DIR}/targets.txt" + CONTENT "${_mkw_np_target_lines}") + +# Dawn stays a prebuilt package on the consumer side too; record how this build +# resolved it so the consumer can reproduce the same imported target. +set(_mkw_np_dawn_line "") +if(TARGET dawn::webgpu_dawn) + set(_mkw_np_dawn_line + "$|$|$\n") +endif() +file(GENERATE OUTPUT "${MKW_NATIVE_PREBUILT_EXPORT_DIR}/dawn.txt" + CONTENT "${_mkw_np_dawn_line}") + +set(_mkw_np_dawn_config_dir "") +if(Dawn_DIR AND EXISTS "${Dawn_DIR}/DawnConfig.cmake") + set(_mkw_np_dawn_config_dir "${Dawn_DIR}") +endif() + +string(REPLACE ";" "," _mkw_np_lib_targets_csv "${_mkw_np_lib_targets}") +file(WRITE "${MKW_NATIVE_PREBUILT_EXPORT_DIR}/meta.txt" + "aurora_dir=${_mkw_np_aurora_dir}\n" + "runtime_dir=${CMAKE_CURRENT_SOURCE_DIR}\n" + "binary_dir=${CMAKE_BINARY_DIR}\n" + "dawn_config_dir=${_mkw_np_dawn_config_dir}\n" + "dawn_prebuilt_source_dir=${dawn_prebuilt_SOURCE_DIR}\n" + "aurora_sdl3_target=${AURORA_SDL3_TARGET}\n" + "aurora_dawn_version=${AURORA_DAWN_VERSION}\n" + "aurora_dawn_linkage=${AURORA_DAWN_LINKAGE}\n" + "cxx_compiler=${CMAKE_CXX_COMPILER}\n" + "cxx_compiler_version=${CMAKE_CXX_COMPILER_VERSION}\n" + "library_targets=${_mkw_np_lib_targets_csv}\n") + +message(STATUS "Native prebuilt export: ${MKW_NATIVE_PREBUILT_EXPORT_DIR}") diff --git a/runtime/cmake/PublicProducts.cmake b/runtime/cmake/PublicProducts.cmake new file mode 100644 index 0000000..db26440 --- /dev/null +++ b/runtime/cmake/PublicProducts.cmake @@ -0,0 +1,292 @@ +# Public WiiCompiled product graph. +# +# The translator owns the translated build graph. Mario Kart's profile-neutral +# functions are compiled once into mkw_base_shared; only callers whose direct +# ABI differs between profiles receive small base/RR variants. + +set(DATA_INIT_FILE "${MKW_RUNTIME_SOURCE_DIR}/../generated/data_sections_init.cpp") +set(DATA_INIT_BLOB_ASM "${MKW_RUNTIME_SOURCE_DIR}/../generated/data_sections_init_blobs.S") +if(EXISTS "${DATA_INIT_FILE}") + list(APPEND SOURCES "${DATA_INIT_FILE}") +endif() +# Crash-report symbolization table emitted by generate-data-init. The stub +# (deliberately outside the globbed src/ tree so it is never picked up twice) +# keeps link succeeding when the generated table has not been produced yet. +set(GUEST_SYMBOL_TABLE_FILE "${MKW_RUNTIME_SOURCE_DIR}/../generated/guest_symbol_table.cpp") +if(EXISTS "${GUEST_SYMBOL_TABLE_FILE}") + list(APPEND SOURCES "${GUEST_SYMBOL_TABLE_FILE}") +else() + list(APPEND SOURCES "${MKW_RUNTIME_SOURCE_DIR}/cmake/guest_symbol_table_stub.cpp") +endif() +if(EXISTS "${DATA_INIT_BLOB_ASM}") + enable_language(ASM) + set_source_files_properties("${DATA_INIT_BLOB_ASM}" PROPERTIES LANGUAGE ASM SKIP_UNITY_BUILD_INCLUSION ON) + list(APPEND SOURCES "${DATA_INIT_BLOB_ASM}") +endif() +list(REMOVE_DUPLICATES SOURCES) + +function(mkw_apply_common_compile_options target) + target_compile_options(${target} PRIVATE -O3 -ffast-math -w -pipe) +endfunction() + +function(mkw_apply_translated_compile_options target) + target_compile_options(${target} PRIVATE + -O2 ${MKW_TRANSLATED_PPC_FP_OPTIONS} -fno-slp-vectorize -w -pipe) +endfunction() + +function(mkw_configure_object_target target) + target_include_directories(${target} PRIVATE + "${MKW_RUNTIME_SOURCE_DIR}/include" + "${MKW_RUNTIME_SOURCE_DIR}/src" + # Workspace root, so translator output is spelled "generated/.h" + # instead of a ../ chain whose depth depends on the includer. + "${MKW_RUNTIME_SOURCE_DIR}/.." + "${MKW_RUNTIME_SOURCE_DIR}/../aurora-main/include") + target_compile_definitions(${target} PRIVATE + TARGET_PC) + set_target_properties(${target} PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON) +endfunction() + +# Translated shard TUs are the memory-hungry compiles; everything else in the build is +# comparatively small. A dedicated Ninja job pool caps how many of them run at once so the +# global parallelism can use every core for the cheap TUs without the memory ceiling being a +# guess. The pool depth is per-machine (derived from installed RAM by LocalBuild.ps1) and is +# deliberately not part of the canonical flag set: it changes scheduling, never output bytes. +if(MKW_TRANSLATED_COMPILE_JOBS GREATER 0) + set_property(GLOBAL APPEND PROPERTY JOB_POOLS "mkw_translated=${MKW_TRANSLATED_COMPILE_JOBS}") +endif() + +function(mkw_bound_translated_compiles target) + if(MKW_TRANSLATED_COMPILE_JOBS GREATER 0) + set_property(TARGET ${target} PROPERTY JOB_POOL_COMPILE mkw_translated) + endif() +endfunction() + +function(mkw_configure_translated_target target) + mkw_configure_object_target(${target}) + mkw_apply_translated_compile_options(${target}) + mkw_bound_translated_compiles(${target}) +endfunction() + +add_library(mkw_runtime_common OBJECT ${SOURCES}) +mkw_configure_object_target(mkw_runtime_common) +target_compile_features(mkw_runtime_common PRIVATE cxx_std_20) +target_compile_definitions(mkw_runtime_common PRIVATE + SDL_MAIN_HANDLED + _DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION) +target_link_libraries(mkw_runtime_common PRIVATE + aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx) +target_link_libraries(mkw_runtime_common PRIVATE mkw::pugixml mkw::toml11 mkw::cryptopp) +target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp) +if(MKW_CPPWINRT_INCLUDE_DIR) + if(NOT EXISTS "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h") + message(FATAL_ERROR + "MKW_CPPWINRT_INCLUDE_DIR does not contain winrt/base.h: ${MKW_CPPWINRT_INCLUDE_DIR}") + endif() + target_include_directories(mkw_runtime_common PRIVATE "${MKW_CPPWINRT_INCLUDE_DIR}") +endif() + +# Keep runtime unity units small and semantically related. The old generated-TU +# batch size put all 57 native runtime sources into one memory-heavy compiler job. +foreach(source IN LISTS SOURCES) + get_filename_component(source_name "${source}" NAME_WE) + string(REPLACE "\\" "/" source_normalized "${source}") + if(source_normalized MATCHES "/hle/gx/") + set(runtime_group "gx_bridge") + elseif(source_name MATCHES "network|socket|dns|dwc|ios") + set(runtime_group "network_ios") + elseif(source_name MATCHES "os_|system|memory|fiber|scheduler") + set(runtime_group "guest_system") + elseif(source_name MATCHES "debug|trace|prof") + set(runtime_group "diagnostics") + else() + string(SHA256 source_hash "${source_name}") + string(SUBSTRING "${source_hash}" 0 4 source_hash_prefix) + math(EXPR runtime_bucket "0x${source_hash_prefix} % 8") + set(runtime_group "runtime_${runtime_bucket}") + endif() + set_source_files_properties("${source}" PROPERTIES UNITY_GROUP "${runtime_group}") +endforeach() +# These translation units implement guest-visible floating-point bit +# semantics. Keep them out of the fast-math runtime unity groups and apply +# the same contraction/rounding policy as translated PPC shards. +set(MKW_PPC_SEMANTIC_RUNTIME_SOURCES + "${MKW_RUNTIME_SOURCE_DIR}/src/ppc_helpers.cpp" + "${MKW_RUNTIME_SOURCE_DIR}/src/fpu_helpers.cpp") +set_source_files_properties(${MKW_PPC_SEMANTIC_RUNTIME_SOURCES} PROPERTIES + SKIP_UNITY_BUILD_INCLUSION ON + SKIP_PRECOMPILE_HEADERS ON + COMPILE_OPTIONS "${MKW_TRANSLATED_PPC_FP_OPTIONS}") +set_target_properties(mkw_runtime_common PROPERTIES UNITY_BUILD ON UNITY_BUILD_MODE GROUP) +target_precompile_headers(mkw_runtime_common PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h") +mkw_apply_common_compile_options(mkw_runtime_common) + +# Host ISA guard. Everything in MKW_ALL_BUILD_TARGETS below is compiled with +# -march=x86-64-v3; this object library deliberately is not, which +# is the whole point of keeping it out of mkw_runtime_common. It runs a CPUID +# check from a C initializer so an unsupported machine gets a readable error +# instead of an illegal-instruction crash. Excluded from the unity build and the +# precompiled header because both are produced with the owning target's flags. +add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}") +target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17) +set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF) +target_compile_options(mkw_cpu_baseline PRIVATE -w) + +if(NOT MKW_BASE_COMMON_SHARDS) + message(FATAL_ERROR "Translator build graph contains no shared base shards") +endif() + +add_library(mkw_base_shared STATIC ${MKW_BASE_COMMON_SHARDS}) +mkw_configure_translated_target(mkw_base_shared) +target_precompile_headers(mkw_base_shared PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h") + +if(MKW_BASE_PORTABLE_SENSITIVE_SHARDS) + add_library(mkw_base_sensitive OBJECT ${MKW_BASE_PORTABLE_SENSITIVE_SHARDS}) + mkw_configure_translated_target(mkw_base_sensitive) + target_precompile_headers(mkw_base_sensitive REUSE_FROM mkw_base_shared) +endif() + +if(MKW_HAVE_RETRO_REWIND) + if(MKW_RETRO_PORTABLE_SENSITIVE_SHARDS) + add_library(mkw_retro_sensitive OBJECT ${MKW_RETRO_PORTABLE_SENSITIVE_SHARDS}) + mkw_configure_translated_target(mkw_retro_sensitive) + target_precompile_headers(mkw_retro_sensitive REUSE_FROM mkw_base_shared) + endif() + + set(MKW_RETRO_TRANSLATED_SOURCES ${MKW_RETRO_MOD_SHARDS} ${MKW_RETRO_EXTRA_SOURCES}) + set(MKW_RETRO_BLOB_OBJECTS) + foreach(source IN LISTS MKW_RETRO_EXTRA_SOURCES) + if(source MATCHES "\\.S$") + enable_language(ASM) + set_source_files_properties("${source}" PROPERTIES LANGUAGE ASM SKIP_PRECOMPILE_HEADERS ON) + endif() + endforeach() + add_library(mkw_retro_rewind_functions OBJECT ${MKW_RETRO_TRANSLATED_SOURCES}) + mkw_configure_translated_target(mkw_retro_rewind_functions) + target_precompile_headers(mkw_retro_rewind_functions REUSE_FROM mkw_base_shared) +endif() + +function(mkw_configure_product target) + target_sources(${target} PRIVATE $) + # Startup CPU check. Must stay a separate object library so it keeps the + # plain baseline ISA while everything around it is built for x86-64-v3. + target_sources(${target} PRIVATE $) + target_include_directories(${target} PRIVATE + "${MKW_RUNTIME_SOURCE_DIR}/include" + "${MKW_RUNTIME_SOURCE_DIR}/src" + # Workspace root, so translator output is spelled "generated/.h" + # instead of a ../ chain whose depth depends on the includer. + "${MKW_RUNTIME_SOURCE_DIR}/.." + "${MKW_RUNTIME_SOURCE_DIR}/../aurora-main/include") + target_compile_definitions(${target} PRIVATE + SDL_MAIN_HANDLED _DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION TARGET_PC) + target_compile_features(${target} PRIVATE cxx_std_20) + mkw_apply_common_compile_options(${target}) + # The dispatch-table and registration shards compile inside the product target itself and + # include the same fat translated headers; bound them by the same pool. + mkw_bound_translated_compiles(${target}) + target_link_libraries(${target} PRIVATE + mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp) + + target_link_libraries(${target} PRIVATE + aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx) + if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake") + include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake") + aurora_copy_runtime_dlls(${target}) + endif() + if(TARGET sqlite3) + get_target_property(MKW_SQLITE_TARGET_TYPE sqlite3 TYPE) + endif() + if(TARGET sqlite3 AND + (MKW_SQLITE_TARGET_TYPE STREQUAL "SHARED_LIBRARY" OR + MKW_SQLITE_TARGET_TYPE STREQUAL "MODULE_LIBRARY")) + add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ $) + endif() + + target_link_libraries(${target} PRIVATE + dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp) + + set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE) + foreach(runtime_dll libc++.dll libunwind.dll) + execute_process( + COMMAND "${CMAKE_CXX_COMPILER}" "--print-file-name=${runtime_dll}" + OUTPUT_VARIABLE runtime_dll_path + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT EXISTS "${runtime_dll_path}") + get_filename_component(mkw_compiler_bin "${CMAKE_CXX_COMPILER}" DIRECTORY) + set(runtime_dll_path "${mkw_compiler_bin}/${runtime_dll}") + endif() + if(NOT EXISTS "${runtime_dll_path}") + message(FATAL_ERROR "llvm-mingw runtime DLL not found: ${runtime_dll}") + endif() + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${runtime_dll_path}" $) + endforeach() + + set(MKW_WII_BOOTSTRAP_SOURCE_DIR "${MKW_RUNTIME_SOURCE_DIR}/assets/wii") + if(NOT EXISTS "${MKW_WII_BOOTSTRAP_SOURCE_DIR}/shared2/wc24") + message(FATAL_ERROR "Missing Wii first-run bootstrap payload: ${MKW_WII_BOOTSTRAP_SOURCE_DIR}") + endif() + add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory + "${MKW_WII_BOOTSTRAP_SOURCE_DIR}" "$/wii_bootstrap") + + set(MKW_DSP_COEFFICIENT_ROM "${MKW_RUNTIME_SOURCE_DIR}/assets/dsp/dsp_coef.bin") + if(NOT EXISTS "${MKW_DSP_COEFFICIENT_ROM}") + message(FATAL_ERROR "Missing Wii DSP coefficient ROM: ${MKW_DSP_COEFFICIENT_ROM}") + endif() + file(SHA256 "${MKW_DSP_COEFFICIENT_ROM}" MKW_DSP_COEFFICIENT_ROM_SHA256) + if(NOT MKW_DSP_COEFFICIENT_ROM_SHA256 STREQUAL + "d7741279c2e8ec5c5fb318f8fbdd6de6bf583520d288e836a5383233a4238179") + message(FATAL_ERROR "Wii DSP coefficient ROM hash mismatch: ${MKW_DSP_COEFFICIENT_ROM_SHA256}") + endif() + add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${MKW_DSP_COEFFICIENT_ROM}" "$/dsp_coef.bin") + + # Aurora imports this portable recipe database into each user's writable + # pipeline cache. Keep the upstream filename so its default resourcesPath + # lookup works without application-specific configuration. + set(MKW_INITIAL_PIPELINE_CACHE + "${MKW_RUNTIME_SOURCE_DIR}/assets/pipeline/initial_pipeline_cache.db") + if(NOT EXISTS "${MKW_INITIAL_PIPELINE_CACHE}") + message(FATAL_ERROR "Missing transferable Aurora pipeline cache: ${MKW_INITIAL_PIPELINE_CACHE}") + endif() + add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${MKW_INITIAL_PIPELINE_CACHE}" + "$/initial_pipeline_cache.db") +endfunction() + +add_executable(WiiCompiled "${MKW_BASE_PRODUCT_SOURCE}" ${MKW_BASE_REGISTRATION_SOURCES}) +mkw_configure_product(WiiCompiled) +target_precompile_headers(WiiCompiled PRIVATE + "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h") +if(TARGET mkw_base_sensitive) + target_sources(WiiCompiled PRIVATE $) +endif() + +if(MKW_HAVE_RETRO_REWIND) + add_executable(RetroRewind "${MKW_RETRO_REWIND_PRODUCT_SOURCE}" ${MKW_RETRO_REGISTRATION_SOURCES}) + mkw_configure_product(RetroRewind) + target_precompile_headers(RetroRewind REUSE_FROM WiiCompiled) + if(TARGET mkw_retro_sensitive) + target_sources(RetroRewind PRIVATE $) + endif() + target_sources(RetroRewind PRIVATE $) + if(MKW_RETRO_BLOB_OBJECTS) + target_sources(RetroRewind PRIVATE ${MKW_RETRO_BLOB_OBJECTS}) + endif() + add_custom_target(mkw_release DEPENDS WiiCompiled RetroRewind) +else() + add_custom_target(mkw_release DEPENDS WiiCompiled) + message(STATUS "RetroRewind target disabled (run translate-mod and emit-build-shards)") +endif() + +set(MKW_ALL_BUILD_TARGETS + mkw_runtime_common mkw_base_shared mkw_base_sensitive mkw_retro_sensitive + mkw_retro_rewind_functions WiiCompiled RetroRewind) +foreach(target IN LISTS MKW_ALL_BUILD_TARGETS) + if(TARGET ${target}) + target_compile_options(${target} PRIVATE -march=x86-64-v3) + endif() +endforeach() diff --git a/runtime/cmake/guest_symbol_table_stub.cpp b/runtime/cmake/guest_symbol_table_stub.cpp new file mode 100644 index 0000000..abe0076 --- /dev/null +++ b/runtime/cmake/guest_symbol_table_stub.cpp @@ -0,0 +1,18 @@ +// Fallback for builds where generated/guest_symbol_table.cpp has not been +// produced yet (generate-data-init emits the real table from the project's +// function map). An empty table simply disables crash-report symbolization. +#include + +extern "C" { + +// The extern declarations force external linkage: namespace-scope const +// objects are internal-linkage by default in C++, even inside extern "C". +extern const uint32_t kGuestMapSymbolCount; +extern const uint32_t kGuestMapSymbolAddresses[]; +extern const char* const kGuestMapSymbolNames[]; + +const uint32_t kGuestMapSymbolCount = 0u; +const uint32_t kGuestMapSymbolAddresses[] = {0u}; +const char* const kGuestMapSymbolNames[] = {""}; + +} // extern "C" diff --git a/runtime/include/abi_bridge.h b/runtime/include/abi_bridge.h new file mode 100644 index 0000000..e4b87f9 --- /dev/null +++ b/runtime/include/abi_bridge.h @@ -0,0 +1,794 @@ +#pragma once +#include "memory.h" +#include "ppc_runtime.h" +#include "system_bridge.h" +#include "game_graphics_options.h" +#include "runtime_log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +inline void InvokeIndirectCpu(uint32_t target, CpuContext* ctx); + +// Some game-facing runtime options alter arguments at well-defined ABI +// boundaries. Keep this independent of the dispatch mechanism: generated +// static calls deliberately bypass InvokeDirectCpu for performance. +// (used for the path mask filtering in ScnRenderer::createPath: depth of +// field is always removed, bloom when the user disabled it) +inline void ApplyRuntimeCallOptions(uint32_t target, CpuContext* ctx) { + if (target == 0x8023BD38u) { + // ScnRenderer::createPath receives the post-processing path mask in r4. + ctx->gpr[4] = RuntimeGameGraphicsOptions::FilterScnRendererPathMask(ctx->gpr[4]); + } +} + +// Persistent per-thread CPU context used across translated function calls. +CpuContext& GetPersistentCpuContext(); +void InitializePersistentCpuContext(); + +enum class FunctionKind : uint8_t { + BaseTranslated = 0, + ModTranslated = 1, + Native = 2, +}; + +inline constexpr uint32_t kPpcAllNonvolatileFprMask = 0xffffc000u; +inline constexpr uint32_t kBaseTranslatedFunctionPriority = 0; +inline constexpr uint32_t kModTranslatedFunctionPriorityBase = 100; +inline constexpr uint32_t kNativeFunctionPriority = 10000; + +#include "isa/ppc_isa_cr.h" + +struct TranslatedFunctionInfo { + uint32_t address = 0; + const char* name = ""; + uint64_t moduleId = 0; + uint32_t priority = 0; + uint32_t nonvolatileFprWriteMask = kPpcAllNonvolatileFprMask; + void* entryPoint = nullptr; // Raw typed function pointer for indirect-call resolution + void (*rawCpuInvoker)(CpuContext*) = nullptr; + bool mustRemainDynamicallyDispatchable = true; + FunctionKind kind = FunctionKind::BaseTranslated; +}; + +struct RawDispatchRecord { + uint32_t address = 0; + void (*entry)(CpuContext*) = nullptr; + uint32_t nonvolatileFprWriteMask = kPpcAllNonvolatileFprMask; + bool preserveNonvolatileGprs = false; +}; + +// Translator-owned indirect dispatch is split by the high address byte and +// then by 4 KiB guest pages. Each populated segment stores only the contiguous +// page range that contains verified function entry points. The final search is +// bounded to at most the 1024 aligned instruction addresses in one guest page. +struct StaticIndirectDispatchPage { + uint32_t firstEntry = 0; + uint16_t entryCount = 0; + // Explicit tail padding to an 8-byte stride. The emitter writes it as a + // literal third initializer, so it is part of the generated-data contract. + uint16_t reserved = 0; +}; + +struct StaticIndirectDispatchSegment { + const StaticIndirectDispatchPage* pages = nullptr; + uint16_t firstPage = 0; + uint16_t pageCount = 0; +}; + +struct StaticIndirectDispatchTable { + const char* profileName = nullptr; + const StaticIndirectDispatchSegment* segments = nullptr; // Exactly 256 high-byte segments. + const RawDispatchRecord* entries = nullptr; + size_t entryCount = 0; +}; + +inline const RawDispatchRecord* FindStaticIndirectDispatchEntry( + const StaticIndirectDispatchTable* table, + uint32_t address) noexcept { + if (!table || !table->segments || !table->entries || table->entryCount == 0) { + return nullptr; + } + + const auto& segment = table->segments[address >> 24]; + const uint32_t pageNumber = (address >> 12) & 0x0FFFu; + if (!segment.pages || pageNumber < segment.firstPage) { + return nullptr; + } + const uint32_t relativePage = pageNumber - segment.firstPage; + if (relativePage >= segment.pageCount) { + return nullptr; + } + + const auto& page = segment.pages[relativePage]; + const size_t first = page.firstEntry; + const size_t count = page.entryCount; + if (first > table->entryCount || count > table->entryCount - first) { + return nullptr; + } + + size_t lower = first; + size_t upper = first + count; + while (lower < upper) { + const size_t middle = lower + (upper - lower) / 2; + const uint32_t candidate = table->entries[middle].address; + if (candidate < address) { + lower = middle + 1; + } else { + upper = middle; + } + } + return lower < first + count && table->entries[lower].address == address + ? &table->entries[lower] + : nullptr; +} + +void RegisterStaticIndirectDispatchTable(const StaticIndirectDispatchTable* table); +inline std::atomic g_publishedStaticIndirectDispatchTable{nullptr}; + +class StaticIndirectDispatchTableRegistrar { +public: + explicit StaticIndirectDispatchTableRegistrar(const StaticIndirectDispatchTable* table) { + RegisterStaticIndirectDispatchTable(table); + } +}; + +inline unsigned PpcLowestSetBitIndex(uint32_t value) noexcept { + return static_cast(__builtin_ctz(value)); +} + +class PpcNonvolatileFprGuard { +public: + explicit PpcNonvolatileFprGuard(CpuContext* cpu, uint32_t mask = kPpcAllNonvolatileFprMask) noexcept + : cpu_(cpu), mask_(mask & kPpcAllNonvolatileFprMask) { + if (!cpu_ || mask_ == 0) { + return; + } + // Real masks are a single contiguous register run - f14..f31 for the + // conservative default and f23..f31 or similar for a measured writer - + // so resolve the run once and copy it as one straight-line block instead + // of testing 18 mask bits. Any other mask keeps the per-bit loop, which + // saves and restores exactly the same registers. + const uint32_t lowest = mask_ & (0u - mask_); + const uint32_t aboveRun = mask_ + lowest; + if ((aboveRun & (aboveRun - 1u)) == 0u) { + const unsigned first = PpcLowestSetBitIndex(mask_); + const unsigned end = aboveRun == 0u ? 32u : PpcLowestSetBitIndex(aboveRun); + firstSlot_ = static_cast(first - 14u); + slotCount_ = static_cast(end - first); + const size_t count = slotCount_; + for (size_t i = 0; i < count; ++i) { + saved_[firstSlot_ + i] = cpu_->fpr[first + i]; + } + return; + } + for (size_t i = 0; i < saved_.size(); ++i) { + if ((mask_ & (1u << (14 + i))) == 0) { + continue; + } + saved_[i] = cpu_->fpr[14 + i]; + } + } + + ~PpcNonvolatileFprGuard() noexcept { + if (!cpu_ || mask_ == 0) { + return; + } + if (slotCount_ != 0) { + const unsigned first = 14u + firstSlot_; + const size_t count = slotCount_; + for (size_t i = 0; i < count; ++i) { + cpu_->fpr[first + i] = saved_[firstSlot_ + i]; + } + return; + } + for (size_t i = 0; i < saved_.size(); ++i) { + if ((mask_ & (1u << (14 + i))) == 0) { + continue; + } + cpu_->fpr[14 + i] = saved_[i]; + } + } + + PpcNonvolatileFprGuard(const PpcNonvolatileFprGuard&) = delete; + PpcNonvolatileFprGuard& operator=(const PpcNonvolatileFprGuard&) = delete; + +private: + CpuContext* cpu_ = nullptr; + uint32_t mask_ = 0; + // Contiguous-run description of mask_. slotCount_ == 0 means the mask is not + // a single run, so the destructor mirrors the constructor's per-bit loop. + uint8_t firstSlot_ = 0; + uint8_t slotCount_ = 0; + // Every element selected by mask_ is written before it is read. Leaving the + // remaining slots uninitialized avoids zero-filling all 18 values around a + // call that may preserve only one or two architectural FPRs. + std::array saved_; +}; + +class PpcNonvolatileGprGuard { +public: + explicit PpcNonvolatileGprGuard(CpuContext* cpu, bool enabled = true) noexcept + : cpu_(enabled ? cpu : nullptr) { + if (!cpu_) { + return; + } + for (size_t i = 0; i < saved_.size(); ++i) { + saved_[i] = cpu_->gpr[14 + i]; + } + } + + ~PpcNonvolatileGprGuard() noexcept { + if (!cpu_) { + return; + } + for (size_t i = 0; i < saved_.size(); ++i) { + cpu_->gpr[14 + i] = saved_[i]; + } + } + + PpcNonvolatileGprGuard(const PpcNonvolatileGprGuard&) = delete; + PpcNonvolatileGprGuard& operator=(const PpcNonvolatileGprGuard&) = delete; + +private: + CpuContext* cpu_ = nullptr; + // Left uninitialized: enabled writes all 18 slots before reading them, disabled reads none, + // so skipping the zero-fill saves 72 bytes of stack work on the common disabled hot path. + std::array saved_; +}; + +// Guest address of OSLoadContext, which models rfi-style context restoration +// and intentionally replaces the full guest register file instead of returning +// like a normal ABI call. It can never be guarded. +inline constexpr uint32_t kOSLoadContextAddress = 0x801A1F58u; + +inline bool ShouldPreserveNonvolatileGprsForRawCpuCall(const TranslatedFunctionInfo* info) noexcept { + return info->kind == FunctionKind::Native && info->address != kOSLoadContextAddress; +} + +inline uint32_t NonvolatileFprGuardMaskFor(const TranslatedFunctionInfo* info) noexcept { + if (!info || info->nonvolatileFprWriteMask == 0) { + return 0; + } + return info->nonvolatileFprWriteMask & kPpcAllNonvolatileFprMask; +} + +template +struct KnownTranslatedCpuCall { + static constexpr bool kAvailable = false; + static constexpr uint32_t kNonvolatileFprWriteMask = kPpcAllNonvolatileFprMask; + static constexpr bool kMustRemainDynamicallyDispatchable = true; + static constexpr void (*Entry)(CpuContext*) = nullptr; +}; + +// Translator-emitted trait specializations, one macro shared by every generated shard. +// `addr` is the 8-digit hex entry point, `winner` the resolved symbol (mods/Retro Rewind can +// publish their own name). A specialization only exists for a single statically bound winner, +// so overridable/dynamically-dispatchable are hardcoded here rather than passed in. +#define MKW_TRANSLATED_TRAIT(addr, winner, nonvolatile_fpr_write_mask) \ + extern "C" void winner(CpuContext* ctx); \ + template <> \ + struct KnownTranslatedCpuCall<0x##addr##u> { \ + static constexpr bool kAvailable = true; \ + static constexpr uint32_t kNonvolatileFprWriteMask = nonvolatile_fpr_write_mask; \ + static constexpr bool kMustRemainDynamicallyDispatchable = false; \ + static constexpr void (*Entry)(CpuContext*) = &winner; \ + } + +template +struct KnownNativeCpuCall { + static constexpr bool kAvailable = false; + static constexpr uint32_t kNonvolatileFprWriteMask = kPpcAllNonvolatileFprMask; + static constexpr void (*Entry)(CpuContext*) = nullptr; +}; + +template +struct KnownTypedNativeCpuCall { + static constexpr bool kAvailable = false; +}; + +#include "native_cpu_calls.inc" + +// Direct-mapped thread-local memo in front of the sorted registry lookup every bctrl performs; +// a miss just re-runs the sorted lookup. 512 entries (12 KiB) covers the per-frame indirect +// working set while staying L1/L2 resident, unlike 4096 which would thrash L2. Must stay a +// power of two, the index masks with (size - 1). +inline constexpr size_t kIndirectDispatchCacheEntries = 512; + +// Namespace-scope `inline thread_local`, not function-local `static thread_local`, to avoid a +// thread-static init epoch check on every bctrl (same as g_currentCpuContext in ppc_runtime.h). +struct IndirectResolvedDispatchMemoEntry { + bool valid; + uint32_t address; + const TranslatedFunctionInfo* info; +}; +inline thread_local IndirectResolvedDispatchMemoEntry + g_indirectResolvedDispatchMemo[kIndirectDispatchCacheEntries]{}; + +struct IndirectRawDispatchMemoEntry { + bool valid; + uint32_t address; + const RawDispatchRecord* record; +}; +inline thread_local IndirectRawDispatchMemoEntry + g_indirectRawDispatchMemo[kIndirectDispatchCacheEntries]{}; + +class TranslatedFunctionRegistry { +public: + static void Register(TranslatedFunctionInfo info); + static void Finalize(); + static inline bool IsLookupPublished() noexcept { + return lookupPublished_.load(std::memory_order_acquire); + } + static std::optional FindNearestByAddress(uint32_t address); + static inline const TranslatedFunctionInfo* FindByAddressPtr(uint32_t address) { + if (!lookupPublished_.load(std::memory_order_acquire)) { + return FindByAddressPtrSlow(address); + } + + auto& cached = g_indirectResolvedDispatchMemo[ + ((address >> 2) * 2654435761u) & (kIndirectDispatchCacheEntries - 1)]; + if (cached.valid && cached.address == address) { + return cached.info; + } + + const auto* info = FindByAddressPtrSlow(address); + cached = {true, address, info}; + return info; + } + + // Hot wrapper: only the direct-mapped memo probe stays inline. Inlining the + // generated-table binary search alongside it pushed the whole function past + // the inliner's budget and out-of-lined the probe itself, which is the part + // every bctrl executes. + static MKW_PPC_FORCE_INLINE const RawDispatchRecord* FindRawByAddressPtr(uint32_t address) { + if (!lookupPublished_.load(std::memory_order_acquire)) { + return nullptr; + } + + auto& cached = g_indirectRawDispatchMemo[ + ((address >> 2) * 2654435761u) & (kIndirectDispatchCacheEntries - 1)]; + if (cached.valid && cached.address == address) { + return cached.record; + } + + return FindRawByAddressPtrMiss(address, cached); + } + // Look up a registered function by host (native) address. + // This enables stack trace symbolication for translated functions. + static std::optional FindByHostAddress(uintptr_t hostAddr); + +private: + // Memo miss: resolve against the header-inline generated table first; only dynamically + // registered records or a not-yet-published table fall through to the slow path. + static MKW_PPC_NO_INLINE MKW_PPC_COLD const RawDispatchRecord* FindRawByAddressPtrMiss( + uint32_t address, IndirectRawDispatchMemoEntry& cached) { + const RawDispatchRecord* record = nullptr; + if (const auto* table = + g_publishedStaticIndirectDispatchTable.load(std::memory_order_acquire)) { + record = FindStaticIndirectDispatchEntry(table, address); + } + if (record == nullptr) { + record = FindRawByAddressPtrSlow(address); + } + cached = {true, address, record}; + return record; + } + + static const TranslatedFunctionInfo* FindByAddressPtrSlow(uint32_t address); + static const RawDispatchRecord* FindRawByAddressPtrSlow(uint32_t address); + + // Finalize publishes immutable registry, generated-table, and dynamic raw + // dispatch storage. Header-inlined cache hits are enabled only after that + // release publication, so a pre-finalization miss can never poison them. + inline static std::atomic_bool lookupPublished_{false}; +}; + +// Cold, profile-owned registration data is emitted in compact table shards. +// Keeping it out of generated function bodies avoids one static constructor +// per translated function. +struct BulkTranslatedFunctionRecord { + uint32_t address; + const char* name; + void (*entry)(CpuContext*); + FunctionKind kind; + bool preservesNonvolatileFprs; + uint32_t nonvolatileFprWriteMask; + uint32_t priority; + uint64_t moduleId; + bool mustRemainDynamicallyDispatchable; +}; + +void RegisterBulkTranslatedFunctions(const BulkTranslatedFunctionRecord* records, size_t count); + +class BulkTranslatedFunctionRegistrar { +public: + BulkTranslatedFunctionRegistrar(const BulkTranslatedFunctionRecord* records, size_t count) { + RegisterBulkTranslatedFunctions(records, count); + } +}; + +MKW_PPC_FORCE_INLINE bool TryDispatchResolvedCpuTarget(const TranslatedFunctionInfo* info, CpuContext* cpu) { + if (!info || !info->rawCpuInvoker) { + return false; + } + + RecompMod::ScopedTranslatedExecutionAddress translatedExecution(info->address); + PpcNonvolatileFprGuard fprGuard(cpu, NonvolatileFprGuardMaskFor(info)); + PpcNonvolatileGprGuard gprGuard(cpu, ShouldPreserveNonvolatileGprsForRawCpuCall(info)); + if (TryGetCpuContext() != cpu) { + CpuContextScope scope(cpu); + info->rawCpuInvoker(cpu); + return true; + } + info->rawCpuInvoker(cpu); + return true; +} + +inline bool TryDispatchRawCpuTarget(const RawDispatchRecord* record, CpuContext* cpu) { + if (!record || !record->entry) { + return false; + } + RecompMod::ScopedTranslatedExecutionAddress translatedExecution(record->address); + PpcNonvolatileGprGuard gprGuard(cpu, record->preserveNonvolatileGprs); + PpcNonvolatileFprGuard fprGuard(cpu, record->nonvolatileFprWriteMask); + if (TryGetCpuContext() != cpu) { + CpuContextScope scope(cpu); + record->entry(cpu); + } else { + record->entry(cpu); + } + return true; +} + +template +inline void DispatchKnownTranslatedCpuTargetStatic(CpuContext* cpu) { + const auto invokeKnownTranslated = [&]() { + KnownTranslatedCpuCall::Entry(cpu); + }; + + // A statically resolved, non-overridable translated call already running + // in this CpuContext needs no registry, context, or diagnostic target + // transition. This is the normal generated-to-generated hot path. + if (TryGetCpuContext() == cpu) { + if constexpr (KnownTranslatedCpuCall::kNonvolatileFprWriteMask == 0) { + invokeKnownTranslated(); + return; + } else { + PpcNonvolatileFprGuard fprGuard( + cpu, KnownTranslatedCpuCall::kNonvolatileFprWriteMask); + invokeKnownTranslated(); + return; + } + } + + CpuContextScope contextScope(cpu); + RecompMod::ScopedTranslatedExecutionAddress translatedExecution(Target); + if constexpr (KnownTranslatedCpuCall::kNonvolatileFprWriteMask == 0) { + invokeKnownTranslated(); + return; + } else { + PpcNonvolatileFprGuard fprGuard(cpu, KnownTranslatedCpuCall::kNonvolatileFprWriteMask); + invokeKnownTranslated(); + } +} + +[[noreturn]] inline void ReportMissingCpuTarget(uint32_t target, CpuContext* cpu) { + // Centralized crash path: stderr diagnostics, heuristics, crash artifacts + // in the run's log folder, popup, exit. + RuntimeCrash::FatalMissingGuestTarget(target, cpu); +} + +template +inline const TranslatedFunctionInfo* ResolveDirectCpuTargetInfo() { + return TranslatedFunctionRegistry::FindByAddressPtr(Target); +} + +template +inline const TranslatedFunctionInfo* ResolveDirectCpuTargetInfoCached() { + // Only latch a result once the registry is published (immutable for the process lifetime); + // a pre-finalization miss or provisional winner must never be cached. 0 = not yet resolved, + // 1 = negative-cache sentinel, avoiding a second ready flag on the hot path. + static std::atomic cachedValue{0}; + constexpr uintptr_t kNegativeCache = 1; + const uintptr_t cached = cachedValue.load(std::memory_order_acquire); + if (cached != 0) { + return cached == kNegativeCache + ? nullptr + : reinterpret_cast(cached); + } + if (!TranslatedFunctionRegistry::IsLookupPublished()) { + return ResolveDirectCpuTargetInfo(); + } + const auto* info = ResolveDirectCpuTargetInfo(); + cachedValue.store(info != nullptr + ? reinterpret_cast(info) + : kNegativeCache, + std::memory_order_release); + return info; +} + +// Caches the "is the published winner still base translation?" verdict for state-free fast +// paths guarding tens of thousands of call sites. Same latching rule as above: 0 = unresolved, +// 1 = negative, 2 = positive, and a verdict computed pre-publication is never stored. +template +inline bool IsBaseTranslatedCpuTargetActive() { + static std::atomic cachedVerdict{0}; + constexpr uint8_t kNegativeVerdict = 1; + constexpr uint8_t kPositiveVerdict = 2; + const uint8_t cached = cachedVerdict.load(std::memory_order_acquire); + if (cached != 0) { + return cached == kPositiveVerdict; + } + const bool publishable = TranslatedFunctionRegistry::IsLookupPublished(); + const auto* info = ResolveDirectCpuTargetInfo(); + const bool active = info != nullptr && info->kind == FunctionKind::BaseTranslated; + if (publishable) { + cachedVerdict.store(active ? kPositiveVerdict : kNegativeVerdict, + std::memory_order_release); + } + return active; +} + +template +inline void InvokeDirectCpu(CpuContext* ctx) { + static_assert(Target != 0, "InvokeDirectCpu cannot target address 0"); + CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext(); + ApplyRuntimeCallOptions(Target, cpu); + if constexpr (KnownNativeCpuCall::kAvailable) { + const auto invokeKnownNative = [&]() { + PpcNonvolatileGprGuard gprGuard(cpu); + if (TryGetCpuContext() != cpu) { + CpuContextScope scope(cpu); + KnownNativeCpuCall::Entry(cpu); + return; + } + KnownNativeCpuCall::Entry(cpu); + }; + if constexpr (KnownNativeCpuCall::kNonvolatileFprWriteMask == 0) { + invokeKnownNative(); + } else { + PpcNonvolatileFprGuard fprGuard(cpu, KnownNativeCpuCall::kNonvolatileFprWriteMask); + invokeKnownNative(); + } + return; + } + + if constexpr (KnownTypedNativeCpuCall::kAvailable) { + PpcNonvolatileGprGuard gprGuard(cpu); + if (TryGetCpuContext() != cpu) { + CpuContextScope scope(cpu); + KnownTypedNativeCpuCall::Invoke(cpu); + } else { + KnownTypedNativeCpuCall::Invoke(cpu); + } + return; + } + + if constexpr (KnownTranslatedCpuCall::kAvailable) { + DispatchKnownTranslatedCpuTargetStatic(cpu); + return; + } + + const auto* registeredInfo = ResolveDirectCpuTargetInfoCached(); + if (TryDispatchResolvedCpuTarget(registeredInfo, cpu)) { + return; + } + + ReportMissingCpuTarget(Target, cpu); +} + +// Indirect jump (bctr without link) - used for switch tables. +inline void InvokeIndirectJump(uint32_t target, CpuContext* ctx) { + // Use the caller's context, not the persistent global context! + // This is essential for tail calls via bctr where the 'this' pointer (r3) + // must be passed correctly to the target function. + CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext(); + if (TryDispatchRawCpuTarget(TranslatedFunctionRegistry::FindRawByAddressPtr(target), cpu)) { + return; + } + + // Cold fallback for dynamically registered or signature-only targets. + const auto* info = TranslatedFunctionRegistry::FindByAddressPtr(target); + if (info) { + if (TryDispatchResolvedCpuTarget(info, cpu)) { + return; + } + } + + // If not registered, this is likely a switch table jump to an intra-function label. + // The translator should have recognized this pattern and generated proper switch code. + RT_LOG(RT_TAG_RUNTIME) << "InvokeIndirectJump: target 0x" + << std::hex << target << std::dec + << " is not a registered function.\n" + << "This is likely a switch statement jump table that the translator\n" + << "failed to recognize. The function containing this bctr needs\n" + << "proper switch pattern recognition." << std::endl; + RT_LOG(RT_TAG_RUNTIME) << "Indirect jump context: pc=0x" << std::hex << cpu->pc + << " lr=0x" << cpu->lr << " ctr=0x" << cpu->ctr + << " r1=0x" << cpu->gpr[1] << " r3=0x" << cpu->gpr[3] + << " r29=0x" << cpu->gpr[29] << " r30=0x" << cpu->gpr[30] + << " r31=0x" << cpu->gpr[31] << std::dec << std::endl; + DumpHostStackTraceForRuntimeHelper(); + std::fflush(stderr); + std::ostringstream message; + message << "The game stopped because an indirect jump targeted guest address 0x" + << std::hex << target + << ", but that address is not a registered translated function.\n\n" + << "The translator may have missed a switch/jump-table target."; + // Same artifact set as RuntimeCrash::FatalMissingGuestTarget, which is this + // path's sibling for indirect *calls*: MarkFatalErrorReported below stops the + // atexit reporter, so the crash log has to be written here. + RuntimeCrash::WriteCrashArtifacts("missing_jump_target", message.str(), &target); + SetRuntimeExitCode(EXIT_FAILURE); + ShowRuntimeFatalPopup("Missing indirect jump target", message.str()); + MarkFatalErrorReported(); + std::exit(EXIT_FAILURE); +} + +inline void InvokeIndirectCpu(uint32_t target, CpuContext* ctx) { + CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext(); + if (target == 0) { + ReportMissingCpuTarget(target, cpu); + } + ApplyRuntimeCallOptions(target, cpu); + if (TryDispatchRawCpuTarget(TranslatedFunctionRegistry::FindRawByAddressPtr(target), cpu)) { + return; + } + if (TryDispatchResolvedCpuTarget(TranslatedFunctionRegistry::FindByAddressPtr(target), cpu)) { + return; + } + ReportMissingCpuTarget(target, cpu); +} + +template +class AbiTrampoline; + +template +class AbiTrampoline { +public: + using TargetFn = Ret (*)(Args...); + + AbiTrampoline(uint32_t address, + const char* name, + TargetFn target, + FunctionKind kind = FunctionKind::BaseTranslated, + bool preservesNonvolatileFprs = false, + uint32_t nonvolatileFprWriteMask = kPpcAllNonvolatileFprMask, + uint32_t priority = 0, + uint64_t moduleId = 0, + void (*rawCpuInvoker)(CpuContext*) = nullptr, + bool mustRemainDynamicallyDispatchable = true) + { + TranslatedFunctionInfo info; + info.address = address; + info.name = name ? name : ""; + info.moduleId = moduleId; + info.priority = priority; + const bool effectivePreservesNonvolatileFprs = + preservesNonvolatileFprs || kind == FunctionKind::Native; + info.nonvolatileFprWriteMask = effectivePreservesNonvolatileFprs ? 0 : (nonvolatileFprWriteMask & kPpcAllNonvolatileFprMask); + info.entryPoint = reinterpret_cast(target); + info.rawCpuInvoker = rawCpuInvoker; + info.mustRemainDynamicallyDispatchable = mustRemainDynamicallyDispatchable; + info.kind = kind; + TranslatedFunctionRegistry::Register(std::move(info)); + } + + static std::tuple BuildArgs(CpuContext* cpu) { + size_t gprIndex = 0; + size_t fprIndex = 0; + return std::tuple{LoadArgument(cpu, gprIndex, fprIndex)...}; + } + + template + static std::remove_reference_t LoadArgument(CpuContext* cpu, size_t& gprIndex, size_t& fprIndex) { + using CleanT = std::remove_reference_t; + if constexpr (std::is_floating_point_v) { + if (fprIndex >= 13) { + throw std::out_of_range("FPR argument overflow"); + } + return static_cast(cpu->fpr[1 + fprIndex++].d); + } else { + if (gprIndex >= 8) { + size_t stackArgIndex = gprIndex - 8; + uint32_t sp = cpu->gpr[1]; + uint32_t argAddr = sp + 8 + static_cast(stackArgIndex * 4); + gprIndex++; + try { + return static_cast(Memory::Read32(argAddr)); + } catch (const Memory::AccessViolation&) { + RT_LOG(RT_TAG_RUNTIME) << "Stack parameter read failed at 0x" << std::hex << argAddr + << " (SP=0x" << sp << ", argIndex=" << std::dec << (gprIndex - 1) << ")" << std::endl; + throw; + } + } + return static_cast(cpu->gpr[3 + gprIndex++]); + } + } + + static void InvokeCpu(TargetFn target, CpuContext* cpu) { + auto args = BuildArgs(cpu); + if constexpr (std::is_void_v) { + std::apply(target, args); + } else { + auto result = std::apply(target, args); + if constexpr (std::is_floating_point_v) { + cpu->fpr[1].d = static_cast(result); + } else { + cpu->gpr[3] = static_cast(result); + } + } + } +}; + +template +struct AbiRawCpuThunk { + static void Invoke(CpuContext* cpu) { + if constexpr (std::is_invocable_v) { + (void)Target(cpu); + } else { + AbiTrampoline>::InvokeCpu(Target, cpu); + } + } +}; + +template +class AbiTrampoline { +public: + using TargetFn = Ret (*)(CpuContext*); + + AbiTrampoline(uint32_t address, + const char* name, + TargetFn target, + FunctionKind kind = FunctionKind::BaseTranslated, + bool preservesNonvolatileFprs = false, + uint32_t nonvolatileFprWriteMask = kPpcAllNonvolatileFprMask, + uint32_t priority = 0, + uint64_t moduleId = 0, + void (*rawCpuInvoker)(CpuContext*) = nullptr, + bool mustRemainDynamicallyDispatchable = true) + { + TranslatedFunctionInfo info; + info.address = address; + info.name = name ? name : ""; + info.moduleId = moduleId; + info.priority = priority; + // Deliberately *not* mirroring the typed specialization's + // `|| kind == FunctionKind::Native`. A CpuContext* native receives the + // whole guest register file and can legitimately write f14-f31 through + // it, so it stays conservative until the individual address has been + // measured clean and its registration opts out below. + info.nonvolatileFprWriteMask = preservesNonvolatileFprs ? 0 : (nonvolatileFprWriteMask & kPpcAllNonvolatileFprMask); + info.entryPoint = reinterpret_cast(target); + info.rawCpuInvoker = rawCpuInvoker ? rawCpuInvoker : reinterpret_cast(target); + info.mustRemainDynamicallyDispatchable = mustRemainDynamicallyDispatchable; + info.kind = kind; + TranslatedFunctionRegistry::Register(std::move(info)); + } +}; + +#define MKW_DETAIL_CAT(a, b) a##b +#define MKW_DETAIL_MAKE_UNIQUE(a, b) MKW_DETAIL_CAT(a, b) + +// Three hand-written registration macros. Generated code never registers per function; every +// translated function reaches the registry via the bulk BulkTranslatedFunctionRecord tables +// in the translator's *_registration TUs. +#define REGISTER_TRANSLATED_FUNCTION(address, fn) \ + static AbiTrampoline MKW_DETAIL_MAKE_UNIQUE(_abi_trampoline_, __COUNTER__)(address, #fn, fn, FunctionKind::BaseTranslated, false, kPpcAllNonvolatileFprMask, 0, 0, nullptr, KnownTranslatedCpuCall
::kMustRemainDynamicallyDispatchable) + +#define REGISTER_NATIVE_FUNCTION(address, fn) \ + static AbiTrampoline MKW_DETAIL_MAKE_UNIQUE(_abi_native_trampoline_, __COUNTER__)(address, #fn, fn, FunctionKind::Native, false, kPpcAllNonvolatileFprMask, 0, 0, &AbiRawCpuThunk<&fn>::Invoke) + +#define REGISTER_NATIVE_FUNCTION_AS(address, fn, pretty_name) \ + static AbiTrampoline MKW_DETAIL_MAKE_UNIQUE(_abi_native_trampoline_named_, __COUNTER__)(address, pretty_name, fn, FunctionKind::Native, false, kPpcAllNonvolatileFprMask, 0, 0, &AbiRawCpuThunk<&fn>::Invoke) diff --git a/runtime/include/audio_backend.h b/runtime/include/audio_backend.h new file mode 100644 index 0000000..ae05827 --- /dev/null +++ b/runtime/include/audio_backend.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include + +#include + +class AudioBackend { +public: + static AudioBackend& Instance(); + + bool Init(uint32_t sampleRate, uint32_t channels); + void Shutdown(); + + // Wii AI DMA frames are big-endian and ordered right, left. SDL expects + // native-endian interleaved left, right samples. + bool PushWiiAiSamplesBE16(const uint8_t* data, size_t bytes); + bool PushSamplesLE16(const int16_t* samples, size_t sampleCount); + + // Applied to the final host output, covering both AX and direct AI DMA. + void SetMasterVolume(float volume); + void SetMuted(bool muted); + +private: + AudioBackend() = default; + ~AudioBackend() = default; + AudioBackend(const AudioBackend&) = delete; + AudioBackend& operator=(const AudioBackend&) = delete; + + bool EnsureInitializedLocked(uint32_t sampleRate, uint32_t channels); + bool QueueHasCapacityLocked(int incomingBytes); + uint32_t QueueLimitBytesLocked() const; + float EffectiveGainLocked() const; + void ApplyGainLocked(); + + mutable std::mutex m_mutex; + SDL_AudioStream* m_stream = nullptr; + SDL_AudioSpec m_spec{}; + uint32_t m_sampleRate = 0; + uint32_t m_channels = 0; + bool m_initialized = false; + float m_masterVolume = 1.0f; + bool m_muted = false; + bool m_reportedDroppedBlock = false; + std::vector m_convertBuffer; +}; diff --git a/runtime/include/aurora_events.h b/runtime/include/aurora_events.h new file mode 100644 index 0000000..ddb2df8 --- /dev/null +++ b/runtime/include/aurora_events.h @@ -0,0 +1,142 @@ +#pragma once + +#include "settings_overlay.h" +#include "runtime_config.h" + +#include +#include +#include + +#include +#include +#include + +#if defined(_WIN32) +#include +#endif + +extern "C" bool g_dynamicAspectRatioEnabled; +void ConfigureMkwDynamicAspect(bool widescreen, uint32_t surfaceWidth, uint32_t surfaceHeight); +void UpdateMkwDynamicAspectSurface(uint32_t surfaceWidth, uint32_t surfaceHeight); +// Arms the "keep EGG::Frustum's projection scale" flag on every screen that +// renders to a fixed-size offscreen target. Cheap and idempotent; called from +// the GX viewport path so it beats bakes that never cross a frame boundary. +void AssertMkwOffscreenScreenBypass(); +inline std::atomic_bool g_mkwDynamicAspectSurfacePending{false}; + +namespace WindowPlacementPersistence { +inline bool sizeDirty = false; +inline bool positionDirty = false; +inline uint32_t width = 0; +inline uint32_t height = 0; +inline int32_t x = 0; +inline int32_t y = 0; +inline std::chrono::steady_clock::time_point changedAt{}; + +inline void Flush(bool force = false) { + if (!sizeDirty && !positionDirty) { + return; + } + constexpr auto kSaveDelay = std::chrono::milliseconds(300); + if (!force && std::chrono::steady_clock::now() - changedAt < kSaveDelay) { + return; + } + if (sizeDirty && width != 0 && height != 0) { + RuntimeConfigFile::SetWindowSize(width, height); + } + if (positionDirty) { + RuntimeConfigFile::SetWindowPosition(x, y); + } + sizeDirty = false; + positionDirty = false; +} +} // namespace WindowPlacementPersistence + +// The close event can be consumed while execution is inside a guest fiber +// (for example, OSSleepThread). Running normal C++/CRT shutdown from that +// fiber re-enters runtime teardown and can fault while the fiber machinery is +// still active. A window close is an intentional successful exit, so end the +// process directly and do not run the crash/atexit paths. +[[noreturn]] inline void ExitForAuroraWindowClose() noexcept { + WindowPlacementPersistence::Flush(true); +#if defined(_WIN32) + ::ExitProcess(0); +#else + std::_Exit(EXIT_SUCCESS); +#endif +} + +// Update cached Aurora window/framebuffer dimensions based on pending events. +inline void ProcessAuroraEvents(const AuroraEvent* events) { + if (!events) { + return; + } + + bool surfaceChanged = false; + for (const AuroraEvent* event = events; event->type != AURORA_NONE; ++event) { + switch (event->type) { + case AURORA_WINDOW_MOVED: + if (aurora_get_display_mode() == AURORA_DISPLAY_MODE_WINDOWED) { + WindowPlacementPersistence::x = event->windowPos.x; + WindowPlacementPersistence::y = event->windowPos.y; + WindowPlacementPersistence::positionDirty = true; + WindowPlacementPersistence::changedAt = std::chrono::steady_clock::now(); + } + break; + case AURORA_WINDOW_RESIZED: + if (aurora_get_display_mode() == AURORA_DISPLAY_MODE_WINDOWED && + event->windowSize.width != 0 && event->windowSize.height != 0) { + WindowPlacementPersistence::width = event->windowSize.width; + WindowPlacementPersistence::height = event->windowSize.height; + WindowPlacementPersistence::sizeDirty = true; + WindowPlacementPersistence::changedAt = std::chrono::steady_clock::now(); + } + surfaceChanged = true; + break; + case AURORA_DISPLAY_SCALE_CHANGED: + surfaceChanged = true; + break; + case AURORA_EXIT: + ExitForAuroraWindowClose(); + default: + break; + } + } + + WindowPlacementPersistence::Flush(); + + if (surfaceChanged) { + // Applying the viewport policy drains GX. Event dispatch can run + // between asynchronous end_frame and the next begin_frame, while the + // frame worker is intentionally waiting for begin permission. Joining + // it here creates a circular wait. Record the newest native size and + // apply it immediately after the next frame has been prepared. + g_mkwDynamicAspectSurfacePending.store(true, std::memory_order_release); + } + settings_overlay::HandleEvents(events); +} + +inline void ApplyPendingMkwDynamicAspectSurface() { + // The OS can adjust a window without a resize event reaching the queue + // (observed with hidden windows clamped to the work area), so re-read the + // surface at every frame boundary instead of only on queued events. + // UpdateMkwDynamicAspectSurface is idempotent and cheap for a stable size. + (void)g_mkwDynamicAspectSurfacePending.exchange(false, std::memory_order_acq_rel); + uint32_t surfaceWidth = 0; + uint32_t surfaceHeight = 0; + AuroraGetSurfaceSize(&surfaceWidth, &surfaceHeight); + UpdateMkwDynamicAspectSurface(surfaceWidth, surfaceHeight); +} + +inline bool BeginAuroraFrame() { + if (!aurora_begin_frame()) { + return false; + } + ApplyPendingMkwDynamicAspectSurface(); + return true; +} + +// Poll Aurora events and update cached window/framebuffer dimensions. +inline void UpdateAuroraAndProcessEvents() { + ProcessAuroraEvents(aurora_update()); +} diff --git a/runtime/include/console_identity.h b/runtime/include/console_identity.h new file mode 100644 index 0000000..2d70745 --- /dev/null +++ b/runtime/include/console_identity.h @@ -0,0 +1,151 @@ +#pragma once + +#include "runtime_config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace RuntimeConsoleIdentity { + +struct Identity { + std::string serial; + std::array mac; +}; + +inline bool IsValidSerial(const std::string& serial) { + return serial.size() == 9 && + serial != "000000000" && + std::all_of(serial.begin(), serial.end(), + [](unsigned char value) { return std::isdigit(value) != 0; }); +} + +inline Identity FromSerial(std::string serial) { + // Keep Nintendo's Wii OUI. The suffix is derived from the persisted serial + // so every API exposes one coherent, stable virtual-console identity. + uint32_t hash = 2166136261u; + for (const unsigned char value : serial) { + hash ^= value; + hash *= 16777619u; + } + uint32_t suffix = hash & 0x00FFFFFFu; + if (suffix == 0 || suffix == 0x00FFFFFFu) { + suffix ^= 0x005A17C3u; + } + + return { + std::move(serial), + { + 0x00, + 0x09, + 0xBF, + static_cast(suffix >> 16), + static_cast(suffix >> 8), + static_cast(suffix), + }, + }; +} + +inline std::optional ReadSerial(const std::filesystem::path& path) { + std::ifstream input(path); + std::string line; + if (!input || !std::getline(input, line)) { + return std::nullopt; + } + constexpr std::string_view prefix = "serial="; + if (line.rfind(prefix, 0) != 0) { + return std::nullopt; + } + std::string serial = line.substr(prefix.size()); + if (!IsValidSerial(serial)) { + return std::nullopt; + } + return serial; +} + +inline bool WriteSerial(const std::filesystem::path& path, const std::string& serial) { + std::error_code ec; + std::filesystem::create_directories(path.parent_path(), ec); + if (ec) { + return false; + } + + const std::filesystem::path temporary = path.string() + ".tmp"; + { + std::ofstream output(temporary, std::ios::trunc); + if (!output) { + return false; + } + output << "serial=" << serial << '\n'; + output.close(); + if (!output) { + return false; + } + } + + std::filesystem::rename(temporary, path, ec); + if (!ec) { + return true; + } + std::filesystem::remove(temporary, ec); + return false; +} + +inline std::string GenerateSerial() { + std::random_device entropy; + std::seed_seq seed{ + entropy(), + entropy(), + entropy(), + entropy(), + }; + std::mt19937 generator(seed); + std::uniform_int_distribution distribution(100000000u, 999999999u); + return std::to_string(distribution(generator)); +} + +inline Identity LoadOrCreate(const std::filesystem::path& path) { + if (const auto serial = ReadSerial(path)) { + return FromSerial(*serial); + } + + const std::string generated = GenerateSerial(); + if (WriteSerial(path, generated)) { + return FromSerial(generated); + } + + // Remain operational in a read-only environment. This fallback matches + // Dolphin's deterministic serial while keeping the same valid identity shape. + return FromSerial("123456789"); +} + +inline const Identity& Current() { + static const Identity identity = + LoadOrCreate(RuntimeConfigFile::ApplicationDataDirectory() / "ConsoleIdentity.txt"); + return identity; +} + +inline std::string FormatMac(const std::array& mac) { + std::ostringstream output; + output << std::uppercase << std::hex << std::setfill('0'); + for (size_t index = 0; index < mac.size(); ++index) { + if (index != 0) { + output << ':'; + } + output << std::setw(2) << static_cast(mac[index]); + } + return output.str(); +} + +} // namespace RuntimeConsoleIdentity diff --git a/runtime/include/fiber_manager.h b/runtime/include/fiber_manager.h new file mode 100644 index 0000000..dbe06d3 --- /dev/null +++ b/runtime/include/fiber_manager.h @@ -0,0 +1,126 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#endif + +#include "ppc_runtime.h" + +// Forward declarations +struct CpuContext; + +// GuestFiberManager: each guest OSThread maps to a Windows Fiber. A scheduler fiber picks +// which guest fiber runs; a real timer thread queues VI retraces at the VI cadence. Guest +// threads only switch at explicit yield points (OSSleepThread, OSYieldThread, ...), matching +// Wii cooperative semantics exactly. + +namespace Fiber { + +// Thread states matching guest OS +enum class ThreadState : uint32_t { + READY = 1, + RUNNING = 2, + WAITING = 4, + MORIBUND = 8, +}; + +// Information about a guest fiber +struct GuestFiber { + void* fiber = nullptr; // Windows fiber handle + uint32_t entryPoint = 0; // Thread entry function + uint32_t entryArg = 0; // Argument to entry function + CpuContext cpuContext{}; // Saved CPU context for this fiber + ThreadState state = ThreadState::READY; + bool isSchedulerFiber = false; // True for the main scheduler fiber + bool terminated = false; // Thread has exited +}; + +class GuestFiberManager { +public: + // Initialize the fiber system - must be called from main thread + static void Initialize(); + + // Shutdown the fiber system + static void Shutdown(); + + // Check if fiber system is initialized + static bool IsInitialized(); + + // Create a fiber for a guest thread (called from OSCreateThread HLE). + // OSCreateThread's stackSize/priority are not passed: the host fiber models + // neither, only the guest SP seeded from stackBase. Returns true on success. + static bool CreateGuestFiber(uint32_t guestThreadAddr, uint32_t entryPoint, + uint32_t entryArg, uint32_t stackBase); + + // Resume a guest thread (called from OSResumeThread HLE) + // This marks the fiber as runnable + static void ResumeGuestThread(uint32_t guestThreadAddr); + + // Suspend a guest thread (called from OSSuspendThread HLE) + static void SuspendGuestThread(uint32_t guestThreadAddr); + + // Called when a guest thread exits or is canceled + static void ExitGuestThread(uint32_t guestThreadAddr, ThreadState finalState); + + // Switch to a specific guest thread's fiber (called from OSLoadContext) + // This saves the current fiber's state and switches to the target + static void SwitchToThread(uint32_t guestThreadAddr, CpuContext* cpu); + + // Get the currently running guest thread address (0 if in scheduler) + static uint32_t GetCurrentGuestThread(); + + // Get fiber info for a guest thread (may be null) + static GuestFiber* GetFiber(uint32_t guestThreadAddr); + + // Check for VI retrace and process it (called from scheduler idle) + static void ProcessTimerEvents(CpuContext* cpu); + + // Register the current host fiber as a guest thread fiber + // This is used for the default/main thread that exists before OSCreateThread + static bool RegisterMainThreadAsFiber(uint32_t guestThreadAddr, CpuContext* cpu); + + // Check if a fiber exists for a guest thread + static bool HasFiber(uint32_t guestThreadAddr); + static bool IsTerminated(uint32_t guestThreadAddr); + +private: + // The fiber entry point wrapper +#if defined(_WIN32) + static void CALLBACK FiberProc(void* param); +#else + static void FiberProc(void* param); +#endif + + // Internal state + static std::mutex s_mutex; + static std::unordered_map s_fibers; + static std::vector s_fibersPendingDelete; + static void* s_schedulerFiber; + static uint32_t s_currentGuestThread; + static bool s_initialized; + // Stored CPU context pointer for fiber switches + static thread_local CpuContext* s_cpuContext; + + static void PurgePendingFibers(); +}; + +// Global pending retrace count. GuestFiberManager::ProcessTimerEvents drains +// this on the guest thread so VI callbacks still run with guest state/locking +// expectations. +extern std::atomic g_viRetracePendingCount; + +} // namespace Fiber + diff --git a/runtime/include/game_graphics_options.h b/runtime/include/game_graphics_options.h new file mode 100644 index 0000000..ce700c4 --- /dev/null +++ b/runtime/include/game_graphics_options.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +namespace RuntimeGameGraphicsOptions { + +inline std::atomic& DisabledPostProcessingPathsState() noexcept { + static std::atomic disabledMask{0}; + return disabledMask; +} + +inline uint32_t DisabledPostProcessingPaths() noexcept { + return DisabledPostProcessingPathsState().load(std::memory_order_relaxed); +} + +inline void SetDisabledPostProcessingPaths(uint32_t disabledMask) noexcept { + DisabledPostProcessingPathsState().store(disabledMask, std::memory_order_relaxed); +} + +inline uint32_t FilterScnRendererPathMask(uint32_t pathMask) noexcept { + return pathMask & ~(DisabledPostProcessingPaths() | 0x20u); +} + +} // namespace RuntimeGameGraphicsOptions diff --git a/runtime/include/guest_flat_memory.h b/runtime/include/guest_flat_memory.h new file mode 100644 index 0000000..caa71c2 --- /dev/null +++ b/runtime/include/guest_flat_memory.h @@ -0,0 +1,80 @@ +#pragma once + +// Flat 4 GiB guest address space: a guest access is just `*(T*)(kFlatGuestBase + addr)` plus a +// byte swap, no page-table load/branch. Two views alias the same physical memory: the GUEST +// view at kFlatGuestBase has page protections as the interception mechanism (unmapped/MMIO/ +// executable/deferred-read pages are uncommitted or protected), while the HOST view is a plain +// alias native runtime code (image loading, DVD reads, HLE, GX) writes through unchecked. +#include +#include +#include + +namespace GuestFlat { + +// Fixed base so the emitted access is `[reg + imm64-in-register]` with no load +// of a global. 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the +// usual image/heap placement. +inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull; +inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'1000'0000'0000ull; + +#define MKW_FLAT_GUEST_BASE (reinterpret_cast(GuestFlat::kFixedFlatGuestBase)) + +enum class Backing { + Owned, + Mem1, + Mem2, +}; + +struct RegionRequest { + uint32_t base = 0; + uint64_t size = 0; + Backing backing = Backing::Owned; +}; + +struct FaultCounters { + uint32_t mmio = 0; // MMIO access that reached the handler + uint32_t efb = 0; // deferred (EFB) read materialized from a trap + uint32_t xguard = 0; // executable-page write trap + uint32_t unmapped = 0; // guest touches that landed outside every mapped region + uint32_t unmappedRegions = 0; // distinct 64 KiB blocks committed on demand +}; + +// True once the reservation exists and translated code may use the flat path. +bool IsActive(); + +// Reserves the 4 GiB space (once per process) and maps every requested region +// into both views. Throws std::runtime_error with a precise diagnosis when the +// reservation, the section objects or a view cannot be created - a silent +// fallback would let translated code read from an unmapped constant base. +void Initialize(const std::vector& regions); + +// Host-view pointer for a mapped guest address, or nullptr when the address is +// outside every mapped region. This is what the page table and +// Memory::GetPointer hand out. +uint8_t* HostPointer(uint32_t guestAddress); + +// Deferred (EFB) reads: the covered guest pages are made PAGE_NOACCESS in the +// guest view so a flat read traps and materializes the copy. +void ProtectDeferredRange(uint32_t address, size_t length); +void UnprotectDeferredRange(uint32_t address, size_t length); + +// Executable-write guard: pages fully covered by a registered executable range +// become PAGE_READONLY in the guest view. Registration order does not matter - +// ranges registered before the mapping exists are re-applied by Initialize. +void RegisterExecutableRange(uint32_t start, uint32_t end); + +FaultCounters Counters(); + +// End-of-run report for the counters above. An unmapped touch is a wild guest +// pointer whose block this module silently committed so execution could go on, +// which makes it invisible unless it is repeated at shutdown; a nonzero count +// is therefore reported as a warning. Idempotent, so every exit path (normal +// return, caught exception, abort handler) may call it. +void LogFaultSummary() noexcept; + +// Returns true when the access violation was a guest-space fault this module +// resolved; the caller must then resume execution. `exceptionPointers` is a +// Windows EXCEPTION_POINTERS*. +bool HandleAccessViolation(void* exceptionPointers) noexcept; + +} // namespace GuestFlat diff --git a/runtime/include/guest_interrupt_context.h b/runtime/include/guest_interrupt_context.h new file mode 100644 index 0000000..51e405d --- /dev/null +++ b/runtime/include/guest_interrupt_context.h @@ -0,0 +1,32 @@ +#pragma once + +#include "abi_bridge.h" +#include "ppc_runtime.h" + +// Deferred HLE dispatch (VRetrace/alarm callbacks) runs mid-function on an arbitrary translated +// caller; using its live CpuContext would let callback exit state clobber resident locals (once +// wild-read a resident r29 via postVRetrace). This models hardware save/restore: callbacks run on +// a private copy seeded from the interrupted r1/r2/r13/FP mode, made ambient via CpuContextScope. +class GuestInterruptCallbackContext { +public: + GuestInterruptCallbackContext() + : registers_(InterruptedRegisters()), scope_(®isters_) {} + + GuestInterruptCallbackContext(const GuestInterruptCallbackContext&) = delete; + GuestInterruptCallbackContext& operator=(const GuestInterruptCallbackContext&) = delete; + + CpuContext* get() noexcept { return ®isters_; } + +private: + static CpuContext InterruptedRegisters() { + // The ambient context is the interrupted guest thread's file. Without + // one (host frame loop between fibers) the persistent context is the + // only meaningful seed for r1/r2/r13. + const CpuContext* interrupted = TryGetCpuContext(); + return interrupted != nullptr ? *interrupted : GetPersistentCpuContext(); + } + + CpuContext registers_; + CpuContextScope scope_; +}; + diff --git a/runtime/include/gx_guest_write.h b/runtime/include/gx_guest_write.h new file mode 100644 index 0000000..f12c21f --- /dev/null +++ b/runtime/include/gx_guest_write.h @@ -0,0 +1,135 @@ +#pragma once + +// Per-granule write-generation counters let GX cache consumers skip re-digesting unchanged +// ranges. Assumes GPU visibility only happens via DCStoreRange/DCFlushRange/DCInvalidateRange, +// so hooking those plus DMA writers is a complete notification. Header-only: hot, must not +// link-depend on the GX HLE. + +#include "memory.h" + +#include +#include +#include +#include + +// Canonical MEM1/MEM2 physical address for any of the guest's cached, uncached +// or physical aliases. Lives here rather than in the GX HLE so the tracking +// table, its notifiers and the GX caches all agree on one address space. +inline uint32_t CanonicalizeGxMainRamAddress(uint32_t addr) noexcept { + if (addr < 0x01800000u) { + return addr; + } + if (addr >= Memory::kMem2PhysicalBase && addr < Memory::kMem2PhysicalEnd) { + return addr; + } + if (addr >= 0x80000000u && addr < 0x81800000u) { + return addr - 0x80000000u; + } + if (addr >= Memory::kMem2CachedBase && addr < Memory::kMem2CachedEnd) { + return addr - 0x80000000u; + } + if (addr >= 0xC0000000u && addr < 0xC1800000u) { + return addr - 0xC0000000u; + } + if (addr >= Memory::kMem2UncachedBase && addr < Memory::kMem2UncachedEnd) { + return addr - 0xC0000000u; + } + return addr; +} + +namespace GxGuestWrite { + +// Granularity matches the GX resource caches' occupancy maps: 64 KiB, so a +// cacheable display list or texture spans only a handful of counters. A false +// bump only costs one re-digest, which is exactly the untracked behaviour. +inline constexpr uint32_t kGranuleShift = 16; // 64 KiB per granule +inline constexpr uint64_t kTrackedSpan = static_cast(Memory::kMem2PhysicalEnd); +inline constexpr size_t kGranuleCount = static_cast(kTrackedSpan >> kGranuleShift); + +// Folded value used for "this range is not covered by the granule map", which +// forces the consumer to recompute its digest on every call. +inline constexpr uint64_t kUntracked = ~0ull; + +// Shared (not thread_local) because notifications come from the OS HLE and from +// aurora's readback callbacks while the caches themselves are per guest thread; +// relaxed ordering is enough, every access is a plain load or a lock-xadd on +// x86 and a missed-by-a-hair ordering only delays a re-digest by one call. +inline std::array, kGranuleCount> g_generations{}; + +// Monotone fold of every granule counter covering [addr, addr + nbytes). +// Counters only ever increase, so the sum changes whenever any covered granule +// is bumped and can never alias back to a previously observed value. +inline uint64_t GenerationForRange(uint32_t addr, uint32_t nbytes) noexcept { + if (nbytes == 0) { + return kUntracked; + } + const uint64_t start = static_cast(CanonicalizeGxMainRamAddress(addr)); + const uint64_t end = start + static_cast(nbytes); + if (end <= start || end > kTrackedSpan) { + return kUntracked; + } + const size_t firstGranule = static_cast(start >> kGranuleShift); + const size_t lastGranule = static_cast((end - 1) >> kGranuleShift); + uint64_t folded = 0; + for (size_t granule = firstGranule; granule <= lastGranule; ++granule) { + folded += g_generations[granule].load(std::memory_order_relaxed); + } + // Never collide with the "untracked" sentinel. + return folded == kUntracked ? folded - 1u : folded; +} + +// Bumps every granule covering [addr, addr + size). Deliberately branch-light: +// this runs on every DC range op, including the ones that only ever touch +// memory no GX cache has ever looked at. +inline void NotifyWrite(uint32_t addr, uint32_t size) noexcept { + if (size == 0) { + return; + } + const uint64_t start = static_cast(CanonicalizeGxMainRamAddress(addr)); + const uint64_t end = start + static_cast(size); + if (end <= start || start >= kTrackedSpan) { + return; + } + const uint64_t clampedEnd = end < kTrackedSpan ? end : kTrackedSpan; + const size_t firstGranule = static_cast(start >> kGranuleShift); + const size_t lastGranule = static_cast((clampedEnd - 1) >> kGranuleShift); + for (size_t granule = firstGranule; granule <= lastGranule; ++granule) { + g_generations[granule].fetch_add(1u, std::memory_order_relaxed); + } +} + +// The skip contract shared by every consumer: a stored digest may be trusted +// only when the range is tracked at all and no notification has landed on it +// since the digest was taken. +inline bool CanSkipDigest(uint64_t storedGeneration, uint64_t currentGeneration) noexcept { + return currentGeneration != kUntracked && storedGeneration == currentGeneration; +} + +// Host-to-guest inversion for one contiguous guest RAM alias. Aurora holds host +// pointers only, so the generation and notification hooks it calls have to map +// them back before they can touch the table. +inline bool HostRangeToGuest(const void* hostBase, uint64_t hostSize, uint32_t guestBase, + const void* hostPtr, size_t size, uint32_t& outAddr) noexcept { + if (hostBase == nullptr || hostPtr == nullptr || size == 0) { + return false; + } + const uintptr_t base = reinterpret_cast(hostBase); + const uintptr_t ptr = reinterpret_cast(hostPtr); + if (ptr < base) { + return false; + } + const uint64_t offset = static_cast(ptr - base); + if (offset >= hostSize || static_cast(size) > hostSize - offset) { + return false; + } + outAddr = guestBase + static_cast(offset); + return true; +} + +// Installs the generation/notification hooks into aurora. Called once after +// aurora_initialize; until then (and if guest RAM cannot be resolved) aurora +// digests its source bytes on every validation, which is the old behaviour. +// Defined in gx_guest_write_hooks.cpp. +void InstallAuroraHooks(); + +} // namespace GxGuestWrite diff --git a/runtime/include/hle/controller_status_contract.h b/runtime/include/hle/controller_status_contract.h new file mode 100644 index 0000000..e948e10 --- /dev/null +++ b/runtime/include/hle/controller_status_contract.h @@ -0,0 +1,82 @@ +#pragma once + +#include "isa/big_endian.h" + +#include +#include +#include + +namespace PadStatusContract { + +inline constexpr std::size_t kGuestStatusSize = 0x0C; +using GuestStatus = std::array; + +struct Fields { + uint16_t buttons = 0; + int8_t stickX = 0; + int8_t stickY = 0; + int8_t substickX = 0; + int8_t substickY = 0; + uint8_t triggerLeft = 0; + uint8_t triggerRight = 0; + uint8_t analogA = 0; + uint8_t analogB = 0; + int8_t error = 0; +}; + +inline GuestStatus Encode(const Fields& fields) +{ + GuestStatus status{}; + BigEndian::Write16(status.data(), fields.buttons); + status[0x02] = static_cast(fields.stickX); + status[0x03] = static_cast(fields.stickY); + status[0x04] = static_cast(fields.substickX); + status[0x05] = static_cast(fields.substickY); + status[0x06] = fields.triggerLeft; + status[0x07] = fields.triggerRight; + status[0x08] = fields.analogA; + status[0x09] = fields.analogB; + status[0x0A] = static_cast(fields.error); + return status; +} + +} // namespace PadStatusContract + +namespace WpadContract { + +inline constexpr std::size_t kChannelCount = 4; +inline constexpr int32_t kStatusDisabled = 0; +inline constexpr int32_t kStatusReady = 3; +inline constexpr int32_t kErrorNoController = -1; +inline constexpr int32_t kErrorNotReady = -2; +inline constexpr int32_t kErrorBadChannel = -6; +inline constexpr int32_t kExtensionCore = 0; + +class State { +public: + void Initialize() { m_initialized = true; } + bool IsInitialized() const { return m_initialized; } + int32_t GetLibraryStatus() const { return m_initialized ? kStatusReady : kStatusDisabled; } + + int32_t GetDataFormat(uint32_t chan) const + { + if (chan >= kChannelCount) { + return kErrorBadChannel; + } + return m_initialized ? 0 : kErrorNotReady; + } + + int32_t SetDataFormat(uint32_t chan, int32_t format) const + { + (void)format; + if (chan >= kChannelCount) { + return kErrorBadChannel; + } + return m_initialized ? kErrorNoController : kErrorNotReady; + } + +private: + bool m_initialized = false; +}; + +} // namespace WpadContract diff --git a/runtime/include/hle/dvd_contract.h b/runtime/include/hle/dvd_contract.h new file mode 100644 index 0000000..f9832a8 --- /dev/null +++ b/runtime/include/hle/dvd_contract.h @@ -0,0 +1,338 @@ +#pragma once + +#include "isa/big_endian.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace DvdFstContract { + +struct RegisteredFile { + std::string hostPath; + std::string dvdPath; + uint32_t size = 0; + uint32_t discOffsetWords = 0; +}; + +struct IndexedEntry { + std::string hostPath; + std::string dvdPath; + uint32_t size = 0; + uint32_t discOffsetWords = 0; + uint32_t parentIndex = 0; + uint32_t subtreeEnd = 0; + bool isDirectory = false; +}; + +struct Image { + std::vector entries; + std::map pathToEntry; + std::vector bytes; +}; + +struct GuestPlacement { + uint32_t address = 0; + uint32_t reservedArenaHi = 0; +}; + +inline std::string CanonicalizePath(const std::string& input) { + std::string path = input; + std::replace(path.begin(), path.end(), '\\', '/'); + + std::vector components; + size_t cursor = 0; + while (cursor < path.size()) { + while (cursor < path.size() && path[cursor] == '/') { + ++cursor; + } + const size_t start = cursor; + while (cursor < path.size() && path[cursor] != '/') { + ++cursor; + } + if (start == cursor) { + continue; + } + + std::string component = path.substr(start, cursor - start); + if (component == ".") { + continue; + } + if (component == "..") { + if (!components.empty()) { + components.pop_back(); + } + continue; + } + components.push_back(std::move(component)); + } + + std::string canonical = "/"; + for (size_t i = 0; i < components.size(); ++i) { + if (i != 0) { + canonical.push_back('/'); + } + canonical += components[i]; + } + return canonical; +} + +inline std::string NormalizeLookupPath(const std::string& input) { + std::string path = CanonicalizePath(input); + std::transform(path.begin(), path.end(), path.begin(), [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + return path; +} + +namespace Detail { + +struct TreeNode { + std::string name; + std::map> children; + std::optional file; +}; + +inline std::vector Components(const std::string& canonicalPath) { + std::vector result; + size_t cursor = canonicalPath == "/" ? canonicalPath.size() : 1; + while (cursor < canonicalPath.size()) { + const size_t slash = canonicalPath.find('/', cursor); + const size_t end = slash == std::string::npos ? canonicalPath.size() : slash; + result.push_back(canonicalPath.substr(cursor, end - cursor)); + cursor = end + 1; + } + return result; +} + +inline std::string Lowercase(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + return value; +} + +inline uint32_t EmitTree(const TreeNode& directory, + uint32_t directoryIndex, + const std::string& directoryPath, + Image& image, + std::vector& names) { + for (const auto& [lookupName, childPointer] : directory.children) { + (void)lookupName; + const TreeNode& child = *childPointer; + const std::string childPath = directoryPath == "/" + ? "/" + child.name + : directoryPath + "/" + child.name; + const uint32_t index = static_cast(image.entries.size()); + + if (!child.children.empty()) { + if (child.file.has_value()) { + throw std::runtime_error("DVD FST path is both a file and a directory: " + childPath); + } + image.entries.push_back({{}, childPath, 0, 0, directoryIndex, 0, true}); + names.push_back(child.name); + image.pathToEntry.emplace(NormalizeLookupPath(childPath), static_cast(index)); + image.entries[index].subtreeEnd = EmitTree(child, index, childPath, image, names); + continue; + } + + if (!child.file.has_value()) { + throw std::runtime_error("DVD FST contains an empty implicit node: " + childPath); + } + const RegisteredFile& file = *child.file; + image.entries.push_back({file.hostPath, childPath, file.size, file.discOffsetWords, + directoryIndex, index + 1, false}); + names.push_back(child.name); + image.pathToEntry.emplace(NormalizeLookupPath(childPath), static_cast(index)); + } + return static_cast(image.entries.size()); +} + +} // namespace Detail + +inline Image BuildImage(const std::vector& registrations) { + // Overlay scanning deliberately registers later mappings last. Collapse those + // mappings before assigning FST indices so one guest path has one stable entry. + std::map filesByPath; + for (RegisteredFile file : registrations) { + file.dvdPath = CanonicalizePath(file.dvdPath); + if (file.dvdPath == "/") { + throw std::runtime_error("DVD FST cannot register the root as a file"); + } + filesByPath[NormalizeLookupPath(file.dvdPath)] = std::move(file); + } + + Detail::TreeNode root; + for (const auto& [lookupPath, file] : filesByPath) { + (void)lookupPath; + Detail::TreeNode* node = &root; + const std::vector components = Detail::Components(file.dvdPath); + for (const std::string& component : components) { + const std::string key = Detail::Lowercase(component); + auto& child = node->children[key]; + if (!child) { + child = std::make_unique(); + child->name = component; + } + node = child.get(); + } + node->name = components.back(); + node->file = file; + } + + Image image; + image.entries.push_back({{}, "/", 0, 0, 0, 0, true}); + image.pathToEntry.emplace("/", 0); + std::vector names(1); + image.entries[0].subtreeEnd = Detail::EmitTree(root, 0, "/", image, names); + + if (image.entries.size() > std::numeric_limits::max() / 12u) { + throw std::runtime_error("DVD FST contains too many entries"); + } + + const size_t entriesSize = image.entries.size() * 12u; + std::vector stringTable(1, 0); + std::vector nameOffsets(image.entries.size(), 0); + for (size_t i = 1; i < names.size(); ++i) { + if (stringTable.size() > 0x00FFFFFFu) { + throw std::runtime_error("DVD FST name table exceeds the Wii 24-bit offset limit"); + } + nameOffsets[i] = static_cast(stringTable.size()); + stringTable.insert(stringTable.end(), names[i].begin(), names[i].end()); + stringTable.push_back(0); + } + + image.bytes.assign(entriesSize + stringTable.size(), 0); + for (size_t i = 0; i < image.entries.size(); ++i) { + const IndexedEntry& entry = image.entries[i]; + const uint32_t typeAndName = (entry.isDirectory ? 0x01000000u : 0u) | nameOffsets[i]; + const uint32_t word1 = entry.isDirectory ? entry.parentIndex : entry.discOffsetWords; + const uint32_t word2 = entry.isDirectory ? entry.subtreeEnd : entry.size; + BigEndian::Write32(image.bytes.data(), i * 12u + 0u, typeAndName); + BigEndian::Write32(image.bytes.data(), i * 12u + 4u, word1); + BigEndian::Write32(image.bytes.data(), i * 12u + 8u, word2); + } + std::copy(stringTable.begin(), stringTable.end(), image.bytes.begin() + entriesSize); + return image; +} + +inline std::optional ReserveBelowArena(uint32_t arenaLo, + uint32_t arenaHi, + size_t byteCount) { + constexpr uint32_t kAlignment = 32; + if (byteCount == 0 || byteCount > std::numeric_limits::max() || arenaHi <= arenaLo) { + return std::nullopt; + } + const uint32_t size = static_cast(byteCount); + if (size > arenaHi - arenaLo) { + return std::nullopt; + } + const uint32_t unaligned = arenaHi - size; + const uint32_t address = unaligned & ~(kAlignment - 1u); + if (address < arenaLo || static_cast(address) + size > arenaHi) { + return std::nullopt; + } + return GuestPlacement{address, address}; +} + +} // namespace DvdFstContract + +namespace DvdReadContract { + +inline constexpr int32_t kInterruptTransferComplete = 1; +inline constexpr int32_t kInterruptDriveError = 2; + +struct LowReadCompletion { + int32_t returnValue; + int32_t callbackResult; +}; + +inline constexpr LowReadCompletion CompletionFor(bool succeeded) noexcept { + return succeeded ? LowReadCompletion{1, kInterruptTransferComplete} + : LowReadCompletion{0, kInterruptDriveError}; +} + +enum class HostReadFailure : uint8_t { + None, + MissingFile, + BadOffset, + ShortRead, +}; + +inline constexpr const char* Describe(HostReadFailure failure) noexcept { + switch (failure) { + case HostReadFailure::None: + return "no error"; + case HostReadFailure::MissingFile: + return "host file is missing or cannot be opened"; + case HostReadFailure::BadOffset: + return "read offset is outside the host file"; + case HostReadFailure::ShortRead: + return "host file did not contain the complete requested range"; + } + return "unknown host read error"; +} + +// Read into private storage first and publish it only after the complete host +// range has been obtained. Callers can therefore leave a guest DMA destination +// untouched for every failure, including a host file truncated after indexing. +inline bool ReadExact(const std::filesystem::path& hostPath, + uint64_t offset, + uint32_t length, + std::vector& destination, + HostReadFailure& failure) { + failure = HostReadFailure::None; + + std::ifstream file(hostPath, std::ios::binary); + if (!file.is_open()) { + failure = HostReadFailure::MissingFile; + return false; + } + + file.seekg(0, std::ios::end); + const std::streamoff fileSize = file.tellg(); + if (fileSize < 0 || + offset > static_cast(std::numeric_limits::max()) || + offset >= static_cast(fileSize)) { + failure = HostReadFailure::BadOffset; + return false; + } + + const uint64_t remaining = static_cast(fileSize) - offset; + if (static_cast(length) > remaining) { + failure = HostReadFailure::ShortRead; + return false; + } + + std::vector staged(length); + file.seekg(static_cast(offset), std::ios::beg); + if (!file) { + failure = HostReadFailure::BadOffset; + return false; + } + + if (length != 0) { + file.read(reinterpret_cast(staged.data()), + static_cast(length)); + if (file.gcount() != static_cast(length)) { + failure = HostReadFailure::ShortRead; + return false; + } + } + + destination = std::move(staged); + return true; +} + +} // namespace DvdReadContract diff --git a/runtime/include/hle/network_deferred_contract.h b/runtime/include/hle/network_deferred_contract.h new file mode 100644 index 0000000..13f7609 --- /dev/null +++ b/runtime/include/hle/network_deferred_contract.h @@ -0,0 +1,199 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace NetworkDeferredContract { + +enum class PreparationState : uint8_t { + NotApplicable, + Ready, + Error, +}; + +// A recognized operation must not be represented by an empty optional: doing +// so makes malformed input and resource failures indistinguishable from an +// unrelated ioctl and lets the caller fall through to a blocking fallback. +template class Preparation { +public: + static Preparation NotApplicable() { + return Preparation(PreparationState::NotApplicable, 0, std::nullopt); + } + + static Preparation Ready(Work work, int32_t failureResult) { + return Preparation(PreparationState::Ready, failureResult, + std::optional(std::move(work))); + } + + static Preparation Error(int32_t result) { + return Preparation(PreparationState::Error, result, std::nullopt); + } + + PreparationState State() const noexcept { return state_; } + int32_t FailureResult() const noexcept { return failureResult_; } + + Work &&TakeWork() && { return std::move(*work_); } + +private: + Preparation(PreparationState state, int32_t failureResult, + std::optional work) + : state_(state), failureResult_(failureResult), work_(std::move(work)) {} + + PreparationState state_ = PreparationState::NotApplicable; + int32_t failureResult_ = 0; + std::optional work_; +}; + +enum class StartDisposition : uint8_t { + NotApplicable, + Started, + ImmediateResult, +}; + +struct StartOutcome { + StartDisposition disposition = StartDisposition::NotApplicable; + int32_t result = 0; + uint64_t token = 0; + + static constexpr StartOutcome NotApplicable() noexcept { return {}; } + + static constexpr StartOutcome Started(uint64_t token) noexcept { + return {StartDisposition::Started, 0, token}; + } + + static constexpr StartOutcome Immediate(int32_t result) noexcept { + return {StartDisposition::ImmediateResult, result, 0}; + } +}; + +// Launcher returns a token when the operation is fully installed. Token zero +// is valid for an asynchronous route because only synchronous callers consume +// it. A launcher failure or exception becomes the operation-specific immediate +// error; it can never be reinterpreted as "not applicable". +template +StartOutcome StartPrepared(Preparation &&preparation, + Launcher &&launcher) { + const PreparationState state = preparation.State(); + const int32_t failureResult = preparation.FailureResult(); + if (state == PreparationState::NotApplicable) { + return StartOutcome::NotApplicable(); + } + if (state == PreparationState::Error) { + return StartOutcome::Immediate(failureResult); + } + + try { + const std::optional token = + std::forward(launcher)(std::move(preparation).TakeWork()); + return token ? StartOutcome::Started(*token) + : StartOutcome::Immediate(failureResult); + } catch (...) { + return StartOutcome::Immediate(failureResult); + } +} + +// Keep an untouched copy of the host-only work value until resolution and +// completion publication both succeed. Any worker-side exception is converted +// into one failure-publication attempt instead of escaping the thread entry and +// terminating the process. +template +void RunWorker(Work work, Resolver &&resolver, Publish &&publish, + PublishFailure &&publishFailure) noexcept { + try { + auto completion = std::forward(resolver)(work); + std::forward(publish)(std::move(completion)); + } catch (...) { + const std::exception_ptr error = std::current_exception(); + try { + std::forward(publishFailure)(std::move(work), error); + } catch (...) { + // There is no safe blocking fallback from a detached worker. The + // production failure publisher logs if its completion queue cannot + // accept the already-normalized failure. + } + } +} + +template +Worker *DetachOrRelease(std::unique_ptr worker, + OnDetachFailure &&onDetachFailure) noexcept { + if (!worker) { + return nullptr; + } + + try { + worker->detach(); + return nullptr; + } catch (...) { + const std::exception_ptr error = std::current_exception(); + Worker *const runningWorker = worker.release(); + try { + std::forward(onDetachFailure)(error); + } catch (...) { + // Diagnostics must not turn containment of a running worker into a + // second failure path. + } + return runningWorker; + } +} + +// IOS encodes an IPv4 sockaddr as a two-byte length/family header followed by +// sockaddr::sa_data. Advertising any larger ai_addrlen would expose bytes that +// were never copied into the guest result. +inline constexpr size_t kWiiSockAddrHeaderBytes = 2; +inline constexpr size_t kWiiSockAddrPayloadBytes = 14; +inline constexpr size_t kWiiIpv4SockAddrBytes = + kWiiSockAddrHeaderBytes + kWiiSockAddrPayloadBytes; + +inline constexpr bool CanCopyIpv4SockAddr(int nativeFamily, size_t nativeLength, + int nativeIpv4Family) noexcept { + return nativeFamily == nativeIpv4Family && + nativeLength >= kWiiIpv4SockAddrBytes; +} + +inline constexpr bool +AdvertisedSockAddrFits(uint32_t advertisedLength) noexcept { + return advertisedLength <= kWiiIpv4SockAddrBytes; +} + +} // namespace NetworkDeferredContract + +namespace NetworkConnectContract { + +// IOS presents a blocking socket to the guest while the retained host socket +// stays nonblocking. A blocking guest connect therefore waits on the guest +// OSThread, never inside WSAPoll/poll on the emulation scheduler thread. +inline constexpr int64_t kGuestBlockingTimeoutMilliseconds = 10000; + +enum class ProbeDisposition : uint8_t { + StaleSocket, + PollError, + SocketReady, + TimedOut, + Pending, +}; + +// Keep the ordering explicit: fd reuse invalidates the operation before any +// host syscall, readiness wins at the deadline, and only a zero-result probe +// may remain pending or time out. +inline constexpr ProbeDisposition ClassifyProbe( + bool socketIdentityIsCurrent, int pollResult, bool deadlineExpired) noexcept { + if (!socketIdentityIsCurrent) { + return ProbeDisposition::StaleSocket; + } + if (pollResult < 0) { + return ProbeDisposition::PollError; + } + if (pollResult > 0) { + return ProbeDisposition::SocketReady; + } + return deadlineExpired ? ProbeDisposition::TimedOut + : ProbeDisposition::Pending; +} + +} // namespace NetworkConnectContract diff --git a/runtime/include/hle/network_poll_contract.h b/runtime/include/hle/network_poll_contract.h new file mode 100644 index 0000000..143bafb --- /dev/null +++ b/runtime/include/hle/network_poll_contract.h @@ -0,0 +1,161 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +namespace NetworkPollContract { + +constexpr size_t kMaxDescriptors = 24; + +#ifdef _WIN32 +using NativeSocket = SOCKET; +using NativePollFd = WSAPOLLFD; +constexpr NativeSocket kInvalidSocket = INVALID_SOCKET; +#else +using NativeSocket = int; +using NativePollFd = pollfd; +constexpr NativeSocket kInvalidSocket = -1; +#endif + +struct CopiedDescriptor { + uint32_t wiiFd = 0; + NativeSocket nativeFd = kInvalidSocket; + uint64_t socketGeneration = 0; + short events = 0; + short revents = 0; +}; + +// A zero-timeout SO_POLL is a pure readiness probe. Running it directly on +// the emulation thread is safe because ProbeNow always passes timeout zero to +// the host API; putting the guest IOS caller to sleep until the next scheduler +// pump only adds a needless context switch to every GameSpy update tick. +inline bool RequiresSchedulerWait(int64_t timeoutMilliseconds) { + return timeoutMilliseconds != 0; +} + +inline short WiiEventsToNative(uint32_t events) { + int native = 0; + if (events & 0x0001u) native |= POLLRDNORM; + if (events & 0x0002u) native |= POLLRDBAND; + if (events & 0x0004u) native |= POLLPRI; + if (events & 0x0008u) native |= POLLWRNORM; + if (events & 0x0010u) native |= POLLWRBAND; + + // ERR/HUP/NVAL are return-only. Winsock's WSAPoll also rejects the + // priority and write-band inputs which Dolphin masks on Windows. + native &= ~(POLLERR | POLLHUP | POLLNVAL); +#ifdef _WIN32 + native &= ~(POLLPRI | POLLWRBAND); +#endif + return static_cast(native); +} + +inline uint32_t NativeEventsToWii(short events) { + uint32_t wii = 0; + if (events & POLLRDNORM) wii |= 0x0001u; + if (events & POLLRDBAND) wii |= 0x0002u; + if (events & POLLPRI) wii |= 0x0004u; + if (events & POLLWRNORM) wii |= 0x0008u; + if (events & POLLWRBAND) wii |= 0x0010u; + if (events & POLLERR) wii |= 0x0020u; + if (events & POLLHUP) wii |= 0x0040u; + if (events & POLLNVAL) wii |= 0x0080u; + return wii; +} + +class Timeout { +public: + using Clock = std::chrono::steady_clock; + using TimePoint = Clock::time_point; + + static Timeout FromMilliseconds(int64_t milliseconds, TimePoint now = Clock::now()) { + Timeout timeout; + if (milliseconds < 0) { + timeout.m_infinite = true; + timeout.m_deadline = TimePoint::max(); + return timeout; + } + + using Milliseconds = std::chrono::milliseconds; + const int64_t maximum = std::chrono::duration_cast(TimePoint::max() - now).count(); + timeout.m_deadline = milliseconds >= maximum + ? TimePoint::max() + : now + Milliseconds(milliseconds); + return timeout; + } + + bool IsExpired(TimePoint now = Clock::now()) const { + return !m_infinite && now >= m_deadline; + } + + bool ShouldRemainPending(int nativeResult, TimePoint now = Clock::now()) const { + return nativeResult == 0 && !IsExpired(now); + } + + bool IsInfinite() const { return m_infinite; } + TimePoint Deadline() const { return m_deadline; } + +private: + bool m_infinite = false; + TimePoint m_deadline{}; +}; + +// Probes only descriptors whose copied socket identity is still live. A dead identity +// (SOClose/SOCleanup/slot reuse) reports POLLNVAL and counts toward readiness like IOS/Dolphin; +// skipping it silently would return 0 forever and strand an infinite-timeout SO_POLL parked +// during socket teardown mid-WFC-connect. +template +int ProbeNow(std::vector& descriptors, IsStillValid&& isStillValid) { + if (descriptors.size() > kMaxDescriptors) { + return -1; + } + + std::array active{}; + std::array originalIndices{}; + size_t activeCount = 0; + int invalidCount = 0; + for (size_t i = 0; i < descriptors.size(); ++i) { + CopiedDescriptor& descriptor = descriptors[i]; + descriptor.revents = 0; + if (!isStillValid(descriptor)) { + descriptor.revents = POLLNVAL; + ++invalidCount; + continue; + } + active[activeCount].fd = descriptor.nativeFd; + active[activeCount].events = descriptor.events; + active[activeCount].revents = 0; + originalIndices[activeCount] = i; + ++activeCount; + } + + if (activeCount == 0) { + return invalidCount; + } + +#ifdef _WIN32 + const int result = WSAPoll(active.data(), static_cast(activeCount), 0); +#else + const int result = poll(active.data(), activeCount, 0); +#endif + if (result >= 0) { + for (size_t i = 0; i < activeCount; ++i) { + descriptors[originalIndices[i]].revents = active[i].revents; + } + return result + invalidCount; + } + return invalidCount > 0 ? invalidCount : result; +} + +} // namespace NetworkPollContract diff --git a/runtime/include/hle/riivolution_contract.h b/runtime/include/hle/riivolution_contract.h new file mode 100644 index 0000000..3839cbf --- /dev/null +++ b/runtime/include/hle/riivolution_contract.h @@ -0,0 +1,692 @@ +// Riivolution patch-XML parsing and patch selection. +// +// Ported from Dolphin Emulator's DiscIO/RiivolutionParser.h/cpp and the +// external-path resolution rules of DiscIO/RiivolutionPatcher.cpp +// (https://github.com/dolphin-emu/dolphin). +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later +// +// Deviations from Dolphin, all deliberate: +// - patches are parsed but never applied here: guest code patching +// belongs to the translator's Code.pul/lowmem pipeline, not the runtime. +// - Riivolution "macros" are not supported + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace RiivolutionContract { + +// ============================================================================ +// Minimal XML document model +// ============================================================================ + +struct XmlNode { + std::string name; + std::vector> attributes; + std::vector children; + + const std::string* FindAttribute(std::string_view attributeName) const { + for (const auto& attribute : attributes) { + if (attribute.first == attributeName) { + return &attribute.second; + } + } + return nullptr; + } + + std::string Attribute(std::string_view attributeName, std::string_view fallback = {}) const { + const std::string* value = FindAttribute(attributeName); + return value ? *value : std::string(fallback); + } + + bool AttributeBool(std::string_view attributeName, bool fallback) const { + const std::string* value = FindAttribute(attributeName); + if (!value) { + return fallback; + } + if (*value == "true" || *value == "1" || *value == "yes") { + return true; + } + if (*value == "false" || *value == "0" || *value == "no") { + return false; + } + return fallback; + } + + // Accepts decimal and 0x-prefixed hex, matching pugixml's number parsing + // (Riivolution XMLs write memory offsets as 0x........). + uint32_t AttributeUint(std::string_view attributeName, uint32_t fallback) const { + const std::string* value = FindAttribute(attributeName); + if (!value || value->empty()) { + return fallback; + } + const std::string& text = *value; + size_t index = 0; + uint32_t base = 10; + if (text.size() > 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) { + base = 16; + index = 2; + } + uint64_t result = 0; + for (; index < text.size(); ++index) { + const char c = text[index]; + uint32_t digit; + if (c >= '0' && c <= '9') { + digit = static_cast(c - '0'); + } else if (base == 16 && c >= 'a' && c <= 'f') { + digit = static_cast(c - 'a' + 10); + } else if (base == 16 && c >= 'A' && c <= 'F') { + digit = static_cast(c - 'A' + 10); + } else { + return fallback; + } + result = result * base + digit; + if (result > 0xffffffffull) { + return fallback; + } + } + return index > (base == 16 ? 2u : 0u) ? static_cast(result) : fallback; + } + + int AttributeInt(std::string_view attributeName, int fallback) const { + const std::string* value = FindAttribute(attributeName); + if (!value || value->empty()) { + return fallback; + } + const bool negative = (*value)[0] == '-'; + const uint32_t magnitude = + AttributeUintFromText(negative ? value->substr(1) : *value, 0x80000000u); + if (magnitude == 0x80000000u && !negative) { + return fallback; + } + return negative ? -static_cast(magnitude) : static_cast(magnitude); + } + +private: + static uint32_t AttributeUintFromText(const std::string& text, uint32_t fallback) { + XmlNode probe; + probe.attributes.push_back({"v", text}); + return probe.AttributeUint("v", fallback); + } +}; + +namespace XmlDetail { + +inline bool StartsWith(std::string_view text, std::string_view prefix) { + return text.size() >= prefix.size() && text.compare(0, prefix.size(), prefix) == 0; +} + +} // namespace XmlDetail + +// Converts only element and attribute data from pugixml. Riivolution carries +// its data in attributes, so text, declarations, comments, and CDATA do not +// need to become part of the contract's data model. +inline XmlNode CopyXmlNode(const pugi::xml_node& source) { + XmlNode destination; + destination.name = source.name(); + for (const pugi::xml_attribute& attribute : source.attributes()) { + destination.attributes.emplace_back(attribute.name(), attribute.value()); + } + for (const pugi::xml_node& child : source.children()) { + if (child.type() == pugi::node_element) { + destination.children.push_back(CopyXmlNode(child)); + } + } + return destination; +} + +// Parses a document with pugixml and returns its root element, or nullopt when +// malformed. load_buffer accepts the UTF-8 BOM used by some Riivolution packs. +inline std::optional ParseXml(std::string_view text) { + pugi::xml_document document; + const pugi::xml_parse_result result = document.load_buffer( + text.data(), text.size(), pugi::parse_default, pugi::encoding_utf8); + if (!result) { + return std::nullopt; + } + + const pugi::xml_node root = document.document_element(); + if (!root) { + return std::nullopt; + } + return CopyXmlNode(root); +} + +// ============================================================================ +// Riivolution data model (mirrors Dolphin's DiscIO::Riivolution) +// ============================================================================ + +struct GameFilter { + std::optional game; + std::optional developer; + std::optional disc; + std::optional version; + std::optional> regions; +}; + +struct PatchReference { + std::string id; + std::map params; +}; + +struct Choice { + std::string name; + std::vector patchReferences; +}; + +struct Option { + std::string name; + std::string id; + std::vector choices; + + // 1-based index into choices; 0 means disabled. + uint32_t selectedChoice = 0; +}; + +struct Section { + std::string name; + std::vector