diff --git a/.github/reviewers.yml b/.github/reviewers.yml
index f50755cca..052594f23 100644
--- a/.github/reviewers.yml
+++ b/.github/reviewers.yml
@@ -29,4 +29,4 @@ infra:
   - TSRBerry
 
 default:
-  - @developers
+  - '@developers'
diff --git a/.github/update_reviewers.py b/.github/update_reviewers.py
deleted file mode 100644
index dd1b1e566..000000000
--- a/.github/update_reviewers.py
+++ /dev/null
@@ -1,87 +0,0 @@
-from pathlib import Path
-from typing import List, Set
-from github import Auth, Github
-from github.Repository import Repository
-from github.GithubException import GithubException
-
-import os
-import sys
-import yaml
-
-
-def add_reviewers(
-    reviewers: Set[str], team_reviewers: Set[str], new_entries: List[str]
-):
-    for reviewer in new_entries:
-        if reviewer.startswith("@"):
-            team_reviewers.add(reviewer[1:])
-        else:
-            reviewers.add(reviewer)
-
-
-def update_reviewers(config, repo: Repository, pr_id: int) -> int:
-    pull_request = repo.get_pull(pr_id)
-
-    if not pull_request:
-        sys.stderr.writable(f"Unknown PR #{pr_id}\n")
-        return 1
-
-    if pull_request.draft:
-        print("Not assigning reviewers for draft PRs")
-        return 0
-
-    pull_request_author = pull_request.user.login
-    reviewers = set()
-    team_reviewers = set()
-
-    for label in pull_request.labels:
-        if label.name in config:
-            add_reviewers(reviewers, team_reviewers, config[label.name])
-
-    if "default" in config:
-        add_reviewers(reviewers, team_reviewers, config["default"])
-
-    if pull_request_author in reviewers:
-        reviewers.remove(pull_request_author)
-
-    try:
-        reviewers = list(reviewers)
-        team_reviewers = list(team_reviewers)
-        print(
-            f"Attempting to assign reviewers ({reviewers}) and team_reviewers ({team_reviewers})"
-        )
-        pull_request.create_review_request(reviewers, team_reviewers)
-        return 0
-    except GithubException as e:
-        sys.stderr.write(f"Cannot assign review request for PR #{pr_id}: {e}\n")
-        return 1
-
-
-if __name__ == "__main__":
-    if len(sys.argv) != 7:
-        sys.stderr.write("usage: <app_id> <private_key_env_name> <installation_id> <repo_path> <pr_id> <config_path>\n")
-        sys.exit(1)
-
-    app_id = sys.argv[1]
-    private_key = os.environ[sys.argv[2]]
-    installation_id = sys.argv[3]
-    repo_path = sys.argv[4]
-    pr_id = int(sys.argv[5])
-    config_path = Path(sys.argv[6])
-
-    auth = Auth.AppAuth(app_id, private_key).get_installation_auth(installation_id)
-    g = Github(auth=auth)
-    repo = g.get_repo(repo_path)
-
-    if not repo:
-        sys.stderr.write("Repository not found!\n")
-        sys.exit(1)
-
-    if not config_path.exists():
-        sys.stderr.write(f'Config "{config_path}" not found!\n')
-        sys.exit(1)
-
-    with open(config_path, "r") as f:
-        config = yaml.safe_load(f)
-
-    sys.exit(update_reviewers(config, repo, pr_id))
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f0aa2a06a..16058d9f8 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -35,7 +35,7 @@ jobs:
 
       fail-fast: false
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
       - uses: actions/setup-dotnet@v3
         with:
@@ -108,7 +108,7 @@ jobs:
         configuration: [ Debug, Release ]
 
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
       - uses: actions/setup-dotnet@v3
         with:
@@ -135,9 +135,13 @@ jobs:
         id: git_short_hash
         run: echo "result=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
 
-      - name: Publish macOS
+      - name: Publish macOS Ryujinx.Ava
         run: |
-          ./distribution/macos/create_macos_build.sh . publish_tmp publish_ava ./distribution/macos/entitlements.xml "${{ env.RYUJINX_BASE_VERSION }}" "${{ steps.git_short_hash.outputs.result }}" "${{ matrix.configuration }}" "-p:ExtraDefineConstants=DISABLE_UPDATER"
+          ./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish_ava ./distribution/macos/entitlements.xml "${{ env.RYUJINX_BASE_VERSION }}" "${{ steps.git_short_hash.outputs.result }}" "${{ matrix.configuration }}" "-p:ExtraDefineConstants=DISABLE_UPDATER"
+
+      - name: Publish macOS Ryujinx.Headless.SDL2
+        run: |
+          ./distribution/macos/create_macos_build_headless.sh . publish_tmp_headless publish_headless ./distribution/macos/entitlements.xml "${{ env.RYUJINX_BASE_VERSION }}" "${{ steps.git_short_hash.outputs.result }}" "${{ matrix.configuration }}" "-p:ExtraDefineConstants=DISABLE_UPDATER"
 
       - name: Upload Ryujinx.Ava artifact
         uses: actions/upload-artifact@v3
@@ -145,3 +149,10 @@ jobs:
           name: ava-ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-macos_universal
           path: "publish_ava/*.tar.gz"
         if: github.event_name == 'pull_request'
+
+      - name: Upload Ryujinx.Headless.SDL2 artifact
+        uses: actions/upload-artifact@v3
+        with:
+          name: sdl2-ryujinx-headless-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-macos_universal
+          path: "publish_headless/*.tar.gz"
+        if: github.event_name == 'pull_request'
\ No newline at end of file
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index 25276b72f..2c982d506 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -23,7 +23,7 @@ jobs:
   format:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
         with:
           fetch-depth: 0
 
diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml
index d4380e05f..4c8ba3e17 100644
--- a/.github/workflows/flatpak.yml
+++ b/.github/workflows/flatpak.yml
@@ -24,7 +24,7 @@ jobs:
       RYUJINX_VERSION: "${{ inputs.ryujinx_version }}"
 
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
         with:
           path: Ryujinx
 
@@ -38,7 +38,7 @@ jobs:
         run: |
           echo "git_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
 
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
         with:
           repository: flathub/org.ryujinx.Ryujinx
           token: ${{ secrets.RYUJINX_BOT_PAT }}
diff --git a/.github/workflows/pr_triage.yml b/.github/workflows/pr_triage.yml
index 448e2c7de..e1c7b8ae8 100644
--- a/.github/workflows/pr_triage.yml
+++ b/.github/workflows/pr_triage.yml
@@ -12,14 +12,24 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      # Grab sources to get update_reviewers.py and reviewers.yml
+      # Grab sources to get latest labeler.yml
       - name: Fetch sources
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
-          # Ensure we pin the source origin as pull_request_target run under forks.
-          fetch-depth: 0
-          repository: Ryujinx/Ryujinx
-          ref: master
+           # Ensure we pin the source origin as pull_request_target run under forks.
+           fetch-depth: 0
+           repository: Ryujinx/Ryujinx
+           ref: master
+
+      - name: Checkout Ryujinx-Mako
+        uses: actions/checkout@v4
+        with:
+            repository: Ryujinx/Ryujinx-Mako
+            ref: master
+            path: '.ryujinx-mako'
+
+      - name: Setup Ryujinx-Mako
+        uses: ./.ryujinx-mako/.github/actions/setup-mako
 
       - name: Update labels based on changes
         uses: actions/labeler@v4
@@ -27,11 +37,11 @@ jobs:
           sync-labels: true
           dot: true
 
-      - run: pip3 install PyGithub
-
       - name: Assign reviewers
         run: |
-          python3 .github/update_reviewers.py ${{ secrets.MAKO_APP_ID }} "MAKO_PRIVATE_KEY" ${{ secrets.MAKO_INSTALLATION_ID }} ${{ github.repository }} ${{ github.event.pull_request.number }} .github/reviewers.yml
+            poetry -n -C .ryujinx-mako run ryujinx-mako update-reviewers ${{ github.repository }} ${{ github.event.pull_request.number }} .github/reviewers.yml
         shell: bash
         env:
+            MAKO_APP_ID: ${{ secrets.MAKO_APP_ID }}
             MAKO_PRIVATE_KEY: ${{ secrets.MAKO_PRIVATE_KEY }}
+            MAKO_INSTALLATION_ID: ${{ secrets.MAKO_INSTALLATION_ID }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5091944a1..988264a31 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -62,7 +62,7 @@ jobs:
             DOTNET_RUNTIME_IDENTIFIER: win10-x64
             RELEASE_ZIP_OS_NAME: win_x64
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
       - uses: actions/setup-dotnet@v3
         with:
@@ -150,7 +150,7 @@ jobs:
     runs-on: ubuntu-latest
     timeout-minutes: ${{ fromJSON(vars.JOB_TIMEOUT) }}
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
       - uses: actions/setup-dotnet@v3
         with:
@@ -188,15 +188,19 @@ jobs:
           sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
         shell: bash
 
-      - name: Publish macOS
+      - name: Publish macOS Ryujinx.Ava
         run: |
-          ./distribution/macos/create_macos_build.sh . publish_tmp publish_ava ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release
+          ./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish_ava ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release
+          
+      - name: Publish macOS Ryujinx.Headless.SDL2
+        run: |
+          ./distribution/macos/create_macos_build_headless.sh . publish_tmp_headless publish_headless ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release
 
       - name: Pushing new release
         uses: ncipollo/release-action@v1
         with:
           name: ${{ steps.version_info.outputs.build_version }}
-          artifacts: "publish_ava/*.tar.gz"
+          artifacts: "publish_ava/*.tar.gz, publish_headless/*.tar.gz"
           tag: ${{ steps.version_info.outputs.build_version }}
           body: "For more information about this release please check out the official [Changelog](https://github.com/Ryujinx/Ryujinx/wiki/Changelog)."
           omitBodyDuringUpdate: true
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..366eb8435
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,147 @@
+# Contribution to Ryujinx
+
+You can contribute to Ryujinx with PRs, testing of PRs and issues. Contributing code and other implementations is greatly appreciated alongside simply filing issues for problems you encounter.
+Please read the entire document before continuing as it can potentially save everyone involved a significant amount of time.
+
+# Quick Links
+
+* [Code Style Documentation](docs/coding-guidelines/coding-style.md)
+* [Pull Request Guidelines](docs/workflow/pr-guide.md)
+
+## Reporting Issues
+
+We always welcome bug reports, feature proposals and overall feedback. Here are a few tips on how you can make reporting your issue as effective as possible.
+
+### Identify Where to Report
+
+The Ryujinx codebase is distributed across multiple repositories in the [Ryujinx organization](https://github.com/Ryujinx). Depending on the feedback you might want to file the issue on a different repo. Here are a few common repos:
+
+* [Ryujinx/Ryujinx](https://github.com/Ryujinx/Ryujinx) Ryujinx core project files.
+* [Ryujinx/Ryujinx-Games-List](https://github.com/Ryujinx/Ryujinx-Games-List) Ryujinx game compatibility list.
+* [Ryujinx/Ryujinx-Website](https://github.com/Ryujinx/Ryujinx-Website) Ryujinx website source code.
+* [Ryujinx/Ryujinx-Ldn-Website](https://github.com/Ryujinx/Ryujinx-Ldn-Website) Ryujinx LDN website source code.
+
+### Finding Existing Issues
+
+Before filing a new issue, please search our [open issues](https://github.com/Ryujinx/Ryujinx/issues) to check if it already exists.
+
+If you do find an existing issue, please include your own feedback in the discussion. Do consider upvoting (👍 reaction) the original post, as this helps us prioritize popular issues in our backlog.
+
+### Writing a Good Feature Request
+
+Please review any feature requests already opened to both check it has not already been suggested, and to familiarize yourself with the format. When ready to submit a proposal, please use the [Feature Request issue template](https://github.com/Ryujinx/Ryujinx/issues/new?assignees=&labels=&projects=&template=feature_request.yml&title=%5BFeature+Request%5D).
+
+### Writing a Good Bug Report
+
+Good bug reports make it easier for maintainers to verify and root cause the underlying problem. The better a bug report, the faster the problem will be resolved. 
+Ideally, a bug report should contain the following information:
+
+* A high-level description of the problem.
+* A _minimal reproduction_, i.e. the smallest time commitment/configuration required to reproduce the wrong behavior. This can be in the form of a small homebrew application, or by providing a save file and reproduction steps for a specific game.
+* A description of the _expected behavior_, contrasted with the _actual behavior_ observed.
+* Information on the environment: OS/distro, CPU, GPU (including driver), RAM etc.
+* A Ryujinx log file of the run instance where the issue occurred. Log files can be found in `[Executable Folder]/Logs` and are named chronologically.
+* Additional information, e.g. is it a regression from previous versions? Are there any known workarounds?
+
+When ready to submit a bug report, please use the [Bug Report issue template](https://github.com/Ryujinx/Ryujinx/issues/new?assignees=&labels=bug&projects=&template=bug_report.yml&title=%5BBug%5D).
+
+## Contributing Changes
+
+Project maintainers will merge changes that both improve the project and meet our standards for code quality.
+
+The [Pull Request Guide](docs/workflow/pr-guide.md) and [License](https://github.com/Ryujinx/Ryujinx/blob/master/LICENSE.txt) docs define additional guidance.
+
+### DOs and DON'Ts
+
+Please do:
+
+* **DO** follow our [coding style](docs/coding-guidelines/coding-style.md) (C# code-specific).
+* **DO** give priority to the current style of the project or file you're changing even if it diverges from the general guidelines.
+* **DO** keep the discussions focused. When a new or related topic comes up
+  it's often better to create new issue than to side track the discussion.
+* **DO** clearly state on an issue that you are going to take on implementing it.
+* **DO** blog and tweet (or whatever) about your contributions, frequently!
+
+Please do not:
+
+* **DON'T** make PRs for style changes.
+* **DON'T** surprise us with big pull requests. Instead, file an issue and talk with us on Discord to start
+  a discussion so we can agree on a direction before you invest a large amount
+  of time.
+* **DON'T** commit code that you didn't write. If you find code that you think is a good fit to add to Ryujinx, file an issue or talk to us on Discord to start a discussion before proceeding.
+* **DON'T** submit PRs that alter licensing related files or headers. If you believe there's a problem with them, file an issue and we'll be happy to discuss it.
+
+### Suggested Workflow
+
+We use and recommend the following workflow:
+
+1. Create or find an issue for your work.
+    - You can skip this step for trivial changes.
+    - Get agreement from the team and the community that your proposed change is a good one if it is of significant size or changes core functionality.
+    - Clearly state that you are going to take on implementing it, if that's the case. You can request that the issue be assigned to you. Note: The issue filer and the implementer don't have to be the same person.
+2. Create a personal fork of the repository on GitHub (if you don't already have one).
+3. In your fork, create a branch off of main (`git checkout -b mybranch`).
+    - Branches are useful since they isolate your changes from incoming changes from upstream. They also enable you to create multiple PRs from the same fork.
+4. Make and commit your changes to your branch.
+    - [Build Instructions](https://github.com/Ryujinx/Ryujinx#building) explains how to build and test.
+    - Commit messages should be clear statements of action and intent.
+6. Build the repository with your changes.
+    - Make sure that the builds are clean.
+    - Make sure that `dotnet format` has been run and any corrections tested and committed.
+7. Create a pull request (PR) against the Ryujinx/Ryujinx repository's **main** branch.
+    - State in the description what issue or improvement your change is addressing.
+    - Check if all the Continuous Integration checks are passing. Refer to [Actions](https://github.com/Ryujinx/Ryujinx/actions) to check for outstanding errors.
+8. Wait for feedback or approval of your changes from the [core development team](https://github.com/orgs/Ryujinx/teams/developers)
+    - Details about the pull request [review procedure](docs/workflow/ci/pr-guide.md).
+9. When the team members have signed off, and all checks are green, your PR will be merged.
+    - The next official build will automatically include your change.
+    - You can delete the branch you used for making the change.
+
+### Good First Issues
+
+The team marks the most straightforward issues as [good first issues](https://github.com/Ryujinx/Ryujinx/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). This set of issues is the place to start if you are interested in contributing but new to the codebase.
+
+### Commit Messages
+
+Please format commit messages as follows (based on [A Note About Git Commit Messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)):
+
+```
+Summarize change in 50 characters or less
+
+Provide more detail after the first line. Leave one blank line below the
+summary and wrap all lines at 72 characters or less.
+
+If the change fixes an issue, leave another blank line after the final
+paragraph and indicate which issue is fixed in the specific format
+below.
+
+Fix #42
+```
+
+Also do your best to factor commits appropriately, not too large with unrelated things in the same commit, and not too small with the same small change applied N times in N different commits.
+
+### PR - CI Process
+
+The [Ryujinx continuous integration](https://github.com/Ryujinx/Ryujinx/actions) (CI) system will automatically perform the required builds and run tests (including the ones you are expected to run) for PRs. Builds and test runs must be clean or have bugs properly filed against flaky/unexpected failures that are unrelated to your change.
+
+If the CI build fails for any reason, the PR actions tab should be consulted for further information on the failure. There are a few usual suspects for such a failure:
+* `dotnet format` has not been run on the PR and has outstanding stylistic issues.
+* There is an error within the PR that fails a test or errors the compiler.
+* Random failure of the workflow can occasionally result in a CI failure. In this scenario a maintainer will manually restart the job.
+
+### PR Feedback
+
+Ryujinx team and community members will provide feedback on your change. Community feedback is highly valued. You may see the absence of team feedback if the community has already provided good review feedback.
+
+Two Ryujinx team members must review and approve every PR prior to merge. They will often reply with "LGTM, see nit". That means that the PR will be merged once the feedback is resolved. "LGTM" == "looks good to me".
+
+There are lots of thoughts and [approaches](https://github.com/antlr/antlr4-cpp/blob/master/CONTRIBUTING.md#emoji) for how to efficiently discuss changes. It is best to be clear and explicit with your feedback. Please be patient with people who might not understand the finer details about your approach to feedback.
+
+#### Copying Changes from Other Projects
+
+Ryujinx uses some implementations and frameworks from other projects. The following rules must be followed for PRs that include changes from another project:
+
+- The license of the file is [permissive](https://en.wikipedia.org/wiki/Permissive_free_software_licence).
+- The license of the file is left in-tact.
+- The contribution is correctly attributed in the [3rd party notices](https://github.com/Ryujinx/Ryujinx/blob/master/distribution/legal/THIRDPARTY.md) file in the repository, as needed.
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index fbae486c3..cde8742f9 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,25 +3,25 @@
     <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
   </PropertyGroup>
   <ItemGroup>
-    <PackageVersion Include="Avalonia" Version="11.0.3" />
-    <PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.0.3" />
-    <PackageVersion Include="Avalonia.Desktop" Version="11.0.3" />
-    <PackageVersion Include="Avalonia.Diagnostics" Version="11.0.3" />
-    <PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.0.3" />
-    <PackageVersion Include="Avalonia.Svg" Version="11.0.0" />
-    <PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0" />
+    <PackageVersion Include="Avalonia" Version="11.0.4" />
+    <PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.0.4" />
+    <PackageVersion Include="Avalonia.Desktop" Version="11.0.4" />
+    <PackageVersion Include="Avalonia.Diagnostics" Version="11.0.4" />
+    <PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.0.4" />
+    <PackageVersion Include="Avalonia.Svg" Version="11.0.0.2" />
+    <PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0.2" />
     <PackageVersion Include="CommandLineParser" Version="2.9.1" />
     <PackageVersion Include="Concentus" Version="1.1.7" />
     <PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
     <PackageVersion Include="DynamicData" Version="7.14.2" />
-    <PackageVersion Include="FluentAvaloniaUI" Version="2.0.1" />
+    <PackageVersion Include="FluentAvaloniaUI" Version="2.0.4" />
     <PackageVersion Include="GtkSharp.Dependencies" Version="1.1.1" />
     <PackageVersion Include="GtkSharp.Dependencies.osx" Version="0.0.5" />
     <PackageVersion Include="jp2masa.Avalonia.Flexbox" Version="0.3.0-beta.4" />
     <PackageVersion Include="LibHac" Version="0.18.0" />
     <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
-    <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.6.0" />
-    <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
+    <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
+    <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
     <PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
     <PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
     <PackageVersion Include="NUnit" Version="3.13.3" />
@@ -44,9 +44,9 @@
     <PackageVersion Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta11" />
     <PackageVersion Include="SPB" Version="0.0.4-build28" />
     <PackageVersion Include="System.Drawing.Common" Version="7.0.0" />
-    <PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="6.31.0" />
+    <PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="7.0.0" />
     <PackageVersion Include="System.IO.Hashing" Version="7.0.0" />
     <PackageVersion Include="System.Management" Version="7.0.2" />
     <PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" />
   </ItemGroup>
-</Project>
\ No newline at end of file
+</Project>
diff --git a/distribution/macos/create_macos_build.sh b/distribution/macos/create_macos_build_ava.sh
similarity index 100%
rename from distribution/macos/create_macos_build.sh
rename to distribution/macos/create_macos_build_ava.sh
diff --git a/distribution/macos/create_macos_build_headless.sh b/distribution/macos/create_macos_build_headless.sh
new file mode 100755
index 000000000..a439aef45
--- /dev/null
+++ b/distribution/macos/create_macos_build_headless.sh
@@ -0,0 +1,111 @@
+#!/bin/bash
+
+set -e
+
+if [ "$#" -lt 7 ]; then
+    echo "usage <BASE_DIR> <TEMP_DIRECTORY> <OUTPUT_DIRECTORY> <ENTITLEMENTS_FILE_PATH> <VERSION> <SOURCE_REVISION_ID> <CONFIGURATION> <EXTRA_ARGS>"
+    exit 1
+fi
+
+mkdir -p "$1"
+mkdir -p "$2"
+mkdir -p "$3"
+
+BASE_DIR=$(readlink -f "$1")
+TEMP_DIRECTORY=$(readlink -f "$2")
+OUTPUT_DIRECTORY=$(readlink -f "$3")
+ENTITLEMENTS_FILE_PATH=$(readlink -f "$4")
+VERSION=$5
+SOURCE_REVISION_ID=$6
+CONFIGURATION=$7
+EXTRA_ARGS=$8
+
+if [ "$VERSION" == "1.1.0" ];
+then
+  RELEASE_TAR_FILE_NAME=sdl2-ryujinx-headless-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.tar
+else
+  RELEASE_TAR_FILE_NAME=sdl2-ryujinx-headless-$VERSION-macos_universal.tar
+fi
+
+ARM64_OUTPUT="$TEMP_DIRECTORY/publish_arm64"
+X64_OUTPUT="$TEMP_DIRECTORY/publish_x64"
+UNIVERSAL_OUTPUT="$OUTPUT_DIRECTORY/publish"
+EXECUTABLE_SUB_PATH=Ryujinx.Headless.SDL2
+
+rm -rf "$TEMP_DIRECTORY"
+mkdir -p "$TEMP_DIRECTORY"
+
+DOTNET_COMMON_ARGS=(-p:DebugType=embedded -p:Version="$VERSION" -p:SourceRevisionId="$SOURCE_REVISION_ID" --self-contained true $EXTRA_ARGS)
+
+dotnet restore
+dotnet build -c "$CONFIGURATION" src/Ryujinx.Headless.SDL2
+dotnet publish -c "$CONFIGURATION" -r osx-arm64 -o "$TEMP_DIRECTORY/publish_arm64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx.Headless.SDL2
+dotnet publish -c "$CONFIGURATION" -r osx-x64 -o "$TEMP_DIRECTORY/publish_x64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx.Headless.SDL2
+
+# Get rid of the support library for ARMeilleure for x64 (that's only for arm64)
+rm -rf "$TEMP_DIRECTORY/publish_x64/libarmeilleure-jitsupport.dylib"
+
+# Get rid of libsoundio from arm64 builds as we don't have a arm64 variant
+# TODO: remove this once done
+rm -rf "$TEMP_DIRECTORY/publish_arm64/libsoundio.dylib"
+
+rm -rf "$OUTPUT_DIRECTORY"
+mkdir -p "$OUTPUT_DIRECTORY"
+
+# Let's copy one of the two different outputs and remove the executable
+cp -R "$ARM64_OUTPUT/" "$UNIVERSAL_OUTPUT"
+rm "$UNIVERSAL_OUTPUT/$EXECUTABLE_SUB_PATH"
+
+# Make it libraries universal
+python3 "$BASE_DIR/distribution/macos/construct_universal_dylib.py" "$ARM64_OUTPUT" "$X64_OUTPUT" "$UNIVERSAL_OUTPUT" "**/*.dylib"
+
+if ! [ -x "$(command -v lipo)" ];
+then
+    if ! [ -x "$(command -v llvm-lipo-14)" ];
+    then
+        LIPO=llvm-lipo
+    else
+        LIPO=llvm-lipo-14
+    fi
+else
+    LIPO=lipo
+fi
+
+# Make the executable universal
+$LIPO "$ARM64_OUTPUT/$EXECUTABLE_SUB_PATH" "$X64_OUTPUT/$EXECUTABLE_SUB_PATH" -output "$UNIVERSAL_OUTPUT/$EXECUTABLE_SUB_PATH" -create
+
+# Now sign it
+if ! [ -x "$(command -v codesign)" ];
+then
+    if ! [ -x "$(command -v rcodesign)" ];
+    then
+        echo "Cannot find rcodesign on your system, please install rcodesign."
+        exit 1
+    fi
+
+    # NOTE: Currently require https://github.com/indygreg/apple-platform-rs/pull/44 to work on other OSes.
+    # cargo install --git "https://github.com/marysaka/apple-platform-rs" --branch "fix/adhoc-app-bundle" apple-codesign --bin "rcodesign"
+    echo "Using rcodesign for ad-hoc signing"
+    for FILE in "$UNIVERSAL_OUTPUT"/*; do
+        if [[ $(file "$FILE") == *"Mach-O"* ]]; then
+            rcodesign sign --entitlements-xml-path "$ENTITLEMENTS_FILE_PATH" "$FILE"
+        fi
+    done  
+else
+    echo "Using codesign for ad-hoc signing"
+    for FILE in "$UNIVERSAL_OUTPUT"/*; do
+        if [[ $(file "$FILE") == *"Mach-O"* ]]; then
+            codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f --deep -s - "$FILE"
+        fi
+    done    
+fi
+
+echo "Creating archive"
+pushd "$OUTPUT_DIRECTORY"
+tar --exclude "publish/Ryujinx.Headless.SDL2" -cvf "$RELEASE_TAR_FILE_NAME" publish 1> /dev/null
+python3 "$BASE_DIR/distribution/misc/add_tar_exec.py" "$RELEASE_TAR_FILE_NAME" "publish/Ryujinx.Headless.SDL2" "publish/Ryujinx.Headless.SDL2"
+gzip -9 < "$RELEASE_TAR_FILE_NAME" > "$RELEASE_TAR_FILE_NAME.gz"
+rm "$RELEASE_TAR_FILE_NAME"
+popd
+
+echo "Done"
\ No newline at end of file
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 000000000..2213086f6
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,40 @@
+# Documents Index
+
+This repo includes several documents that explain both high-level and low-level concepts about Ryujinx and its functions. These are very useful for contributors, to get context that can be very difficult to acquire from just reading code.
+
+Intro to Ryujinx
+==================
+
+Ryujinx is an open-source Nintendo Switch emulator, created by gdkchan, written in C#. 
+* The CPU emulator, ARMeilleure, emulates an ARMv8 CPU and currently has support for most 64-bit ARMv8 and some of the ARMv7 (and older) instructions.
+* The GPU emulator emulates the Switch's Maxwell GPU using either the OpenGL (version 4.5 minimum), Vulkan, or Metal (via MoltenVK) APIs through a custom build of OpenTK or Silk.NET respectively.
+* Audio output is entirely supported via C# wrappers for SDL2, with OpenAL & libsoundio as fallbacks.
+
+Getting Started
+===============
+
+- [Installing the .NET SDK](https://dotnet.microsoft.com/download)
+- [Official .NET Docs](https://docs.microsoft.com/dotnet/core/)
+
+Contributing (Building, testing, benchmarking, profiling, etc.)
+===============
+
+If you want to contribute a code change to this repo, start here.
+
+- [Contributor Guide](../CONTRIBUTING.md)
+
+Coding Guidelines
+=================
+
+- [C# coding style](coding-guidelines/coding-style.md)
+- [Service Implementation Guidelines - WIP](https://gist.github.com/gdkchan/84ba88cd50efbe58d1babfaa7cd7c455)
+
+Project Docs
+=================
+
+To be added. Many project files will contain basic XML docs for key functions and classes in the meantime.
+
+Other Information
+=================
+
+- N/A
diff --git a/docs/coding-guidelines/coding-style.md b/docs/coding-guidelines/coding-style.md
new file mode 100644
index 000000000..9c84055d6
--- /dev/null
+++ b/docs/coding-guidelines/coding-style.md
@@ -0,0 +1,116 @@
+# C# Coding Style
+
+The general rule we follow is "use Visual Studio defaults". 
+Using an IDE that supports the `.editorconfig` standard will make this much simpler.
+
+1. We use [Allman style](http://en.wikipedia.org/wiki/Indent_style#Allman_style) braces, where each brace begins on a new line. A single line statement block can go without braces but the block must be properly indented on its own line and must not be nested in other statement blocks that use braces (See rule 18 for more details). One exception is that a `using` statement is permitted to be nested within another `using` statement by starting on the following line at the same indentation level, even if the nested `using` contains a controlled block.
+2. We use four spaces of indentation (no tabs).
+3. We use `_camelCase` for internal and private fields and use `readonly` where possible. Prefix internal and private instance fields with `_`, static fields with `s_` and thread static fields with `t_`. When used on static fields, `readonly` should come after `static` (e.g. `static readonly` not `readonly static`).  Public fields should be used sparingly and should use PascalCasing with no prefix when used.
+4. We avoid `this.` unless absolutely necessary.
+5. We always specify the visibility, even if it's the default (e.g.
+   `private string _foo` not `string _foo`). Visibility should be the first modifier (e.g.
+   `public abstract` not `abstract public`).
+6. Namespace imports should be specified at the top of the file, *outside* of `namespace` declarations.
+7. Avoid more than one empty line at any time. For example, do not have two
+   blank lines between members of a type.
+8. Avoid spurious free spaces.
+   For example avoid `if (someVar == 0)...`, where the dots mark the spurious free spaces.
+   Consider enabling "View White Space (Ctrl+R, Ctrl+W)" or "Edit -> Advanced -> View White Space" if using Visual Studio to aid detection.
+9. If a file happens to differ in style from these guidelines (e.g. private members are named `m_member`
+   rather than `_member`), the existing style in that file takes precedence.
+10. We only use `var` when the type is explicitly named on the right-hand side, typically due to either `new` or an explicit cast, e.g. `var stream = new FileStream(...)` not `var stream = OpenStandardInput()`.
+    - Similarly, target-typed `new()` can only be used when the type is explicitly named on the left-hand side, in a variable definition statement or a field definition statement. e.g. `FileStream stream = new(...);`, but not `stream = new(...);` (where the type was specified on a previous line).
+11. We use language keywords instead of BCL types (e.g. `int, string, float` instead of `Int32, String, Single`, etc) for both type references as well as method calls (e.g. `int.Parse` instead of `Int32.Parse`). See issue [#13976](https://github.com/dotnet/runtime/issues/13976) for examples.
+12. We use PascalCasing to name all our constant local variables and fields. The only exception is for interop code where the constant value should exactly match the name and value of the code you are calling via interop.
+13. We use PascalCasing for all method names, including local functions.
+14. We use ```nameof(...)``` instead of ```"..."``` whenever possible and relevant.
+15. Fields should be specified at the top within type declarations.
+16. When including non-ASCII characters in the source code use Unicode escape sequences (\uXXXX) instead of literal characters. Literal non-ASCII characters occasionally get garbled by a tool or editor.
+17. When using labels (for goto), indent the label one less than the current indentation.
+18. When using a single-statement if, we follow these conventions:
+    - Never use single-line form (for example: `if (source == null) throw new ArgumentNullException("source");`)
+    - Using braces is always accepted, and required if any block of an `if`/`else if`/.../`else` compound statement uses braces or if a single statement body spans multiple lines.
+    - Braces may be omitted only if the body of *every* block associated with an `if`/`else if`/.../`else` compound statement is placed on a single line.
+19. Make all internal and private types static or sealed unless derivation from them is required.  As with any implementation detail, they can be changed if/when derivation is required in the future.
+20. XML docs should be used when writing interfaces or when a class/method is deemed sufficient in scope or complexity.
+21. So-called [Magic Numbers](https://en.wikipedia.org/wiki/Magic_number_(programming)) should be defined as named constants before use (for example `for (int i = 56; i < 68; i++)` could read `for (int i = _currentAge; i < _retireAge; i++)`). 
+    This may be ignored for trivial or syntactically common statements.
+
+An [EditorConfig](https://editorconfig.org "EditorConfig homepage") file (`.editorconfig`) has been provided at the root of the runtime repository, enabling C# auto-formatting conforming to the above guidelines.
+
+### Example File:
+
+``ShaderCache.cs:``
+
+```C#
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Logging;
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.Gpu.Engine.Threed;
+using Ryujinx.Graphics.Gpu.Engine.Types;
+using Ryujinx.Graphics.Gpu.Image;
+using Ryujinx.Graphics.Gpu.Memory;
+using Ryujinx.Graphics.Gpu.Shader.DiskCache;
+using Ryujinx.Graphics.Shader;
+using Ryujinx.Graphics.Shader.Translation;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+
+namespace Ryujinx.Graphics.Gpu.Shader
+{
+    /// <summary>
+    /// Memory cache of shader code.
+    /// </summary>
+    class ShaderCache : IDisposable
+    {
+        /// <summary>
+        /// Default flags used on the shader translation process.
+        /// </summary>
+        public const TranslationFlags DefaultFlags = TranslationFlags.DebugMode;
+
+        private readonly struct TranslatedShader
+        {
+            public readonly CachedShaderStage Shader;
+            public readonly ShaderProgram Program;
+
+            public TranslatedShader(CachedShaderStage shader, ShaderProgram program)
+            {
+                Shader = shader;
+                Program = program;
+            }
+        }
+        ...
+
+        /// <summary>
+        /// Processes the queue of shaders that must save their binaries to the disk cache.
+        /// </summary>
+        public void ProcessShaderCacheQueue()
+        {
+            // Check to see if the binaries for previously compiled shaders are ready, and save them out.
+
+            while (_programsToSaveQueue.TryPeek(out ProgramToSave programToSave))
+            {
+                ProgramLinkStatus result = programToSave.HostProgram.CheckProgramLink(false);
+
+                if (result != ProgramLinkStatus.Incomplete)
+                {
+                    if (result == ProgramLinkStatus.Success)
+                    {
+                        _cacheWriter.AddShader(programToSave.CachedProgram, programToSave.BinaryCode ?? programToSave.HostProgram.GetBinary());
+                    }
+
+                    _programsToSaveQueue.Dequeue();
+                }
+                else
+                {
+                    break;
+                }
+            }
+        }
+    }
+}
+```
+
+For other languages, our current best guidance is consistency. When editing files, keep new code and changes consistent with the style in the files. For new files, it should conform to the style for that component. If there is a completely new component, anything that is reasonably broadly accepted is fine.
diff --git a/docs/workflow/pr-guide.md b/docs/workflow/pr-guide.md
new file mode 100644
index 000000000..cc2c5900b
--- /dev/null
+++ b/docs/workflow/pr-guide.md
@@ -0,0 +1,56 @@
+# Pull Request Guide
+
+## Contributing Rules
+
+All contributions to Ryujinx/Ryujinx repository are made via pull requests (PRs) rather than through direct commits. The pull requests are reviewed and merged by the maintainers after a review and at least two approvals from the core development team.
+
+To merge pull requests, you must have write permissions in the repository.
+
+## Quick Code Review Rules
+
+* Do not mix unrelated changes in one pull request. For example, a code style change should never be mixed with a bug fix.
+* All changes should follow the existing code style. You can read more about our code style at [docs/coding-guidelines](../coding-guidelines/coding-style.md).
+* Adding external dependencies is to be avoided unless not doing so would introduce _significant_ complexity. Any dependency addition should be justified and discussed before merge.
+* Use Draft pull requests for changes you are still working on but want early CI loop feedback. When you think your changes are ready for review, [change the status](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/changing-the-stage-of-a-pull-request) of your pull request.
+* Rebase your changes when required or directly requested. Changes should always be commited on top of the upstream branch, not the other way around.
+* If you are asked to make changes during the review process do them as a new commit.
+* Only resolve GitHub conversations with reviewers once they have been addressed with a commit, or via a mutual agreement.
+
+## Pull Request Ownership
+
+Every pull request will have automatically have labels and reviewers assigned. The label not only indicates the code segment which the change touches but also the area reviewers to be assigned.
+
+If during the code review process a merge conflict occurs, the PR author is responsible for its resolution. Help will be provided if necessary although GitHub makes this easier by allowing simple conflict resolution using the [conflict-editor](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/resolving-a-merge-conflict-on-github).
+
+## Pull Request Builds
+
+When submitting a PR to the `Ryujinx/Ryujinx` repository, various builds will run validating many areas to ensure we keep developer productivity and product quality high. These various workflows can be tracked in the [Actions](https://github.com/Ryujinx/Ryujinx/actions) tab of the repository. If the job continues to completion, the build artifacts will be uploaded and posted as a comment in the PR discussion.
+
+## Review Turnaround Times
+
+Ryujinx is a project that is maintained by volunteers on a completely free-time basis. As such we cannot guarantee any particular timeframe for pull request review and approval. Weeks to months are common for larger (>500 line) PRs but there are some additional best practises to avoid review purgatory.
+
+* Make the reviewers life easier wherever possible. Make use of descriptive commit names, code comments and XML docs where applicable.
+* If there is disagreement on feedback then always lean on the side of the development team and community over any personal opinion.
+* We're human. We miss things. We forget things. If there has been radio silence on your changes for a substantial period of time then do not hesitate to reach out directly either with something simple like "bump" on GitHub or a directly on Discord.
+
+To re-iterate, make the review as easy for us as possible, respond promptly and be comfortable to interact directly with us for anything else.
+
+## Merging Pull Requests
+
+Anyone with write access can merge a pull request manually when the following conditions have been met:
+
+* The PR has been approved by two reviewers and any other objections are addressed.
+    * You can request follow up reviews from the original reviewers if they requested changes.
+* The PR successfully builds and passes all tests in the Continuous Integration (CI) system. In case of failures, refer to the [Actions](https://github.com/Ryujinx/Ryujinx/actions) tab of your PR.
+
+Typically, PRs are merged as one commit (squash merges). It creates a simpler history than a Merge Commit. "Special circumstances" are rare, and typically mean that there are a series of cleanly separated changes that will be too hard to understand if squashed together, or for some reason we want to preserve the ability to dissect them.
+
+## Blocking Pull Request Merging
+
+If for whatever reason you would like to move your pull request back to an in-progress status to avoid merging it in the current form, you can turn the PR into a draft PR by selecting the option under the reviewers section. Alternatively, you can do that by adding [WIP] prefix to the pull request title.
+
+## Old Pull Request Policy
+
+From time to time we will review older PRs and check them for relevance. If we find the PR is inactive or no longer applies, we will close it. As the PR owner, you can simply reopen it if you feel your closed PR needs our attention.
+
diff --git a/src/ARMeilleure/Diagnostics/Symbols.cs b/src/ARMeilleure/Diagnostics/Symbols.cs
index 86469d8bb..d857c079f 100644
--- a/src/ARMeilleure/Diagnostics/Symbols.cs
+++ b/src/ARMeilleure/Diagnostics/Symbols.cs
@@ -1,6 +1,7 @@
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.Text;
 
 namespace ARMeilleure.Diagnostics
 {
@@ -33,7 +34,6 @@ namespace ARMeilleure.Diagnostics
 
         public static string Get(ulong address)
         {
-
             if (_symbols.TryGetValue(address, out string result))
             {
                 return result;
@@ -48,13 +48,15 @@ namespace ARMeilleure.Diagnostics
                         ulong diff = address - symbol.Start;
                         ulong rem = diff % symbol.ElementSize;
 
-                        result = symbol.Name + "_" + diff / symbol.ElementSize;
+                        StringBuilder resultBuilder = new();
+                        resultBuilder.Append($"{symbol.Name}_{diff / symbol.ElementSize}");
 
                         if (rem != 0)
                         {
-                            result += "+" + rem;
+                            resultBuilder.Append($"+{rem}");
                         }
 
+                        result = resultBuilder.ToString();
                         _symbols.TryAdd(address, result);
 
                         return result;
diff --git a/src/ARMeilleure/Translation/IntervalTree.cs b/src/ARMeilleure/Translation/IntervalTree.cs
index afd89b930..da29d6a68 100644
--- a/src/ARMeilleure/Translation/IntervalTree.cs
+++ b/src/ARMeilleure/Translation/IntervalTree.cs
@@ -189,7 +189,7 @@ namespace ARMeilleure.Translation
             {
                 if (start.CompareTo(node.End) < 0)
                 {
-                    if (overlaps.Length >= overlapCount)
+                    if (overlaps.Length <= overlapCount)
                     {
                         Array.Resize(ref overlaps, overlapCount + ArrayGrowthSize);
                     }
diff --git a/src/ARMeilleure/Translation/TranslatorCache.cs b/src/ARMeilleure/Translation/TranslatorCache.cs
index 11286381b..99ca58dc6 100644
--- a/src/ARMeilleure/Translation/TranslatorCache.cs
+++ b/src/ARMeilleure/Translation/TranslatorCache.cs
@@ -7,14 +7,14 @@ namespace ARMeilleure.Translation
     internal class TranslatorCache<T>
     {
         private readonly IntervalTree<ulong, T> _tree;
-        private readonly ReaderWriterLock _treeLock;
+        private readonly ReaderWriterLockSlim _treeLock;
 
         public int Count => _tree.Count;
 
         public TranslatorCache()
         {
             _tree = new IntervalTree<ulong, T>();
-            _treeLock = new ReaderWriterLock();
+            _treeLock = new ReaderWriterLockSlim();
         }
 
         public bool TryAdd(ulong address, ulong size, T value)
@@ -24,70 +24,70 @@ namespace ARMeilleure.Translation
 
         public bool AddOrUpdate(ulong address, ulong size, T value, Func<ulong, T, T> updateFactoryCallback)
         {
-            _treeLock.AcquireWriterLock(Timeout.Infinite);
+            _treeLock.EnterWriteLock();
             bool result = _tree.AddOrUpdate(address, address + size, value, updateFactoryCallback);
-            _treeLock.ReleaseWriterLock();
+            _treeLock.ExitWriteLock();
 
             return result;
         }
 
         public T GetOrAdd(ulong address, ulong size, T value)
         {
-            _treeLock.AcquireWriterLock(Timeout.Infinite);
+            _treeLock.EnterWriteLock();
             value = _tree.GetOrAdd(address, address + size, value);
-            _treeLock.ReleaseWriterLock();
+            _treeLock.ExitWriteLock();
 
             return value;
         }
 
         public bool Remove(ulong address)
         {
-            _treeLock.AcquireWriterLock(Timeout.Infinite);
+            _treeLock.EnterWriteLock();
             bool removed = _tree.Remove(address) != 0;
-            _treeLock.ReleaseWriterLock();
+            _treeLock.ExitWriteLock();
 
             return removed;
         }
 
         public void Clear()
         {
-            _treeLock.AcquireWriterLock(Timeout.Infinite);
+            _treeLock.EnterWriteLock();
             _tree.Clear();
-            _treeLock.ReleaseWriterLock();
+            _treeLock.ExitWriteLock();
         }
 
         public bool ContainsKey(ulong address)
         {
-            _treeLock.AcquireReaderLock(Timeout.Infinite);
+            _treeLock.EnterReadLock();
             bool result = _tree.ContainsKey(address);
-            _treeLock.ReleaseReaderLock();
+            _treeLock.ExitReadLock();
 
             return result;
         }
 
         public bool TryGetValue(ulong address, out T value)
         {
-            _treeLock.AcquireReaderLock(Timeout.Infinite);
+            _treeLock.EnterReadLock();
             bool result = _tree.TryGet(address, out value);
-            _treeLock.ReleaseReaderLock();
+            _treeLock.ExitReadLock();
 
             return result;
         }
 
         public int GetOverlaps(ulong address, ulong size, ref ulong[] overlaps)
         {
-            _treeLock.AcquireReaderLock(Timeout.Infinite);
+            _treeLock.EnterReadLock();
             int count = _tree.Get(address, address + size, ref overlaps);
-            _treeLock.ReleaseReaderLock();
+            _treeLock.ExitReadLock();
 
             return count;
         }
 
         public List<T> AsList()
         {
-            _treeLock.AcquireReaderLock(Timeout.Infinite);
+            _treeLock.EnterReadLock();
             List<T> list = _tree.AsList();
-            _treeLock.ReleaseReaderLock();
+            _treeLock.ExitReadLock();
 
             return list;
         }
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/AuxiliaryBufferCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/AuxiliaryBufferCommand.cs
index 7ed32800f..73d66dcf4 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/Command/AuxiliaryBufferCommand.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/AuxiliaryBufferCommand.cs
@@ -31,9 +31,18 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
 
         public bool IsEffectEnabled { get; }
 
-        public AuxiliaryBufferCommand(uint bufferOffset, byte inputBufferOffset, byte outputBufferOffset,
-                          ref AuxiliaryBufferAddresses sendBufferInfo, bool isEnabled, uint countMax,
-                          CpuAddress outputBuffer, CpuAddress inputBuffer, uint updateCount, uint writeOffset, int nodeId)
+        public AuxiliaryBufferCommand(
+            uint bufferOffset,
+            byte inputBufferOffset,
+            byte outputBufferOffset,
+            ref AuxiliaryBufferAddresses sendBufferInfo,
+            bool isEnabled,
+            uint countMax,
+            CpuAddress outputBuffer,
+            CpuAddress inputBuffer,
+            uint updateCount,
+            uint writeOffset,
+            int nodeId)
         {
             Enabled = true;
             NodeId = nodeId;
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/BiquadFilterCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/BiquadFilterCommand.cs
index f56dd70e3..ac1e581f6 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/Command/BiquadFilterCommand.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/BiquadFilterCommand.cs
@@ -21,7 +21,14 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
 
         private BiquadFilterParameter _parameter;
 
-        public BiquadFilterCommand(int baseIndex, ref BiquadFilterParameter filter, Memory<BiquadFilterState> biquadFilterStateMemory, int inputBufferOffset, int outputBufferOffset, bool needInitialization, int nodeId)
+        public BiquadFilterCommand(
+            int baseIndex,
+            ref BiquadFilterParameter filter,
+            Memory<BiquadFilterState> biquadFilterStateMemory,
+            int inputBufferOffset,
+            int outputBufferOffset,
+            bool needInitialization,
+            int nodeId)
         {
             _parameter = filter;
             BiquadFilterState = biquadFilterStateMemory;
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandList.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandList.cs
index 19a9576f7..3fe106ddf 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandList.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandList.cs
@@ -77,7 +77,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public unsafe void ClearBuffer(int index)
         {
-            Unsafe.InitBlock((void*)GetBufferPointer(index), 0, SampleCount);
+            Unsafe.InitBlock((void*)GetBufferPointer(index), 0, SampleCount * sizeof(float));
         }
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -89,7 +89,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public unsafe void CopyBuffer(int outputBufferIndex, int inputBufferIndex)
         {
-            Unsafe.CopyBlock((void*)GetBufferPointer(outputBufferIndex), (void*)GetBufferPointer(inputBufferIndex), SampleCount);
+            Unsafe.CopyBlock((void*)GetBufferPointer(outputBufferIndex), (void*)GetBufferPointer(inputBufferIndex), SampleCount * sizeof(float));
         }
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs
index 01291852e..1d5917bbe 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs
@@ -94,18 +94,18 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
 
                     float newMean = inputMovingAverage.Update(FloatingPointHelper.MeanSquare(channelInput), _parameter.InputGain);
                     float y = FloatingPointHelper.Log10(newMean) * 10.0f;
-                    float z = 0.0f;
+                    float z = 1.0f;
 
-                    bool unknown10OutOfRange = false;
+                    bool unknown10OutOfRange = y >= state.Unknown10;
 
                     if (newMean < 1.0e-10f)
                     {
-                        z = 1.0f;
+                        y = -100.0f;
 
-                        unknown10OutOfRange = state.Unknown10 < -100.0f;
+                        unknown10OutOfRange = state.Unknown10 <= -100.0f;
                     }
 
-                    if (y >= state.Unknown10 || unknown10OutOfRange)
+                    if (unknown10OutOfRange)
                     {
                         float tmpGain;
 
@@ -118,7 +118,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
                             tmpGain = (y - state.Unknown10) * ((y - state.Unknown10) * -state.CompressorGainReduction);
                         }
 
-                        z = FloatingPointHelper.DecibelToLinearExtended(tmpGain);
+                        z = FloatingPointHelper.DecibelToLinear(tmpGain);
                     }
 
                     float unknown4New = z;
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/DelayCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/DelayCommand.cs
index 003806cf7..6fa3777f4 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/Command/DelayCommand.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/DelayCommand.cs
@@ -88,7 +88,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
             float outGain = FixedPointHelper.ToFloat(Parameter.OutGain, FixedPointPrecision);
 
             Matrix2x2 delayFeedback = new(delayFeedbackBaseGain, delayFeedbackCrossGain,
-                                                    delayFeedbackCrossGain, delayFeedbackBaseGain);
+                                          delayFeedbackCrossGain, delayFeedbackBaseGain);
 
             for (int i = 0; i < sampleCount; i++)
             {
@@ -125,9 +125,9 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
             float outGain = FixedPointHelper.ToFloat(Parameter.OutGain, FixedPointPrecision);
 
             Matrix4x4 delayFeedback = new(delayFeedbackBaseGain, delayFeedbackCrossGain, delayFeedbackCrossGain, 0.0f,
-                                                    delayFeedbackCrossGain, delayFeedbackBaseGain, 0.0f, delayFeedbackCrossGain,
-                                                    delayFeedbackCrossGain, 0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain,
-                                                    0.0f, delayFeedbackCrossGain, delayFeedbackCrossGain, delayFeedbackBaseGain);
+                                          delayFeedbackCrossGain, delayFeedbackBaseGain, 0.0f, delayFeedbackCrossGain,
+                                          delayFeedbackCrossGain, 0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain,
+                                          0.0f, delayFeedbackCrossGain, delayFeedbackCrossGain, delayFeedbackBaseGain);
 
 
             for (int i = 0; i < sampleCount; i++)
@@ -172,11 +172,11 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
             float outGain = FixedPointHelper.ToFloat(Parameter.OutGain, FixedPointPrecision);
 
             Matrix6x6 delayFeedback = new(delayFeedbackBaseGain, 0.0f, delayFeedbackCrossGain, 0.0f, delayFeedbackCrossGain, 0.0f,
-                                                    0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain, 0.0f, 0.0f, delayFeedbackCrossGain,
-                                                    delayFeedbackCrossGain, delayFeedbackCrossGain, delayFeedbackBaseGain, 0.0f, 0.0f, 0.0f,
-                                                    0.0f, 0.0f, 0.0f, feedbackGain, 0.0f, 0.0f,
-                                                    delayFeedbackCrossGain, 0.0f, 0.0f, 0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain,
-                                                    0.0f, delayFeedbackCrossGain, 0.0f, 0.0f, delayFeedbackCrossGain, delayFeedbackBaseGain);
+                                          0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain, 0.0f, 0.0f, delayFeedbackCrossGain,
+                                          delayFeedbackCrossGain, delayFeedbackCrossGain, delayFeedbackBaseGain, 0.0f, 0.0f, 0.0f,
+                                          0.0f, 0.0f, 0.0f, feedbackGain, 0.0f, 0.0f,
+                                          delayFeedbackCrossGain, 0.0f, 0.0f, 0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain,
+                                          0.0f, delayFeedbackCrossGain, 0.0f, 0.0f, delayFeedbackCrossGain, delayFeedbackBaseGain);
 
             for (int i = 0; i < sampleCount; i++)
             {
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs
index 682098670..f6e1654dd 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs
@@ -28,7 +28,14 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
 
         private LimiterParameter _parameter;
 
-        public LimiterCommandVersion2(uint bufferOffset, LimiterParameter parameter, Memory<LimiterState> state, Memory<EffectResultState> resultState, bool isEnabled, ulong workBuffer, int nodeId)
+        public LimiterCommandVersion2(
+            uint bufferOffset,
+            LimiterParameter parameter,
+            Memory<LimiterState> state,
+            Memory<EffectResultState> resultState,
+            bool isEnabled,
+            ulong workBuffer,
+            int nodeId)
         {
             Enabled = true;
             NodeId = nodeId;
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/ReverbCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/ReverbCommand.cs
index f494b3028..874eb8e8b 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/Command/ReverbCommand.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/ReverbCommand.cs
@@ -79,53 +79,57 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         private void ProcessReverbMono(ref ReverbState state, ReadOnlySpan<IntPtr> outputBuffers, ReadOnlySpan<IntPtr> inputBuffers, uint sampleCount)
         {
-            ProcessReverbGeneric(ref state,
-                     outputBuffers,
-                     inputBuffers,
-                     sampleCount,
-                     _outputEarlyIndicesTableMono,
-                     _targetEarlyDelayLineIndicesTableMono,
-                     _targetOutputFeedbackIndicesTableMono,
-                     _outputIndicesTableMono);
+            ProcessReverbGeneric(
+                ref state,
+                outputBuffers,
+                inputBuffers,
+                sampleCount,
+                _outputEarlyIndicesTableMono,
+                _targetEarlyDelayLineIndicesTableMono,
+                _targetOutputFeedbackIndicesTableMono,
+                _outputIndicesTableMono);
         }
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         private void ProcessReverbStereo(ref ReverbState state, ReadOnlySpan<IntPtr> outputBuffers, ReadOnlySpan<IntPtr> inputBuffers, uint sampleCount)
         {
-            ProcessReverbGeneric(ref state,
-                     outputBuffers,
-                     inputBuffers,
-                     sampleCount,
-                     _outputEarlyIndicesTableStereo,
-                     _targetEarlyDelayLineIndicesTableStereo,
-                     _targetOutputFeedbackIndicesTableStereo,
-                     _outputIndicesTableStereo);
+            ProcessReverbGeneric(
+                ref state,
+                outputBuffers,
+                inputBuffers,
+                sampleCount,
+                _outputEarlyIndicesTableStereo,
+                _targetEarlyDelayLineIndicesTableStereo,
+                _targetOutputFeedbackIndicesTableStereo,
+                _outputIndicesTableStereo);
         }
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         private void ProcessReverbQuadraphonic(ref ReverbState state, ReadOnlySpan<IntPtr> outputBuffers, ReadOnlySpan<IntPtr> inputBuffers, uint sampleCount)
         {
-            ProcessReverbGeneric(ref state,
-                     outputBuffers,
-                     inputBuffers,
-                     sampleCount,
-                     _outputEarlyIndicesTableQuadraphonic,
-                     _targetEarlyDelayLineIndicesTableQuadraphonic,
-                     _targetOutputFeedbackIndicesTableQuadraphonic,
-                     _outputIndicesTableQuadraphonic);
+            ProcessReverbGeneric(
+                ref state,
+                outputBuffers,
+                inputBuffers,
+                sampleCount,
+                _outputEarlyIndicesTableQuadraphonic,
+                _targetEarlyDelayLineIndicesTableQuadraphonic,
+                _targetOutputFeedbackIndicesTableQuadraphonic,
+                _outputIndicesTableQuadraphonic);
         }
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         private void ProcessReverbSurround(ref ReverbState state, ReadOnlySpan<IntPtr> outputBuffers, ReadOnlySpan<IntPtr> inputBuffers, uint sampleCount)
         {
-            ProcessReverbGeneric(ref state,
-                     outputBuffers,
-                     inputBuffers,
-                     sampleCount,
-                     _outputEarlyIndicesTableSurround,
-                     _targetEarlyDelayLineIndicesTableSurround,
-                     _targetOutputFeedbackIndicesTableSurround,
-                     _outputIndicesTableSurround);
+            ProcessReverbGeneric(
+                ref state,
+                outputBuffers,
+                inputBuffers,
+                sampleCount,
+                _outputEarlyIndicesTableSurround,
+                _targetEarlyDelayLineIndicesTableSurround,
+                _targetOutputFeedbackIndicesTableSurround,
+                _outputIndicesTableSurround);
         }
 
         private unsafe void ProcessReverbGeneric(ref ReverbState state, ReadOnlySpan<IntPtr> outputBuffers, ReadOnlySpan<IntPtr> inputBuffers, uint sampleCount, ReadOnlySpan<int> outputEarlyIndicesTable, ReadOnlySpan<int> targetEarlyDelayLineIndicesTable, ReadOnlySpan<int> targetOutputFeedbackIndicesTable, ReadOnlySpan<int> outputIndicesTable)
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/FloatingPointHelper.cs b/src/Ryujinx.Audio/Renderer/Dsp/FloatingPointHelper.cs
index b231dbb6a..415e1c195 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/FloatingPointHelper.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/FloatingPointHelper.cs
@@ -52,7 +52,7 @@ namespace Ryujinx.Audio.Renderer.Dsp
         {
             // NOTE: Nintendo uses an approximation of log10, we don't.
             // As such, we support the same ranges as Nintendo to avoid unexpected behaviours.
-            return MathF.Pow(10, MathF.Max(x, 1.0e-10f));
+            return MathF.Log10(MathF.Max(x, 1.0e-10f));
         }
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -62,7 +62,8 @@ namespace Ryujinx.Audio.Renderer.Dsp
 
             foreach (float input in inputs)
             {
-                res += (input * input);
+                float normInput = input * (1f / 32768f);
+                res += normInput * normInput;
             }
 
             res /= inputs.Length;
@@ -81,19 +82,6 @@ namespace Ryujinx.Audio.Renderer.Dsp
             return MathF.Pow(10.0f, db / 20.0f);
         }
 
-        /// <summary>
-        /// Map decibel to linear in [0, 2] range.
-        /// </summary>
-        /// <param name="db">The decibel value to convert</param>
-        /// <returns>Converted linear value in [0, 2] range</returns>
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        public static float DecibelToLinearExtended(float db)
-        {
-            float tmp = MathF.Log2(DecibelToLinear(db));
-
-            return MathF.Truncate(tmp) + MathF.Pow(2.0f, tmp - MathF.Truncate(tmp));
-        }
-
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static float DegreesToRadians(float degrees)
         {
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/PcmHelper.cs b/src/Ryujinx.Audio/Renderer/Dsp/PcmHelper.cs
index d209c515b..8134e6b77 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/PcmHelper.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/PcmHelper.cs
@@ -20,6 +20,11 @@ namespace Ryujinx.Audio.Renderer.Dsp
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static int GetBufferSize<T>(int startSampleOffset, int endSampleOffset, int offset, int count) where T : unmanaged
         {
+            if (endSampleOffset < startSampleOffset)
+            {
+                return 0;
+            }
+
             return GetCountToDecode(startSampleOffset, endSampleOffset, offset, count) * Unsafe.SizeOf<T>();
         }
 
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/State/CompressorState.cs b/src/Ryujinx.Audio/Renderer/Dsp/State/CompressorState.cs
index 76aff8072..9ee573205 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/State/CompressorState.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/State/CompressorState.cs
@@ -3,7 +3,7 @@ using Ryujinx.Audio.Renderer.Parameter.Effect;
 
 namespace Ryujinx.Audio.Renderer.Dsp.State
 {
-    public class CompressorState
+    public struct CompressorState
     {
         public ExponentialMovingAverage InputMovingAverage;
         public float Unknown4;
@@ -45,7 +45,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.State
             CompressorGainReduction = (1.0f - ratio) / Constants.ChannelCountMax;
             Unknown10 = threshold - 1.5f;
             Unknown14 = threshold + 1.5f;
-            OutputGain = FloatingPointHelper.DecibelToLinearExtended(parameter.OutputGain + makeupGain);
+            OutputGain = FloatingPointHelper.DecibelToLinear(parameter.OutputGain + makeupGain);
         }
     }
 }
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/State/DelayState.cs b/src/Ryujinx.Audio/Renderer/Dsp/State/DelayState.cs
index c56fa078a..17ad2a40d 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/State/DelayState.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/State/DelayState.cs
@@ -4,7 +4,7 @@ using System.Runtime.CompilerServices;
 
 namespace Ryujinx.Audio.Renderer.Dsp.State
 {
-    public class DelayState
+    public struct DelayState
     {
         public DelayLine[] DelayLines { get; }
         public float[] LowPassZ { get; set; }
@@ -53,7 +53,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.State
             LowPassBaseGain = 1.0f - LowPassFeedbackGain;
         }
 
-        public void UpdateLowPassFilter(ref float tempRawRef, uint channelCount)
+        public readonly void UpdateLowPassFilter(ref float tempRawRef, uint channelCount)
         {
             for (int i = 0; i < channelCount; i++)
             {
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/State/LimiterState.cs b/src/Ryujinx.Audio/Renderer/Dsp/State/LimiterState.cs
index 80d1cb62e..1388bfcef 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/State/LimiterState.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/State/LimiterState.cs
@@ -4,7 +4,7 @@ using System;
 
 namespace Ryujinx.Audio.Renderer.Dsp.State
 {
-    public class LimiterState
+    public struct LimiterState
     {
         public ExponentialMovingAverage[] DetectorAverage;
         public ExponentialMovingAverage[] CompressionGainAverage;
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/State/Reverb3dState.cs b/src/Ryujinx.Audio/Renderer/Dsp/State/Reverb3dState.cs
index 5056b750e..e83e0d5fc 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/State/Reverb3dState.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/State/Reverb3dState.cs
@@ -4,7 +4,7 @@ using System;
 
 namespace Ryujinx.Audio.Renderer.Dsp.State
 {
-    public class Reverb3dState
+    public struct Reverb3dState
     {
         private readonly float[] _fdnDelayMinTimes = new float[4] { 5.0f, 6.0f, 13.0f, 14.0f };
         private readonly float[] _fdnDelayMaxTimes = new float[4] { 45.704f, 82.782f, 149.94f, 271.58f };
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/State/ReverbState.cs b/src/Ryujinx.Audio/Renderer/Dsp/State/ReverbState.cs
index 2f574f475..f1927b718 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/State/ReverbState.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/State/ReverbState.cs
@@ -5,7 +5,7 @@ using System;
 
 namespace Ryujinx.Audio.Renderer.Dsp.State
 {
-    public class ReverbState
+    public struct ReverbState
     {
         private static readonly float[] _fdnDelayTimes = new float[20]
         {
@@ -54,7 +54,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.State
             // Room
             0.70f, 0.68f, 0.70f, 0.68f, 0.70f, 0.68f, 0.70f, 0.68f, 0.68f, 0.68f,
             // Chamber
-            0.70f, 0.68f, 0.70f, 0.68f, 0.70f, 0.68f, 0.68f, 0.68f, 0.68f, 0.68f, 
+            0.70f, 0.68f, 0.70f, 0.68f, 0.70f, 0.68f, 0.68f, 0.68f, 0.68f, 0.68f,
             // Hall
             0.50f, 0.70f, 0.70f, 0.68f, 0.50f, 0.68f, 0.68f, 0.70f, 0.68f, 0.00f,
             // Cathedral
diff --git a/src/Ryujinx.Audio/Renderer/Parameter/VoiceInParameter.cs b/src/Ryujinx.Audio/Renderer/Parameter/VoiceInParameter.cs
index 86f92442b..f33d82aa0 100644
--- a/src/Ryujinx.Audio/Renderer/Parameter/VoiceInParameter.cs
+++ b/src/Ryujinx.Audio/Renderer/Parameter/VoiceInParameter.cs
@@ -264,8 +264,8 @@ namespace Ryujinx.Audio.Renderer.Parameter
             {
                 uint dataTypeSize = (uint)Unsafe.SizeOf<T>();
 
-                return StartSampleOffset * dataTypeSize <= Size &&
-                       EndSampleOffset * dataTypeSize <= Size;
+                return (ulong)StartSampleOffset * dataTypeSize <= Size &&
+                       (ulong)EndSampleOffset * dataTypeSize <= Size;
             }
 
             /// <summary>
diff --git a/src/Ryujinx.Ava/App.axaml.cs b/src/Ryujinx.Ava/App.axaml.cs
index 031e7e447..c1a3ab3e2 100644
--- a/src/Ryujinx.Ava/App.axaml.cs
+++ b/src/Ryujinx.Ava/App.axaml.cs
@@ -104,7 +104,7 @@ namespace Ryujinx.Ava
                 {
                     "Light" => ThemeVariant.Light,
                     "Dark" => ThemeVariant.Dark,
-                    _ => ThemeVariant.Default
+                    _ => ThemeVariant.Default,
                 };
 
                 if (enableCustomTheme)
diff --git a/src/Ryujinx.Ava/AppHost.cs b/src/Ryujinx.Ava/AppHost.cs
index a8388e9cd..c473cf562 100644
--- a/src/Ryujinx.Ava/AppHost.cs
+++ b/src/Ryujinx.Ava/AppHost.cs
@@ -3,7 +3,6 @@ using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Input;
-using Avalonia.Rendering;
 using Avalonia.Threading;
 using LibHac.Tools.FsSystem;
 using Ryujinx.Audio.Backends.Dummy;
@@ -21,6 +20,7 @@ using Ryujinx.Ava.UI.ViewModels;
 using Ryujinx.Ava.UI.Windows;
 using Ryujinx.Common;
 using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Configuration.Multiplayer;
 using Ryujinx.Common.Logging;
 using Ryujinx.Common.SystemInterop;
 using Ryujinx.Graphics.GAL;
@@ -191,6 +191,7 @@ namespace Ryujinx.Ava
             ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough;
 
             ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState;
+            ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState;
 
             _gpuCancellationTokenSource = new CancellationTokenSource();
             _gpuDoneEvent = new ManualResetEvent(false);
@@ -412,6 +413,11 @@ namespace Ryujinx.Ava
             Device.Configuration.MultiplayerLanInterfaceId = e.NewValue;
         }
 
+        private void UpdateMultiplayerModeState(object sender, ReactiveEventArgs<MultiplayerMode> e)
+        {
+            Device.Configuration.MultiplayerMode = e.NewValue;
+        }
+
         public void Stop()
         {
             _isActive = false;
@@ -782,7 +788,8 @@ namespace Ryujinx.Ava
                                                      ConfigurationState.Instance.Graphics.AspectRatio,
                                                      ConfigurationState.Instance.System.AudioVolume,
                                                      ConfigurationState.Instance.System.UseHypervisor,
-                                                     ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value);
+                                                     ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value,
+                                                     ConfigurationState.Instance.Multiplayer.Mode);
 
             Device = new Switch(configuration);
         }
diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json
index d547f9162..c71241f66 100644
--- a/src/Ryujinx.Ava/Assets/Locales/en_US.json
+++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json
@@ -546,7 +546,7 @@
   "SwkbdMinCharacters": "Must be at least {0} characters long",
   "SwkbdMinRangeCharacters": "Must be {0}-{1} characters long",
   "SoftwareKeyboard": "Software Keyboard",
-  "SoftwareKeyboardModeNumbersOnly": "Must be numbers only",
+  "SoftwareKeyboardModeNumeric": "Must be 0-9 or '.' only",
   "SoftwareKeyboardModeAlphabet": "Must be non CJK-characters only",
   "SoftwareKeyboardModeASCII": "Must be ASCII text only",
   "DialogControllerAppletMessagePlayerRange": "Application requests {0} player(s) with:\n\nTYPES: {1}\n\nPLAYERS: {2}\n\n{3}Please open Settings and reconfigure Input now or press Close.",
@@ -654,5 +654,8 @@
   "NetworkInterfaceDefault": "Default",
   "PackagingShaders": "Packaging Shaders",
   "AboutChangelogButton": "View Changelog on GitHub",
-  "AboutChangelogButtonTooltipMessage": "Click to open the changelog for this version in your default browser."
+  "AboutChangelogButtonTooltipMessage": "Click to open the changelog for this version in your default browser.",
+  "SettingsTabNetworkMultiplayer": "Multiplayer",
+  "MultiplayerMode": "Mode:",
+  "MultiplayerModeTooltip": "Change multiplayer mode"
 }
diff --git a/src/Ryujinx.Ava/Common/ApplicationHelper.cs b/src/Ryujinx.Ava/Common/ApplicationHelper.cs
index 9e4757538..b8cd06f3d 100644
--- a/src/Ryujinx.Ava/Common/ApplicationHelper.cs
+++ b/src/Ryujinx.Ava/Common/ApplicationHelper.cs
@@ -15,7 +15,6 @@ using LibHac.Tools.FsSystem.NcaUtils;
 using Ryujinx.Ava.Common.Locale;
 using Ryujinx.Ava.UI.Controls;
 using Ryujinx.Ava.UI.Helpers;
-using Ryujinx.Ava.UI.Windows;
 using Ryujinx.Common.Logging;
 using Ryujinx.HLE.FileSystem;
 using Ryujinx.HLE.HOS.Services.Account.Acc;
@@ -36,11 +35,9 @@ namespace Ryujinx.Ava.Common
         private static HorizonClient _horizonClient;
         private static AccountManager _accountManager;
         private static VirtualFileSystem _virtualFileSystem;
-        private static StyleableWindow _owner;
 
-        public static void Initialize(VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, StyleableWindow owner)
+        public static void Initialize(VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient)
         {
-            _owner = owner;
             _virtualFileSystem = virtualFileSystem;
             _horizonClient = horizonClient;
             _accountManager = accountManager;
@@ -148,7 +145,7 @@ namespace Ryujinx.Ava.Common
             var result = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
             {
                 Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle],
-                AllowMultiple = false
+                AllowMultiple = false,
             });
 
             if (result.Count == 0)
diff --git a/src/Ryujinx.Ava/Common/Locale/LocaleExtension.cs b/src/Ryujinx.Ava/Common/Locale/LocaleExtension.cs
index cd1c1510d..8d345ae08 100644
--- a/src/Ryujinx.Ava/Common/Locale/LocaleExtension.cs
+++ b/src/Ryujinx.Ava/Common/Locale/LocaleExtension.cs
@@ -1,6 +1,7 @@
-using Avalonia.Data;
+using Avalonia.Data.Core;
 using Avalonia.Markup.Xaml;
 using Avalonia.Markup.Xaml.MarkupExtensions;
+using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings;
 using System;
 
 namespace Ryujinx.Ava.Common.Locale
@@ -18,11 +19,20 @@ namespace Ryujinx.Ava.Common.Locale
         {
             LocaleKeys keyToUse = Key;
 
-            ReflectionBindingExtension binding = new($"[{keyToUse}]")
-            {
-                Mode = BindingMode.OneWay,
-                Source = LocaleManager.Instance,
-            };
+            var builder = new CompiledBindingPathBuilder();
+
+            builder.SetRawSource(LocaleManager.Instance)
+                .Property(new ClrPropertyInfo("Item",
+                obj => (LocaleManager.Instance[keyToUse]),
+                null,
+                typeof(string)), (weakRef, iPropInfo) =>
+                {
+                    return PropertyInfoAccessorFactory.CreateInpcPropertyAccessor(weakRef, iPropInfo);
+                });
+
+            var path = builder.Build();
+
+            var binding = new CompiledBindingExtension(path);
 
             return binding.ProvideValue(serviceProvider);
         }
diff --git a/src/Ryujinx.Ava/Input/AvaloniaKeyboardDriver.cs b/src/Ryujinx.Ava/Input/AvaloniaKeyboardDriver.cs
index 856ed6f7e..bff24a81e 100644
--- a/src/Ryujinx.Ava/Input/AvaloniaKeyboardDriver.cs
+++ b/src/Ryujinx.Ava/Input/AvaloniaKeyboardDriver.cs
@@ -1,6 +1,5 @@
 using Avalonia.Controls;
 using Avalonia.Input;
-using Avalonia.Interactivity;
 using Ryujinx.Ava.Common.Locale;
 using Ryujinx.Input;
 using System;
diff --git a/src/Ryujinx.Ava/Modules/Updater/Updater.cs b/src/Ryujinx.Ava/Modules/Updater/Updater.cs
index 5fa5241df..af7608d34 100644
--- a/src/Ryujinx.Ava/Modules/Updater/Updater.cs
+++ b/src/Ryujinx.Ava/Modules/Updater/Updater.cs
@@ -82,12 +82,9 @@ namespace Ryujinx.Modules
             {
                 Logger.Error?.Print(LogClass.Application, "Failed to convert the current Ryujinx version!");
 
-                Dispatcher.UIThread.Post(async () =>
-                {
-                    await ContentDialogHelper.CreateWarningDialog(
-                        LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedMessage],
-                        LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
-                });
+                await ContentDialogHelper.CreateWarningDialog(
+                    LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedMessage],
+                    LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
 
                 _running = false;
 
@@ -114,10 +111,9 @@ namespace Ryujinx.Modules
                         {
                             if (showVersionUpToDate)
                             {
-                                Dispatcher.UIThread.Post(async () =>
-                                {
-                                    await ContentDialogHelper.CreateUpdaterInfoDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage], "");
-                                });
+                                await ContentDialogHelper.CreateUpdaterInfoDialog(
+                                    LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
+                                    "");
                             }
 
                             _running = false;
@@ -134,10 +130,9 @@ namespace Ryujinx.Modules
                 {
                     if (showVersionUpToDate)
                     {
-                        Dispatcher.UIThread.Post(async () =>
-                        {
-                            await ContentDialogHelper.CreateUpdaterInfoDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage], "");
-                        });
+                        await ContentDialogHelper.CreateUpdaterInfoDialog(
+                            LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
+                            "");
                     }
 
                     _running = false;
@@ -149,10 +144,8 @@ namespace Ryujinx.Modules
             {
                 Logger.Error?.Print(LogClass.Application, exception.Message);
 
-                Dispatcher.UIThread.Post(async () =>
-                {
-                    await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterFailedToGetVersionMessage]);
-                });
+                await ContentDialogHelper.CreateErrorDialog(
+                    LocaleManager.Instance[LocaleKeys.DialogUpdaterFailedToGetVersionMessage]);
 
                 _running = false;
 
@@ -167,12 +160,9 @@ namespace Ryujinx.Modules
             {
                 Logger.Error?.Print(LogClass.Application, "Failed to convert the received Ryujinx version from Github!");
 
-                Dispatcher.UIThread.Post(async () =>
-                {
-                    await ContentDialogHelper.CreateWarningDialog(
-                        LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedGithubMessage],
-                        LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
-                });
+                await ContentDialogHelper.CreateWarningDialog(
+                    LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedGithubMessage],
+                    LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
 
                 _running = false;
 
@@ -183,10 +173,9 @@ namespace Ryujinx.Modules
             {
                 if (showVersionUpToDate)
                 {
-                    Dispatcher.UIThread.Post(async () =>
-                    {
-                        await ContentDialogHelper.CreateUpdaterInfoDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage], "");
-                    });
+                    await ContentDialogHelper.CreateUpdaterInfoDialog(
+                        LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
+                        "");
                 }
 
                 _running = false;
@@ -212,7 +201,7 @@ namespace Ryujinx.Modules
                 _buildSize = -1;
             }
 
-            Dispatcher.UIThread.Post(async () =>
+            await Dispatcher.UIThread.InvokeAsync(async () =>
             {
                 // Show a message asking the user if they want to update
                 var shouldUpdate = await ContentDialogHelper.CreateChoiceDialog(
@@ -222,7 +211,7 @@ namespace Ryujinx.Modules
 
                 if (shouldUpdate)
                 {
-                    UpdateRyujinx(mainWindow, _buildUrl);
+                    await UpdateRyujinx(mainWindow, _buildUrl);
                 }
                 else
                 {
@@ -241,7 +230,7 @@ namespace Ryujinx.Modules
             return result;
         }
 
-        private static async void UpdateRyujinx(Window parent, string downloadUrl)
+        private static async Task UpdateRyujinx(Window parent, string downloadUrl)
         {
             _updateSuccessful = false;
 
@@ -579,27 +568,24 @@ namespace Ryujinx.Modules
             }
         }
 
-        private static async void InstallUpdate(TaskDialog taskDialog, string updateFile)
+        private static void InstallUpdate(TaskDialog taskDialog, string updateFile)
         {
             // Extract Update
             taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterExtracting];
             taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal);
 
-            await Task.Run(() =>
+            if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
             {
-                if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
-                {
-                    ExtractTarGzipFile(taskDialog, updateFile, _updateDir);
-                }
-                else if (OperatingSystem.IsWindows())
-                {
-                    ExtractZipFile(taskDialog, updateFile, _updateDir);
-                }
-                else
-                {
-                    throw new NotSupportedException();
-                }
-            });
+                ExtractTarGzipFile(taskDialog, updateFile, _updateDir);
+            }
+            else if (OperatingSystem.IsWindows())
+            {
+                ExtractZipFile(taskDialog, updateFile, _updateDir);
+            }
+            else
+            {
+                throw new NotSupportedException();
+            }
 
             // Delete downloaded zip
             File.Delete(updateFile);
@@ -613,36 +599,33 @@ namespace Ryujinx.Modules
             if (!OperatingSystem.IsMacOS())
             {
                 // Replace old files
-                await Task.Run(() =>
+                double count = 0;
+                foreach (string file in allFiles)
                 {
-                    double count = 0;
-                    foreach (string file in allFiles)
+                    count++;
+                    try
                     {
-                        count++;
-                        try
-                        {
-                            File.Move(file, file + ".ryuold");
+                        File.Move(file, file + ".ryuold");
 
-                            Dispatcher.UIThread.Post(() =>
-                            {
-                                taskDialog.SetProgressBarState(GetPercentage(count, allFiles.Count), TaskDialogProgressState.Normal);
-                            });
-                        }
-                        catch
+                        Dispatcher.UIThread.InvokeAsync(() =>
                         {
-                            Logger.Warning?.Print(LogClass.Application, LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.UpdaterRenameFailed, file));
-                        }
+                            taskDialog.SetProgressBarState(GetPercentage(count, allFiles.Count), TaskDialogProgressState.Normal);
+                        });
                     }
-
-                    Dispatcher.UIThread.Post(() =>
+                    catch
                     {
-                        taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterAddingFiles];
-                        taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal);
-                    });
+                        Logger.Warning?.Print(LogClass.Application, LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.UpdaterRenameFailed, file));
+                    }
+                }
 
-                    MoveAllFilesOver(_updatePublishDir, _homeDir, taskDialog);
+                Dispatcher.UIThread.InvokeAsync(() =>
+                {
+                    taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterAddingFiles];
+                    taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal);
                 });
 
+                MoveAllFilesOver(_updatePublishDir, _homeDir, taskDialog);
+
                 Directory.Delete(_updateDir, true);
             }
 
@@ -658,12 +641,11 @@ namespace Ryujinx.Modules
             {
                 if (showWarnings)
                 {
-                    Dispatcher.UIThread.Post(async () =>
-                    {
-                        await ContentDialogHelper.CreateWarningDialog(
+                    Dispatcher.UIThread.InvokeAsync(() =>
+                        ContentDialogHelper.CreateWarningDialog(
                             LocaleManager.Instance[LocaleKeys.DialogUpdaterArchNotSupportedMessage],
-                            LocaleManager.Instance[LocaleKeys.DialogUpdaterArchNotSupportedSubMessage]);
-                    });
+                            LocaleManager.Instance[LocaleKeys.DialogUpdaterArchNotSupportedSubMessage])
+                    );
                 }
 
                 return false;
@@ -673,12 +655,11 @@ namespace Ryujinx.Modules
             {
                 if (showWarnings)
                 {
-                    Dispatcher.UIThread.Post(async () =>
-                    {
-                        await ContentDialogHelper.CreateWarningDialog(
+                    Dispatcher.UIThread.InvokeAsync(() =>
+                        ContentDialogHelper.CreateWarningDialog(
                             LocaleManager.Instance[LocaleKeys.DialogUpdaterNoInternetMessage],
-                            LocaleManager.Instance[LocaleKeys.DialogUpdaterNoInternetSubMessage]);
-                    });
+                            LocaleManager.Instance[LocaleKeys.DialogUpdaterNoInternetSubMessage])
+                    );
                 }
 
                 return false;
@@ -688,12 +669,11 @@ namespace Ryujinx.Modules
             {
                 if (showWarnings)
                 {
-                    Dispatcher.UIThread.Post(async () =>
-                    {
-                        await ContentDialogHelper.CreateWarningDialog(
+                    Dispatcher.UIThread.InvokeAsync(() =>
+                        ContentDialogHelper.CreateWarningDialog(
                             LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildMessage],
-                            LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage]);
-                    });
+                            LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage])
+                    );
                 }
 
                 return false;
@@ -705,21 +685,19 @@ namespace Ryujinx.Modules
             {
                 if (ReleaseInformation.IsFlatHubBuild())
                 {
-                    Dispatcher.UIThread.Post(async () =>
-                    {
-                        await ContentDialogHelper.CreateWarningDialog(
+                    Dispatcher.UIThread.InvokeAsync(() =>
+                        ContentDialogHelper.CreateWarningDialog(
                             LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle],
-                            LocaleManager.Instance[LocaleKeys.DialogUpdaterFlatpakNotSupportedMessage]);
-                    });
+                            LocaleManager.Instance[LocaleKeys.DialogUpdaterFlatpakNotSupportedMessage])
+                    );
                 }
                 else
                 {
-                    Dispatcher.UIThread.Post(async () =>
-                    {
-                        await ContentDialogHelper.CreateWarningDialog(
+                    Dispatcher.UIThread.InvokeAsync(() =>
+                        ContentDialogHelper.CreateWarningDialog(
                             LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle],
-                            LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage]);
-                    });
+                            LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage])
+                    );
                 }
             }
 
diff --git a/src/Ryujinx.Ava/UI/Applet/AvaHostUiHandler.cs b/src/Ryujinx.Ava/UI/Applet/AvaHostUiHandler.cs
index a2a944297..9fc7c6b6d 100644
--- a/src/Ryujinx.Ava/UI/Applet/AvaHostUiHandler.cs
+++ b/src/Ryujinx.Ava/UI/Applet/AvaHostUiHandler.cs
@@ -106,7 +106,7 @@ namespace Ryujinx.Ava.UI.Applet
             bool error = false;
             string inputText = args.InitialText ?? "";
 
-            Dispatcher.UIThread.Post(async () =>
+            Dispatcher.UIThread.InvokeAsync(async () =>
             {
                 try
                 {
@@ -149,7 +149,7 @@ namespace Ryujinx.Ava.UI.Applet
 
             bool showDetails = false;
 
-            Dispatcher.UIThread.Post(async () =>
+            Dispatcher.UIThread.InvokeAsync(async () =>
             {
                 try
                 {
diff --git a/src/Ryujinx.Ava/UI/Applet/ErrorAppletWindow.axaml b/src/Ryujinx.Ava/UI/Applet/ErrorAppletWindow.axaml
index a70fc8d44..6186b7d93 100644
--- a/src/Ryujinx.Ava/UI/Applet/ErrorAppletWindow.axaml
+++ b/src/Ryujinx.Ava/UI/Applet/ErrorAppletWindow.axaml
@@ -6,9 +6,11 @@
     xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
     Title="{locale:Locale ErrorWindowTitle}"
+    xmlns:views="using:Ryujinx.Ava.UI.Applet"
     Width="450"
     Height="340"
     CanResize="False"
+    x:DataType="views:ErrorAppletWindow"
     SizeToContent="Height"
     mc:Ignorable="d"
     Focusable="True">
@@ -38,7 +40,7 @@
             Grid.Column="1"
             Margin="10"
             VerticalAlignment="Stretch"
-            Text="{ReflectionBinding Message}"
+            Text="{Binding Message}"
             TextWrapping="Wrap" />
         <StackPanel
             Name="ButtonStack"
@@ -49,4 +51,4 @@
             Orientation="Horizontal"
             Spacing="10" />
     </Grid>
-</Window>
\ No newline at end of file
+</Window>
diff --git a/src/Ryujinx.Ava/UI/Applet/SwkbdAppletDialog.axaml b/src/Ryujinx.Ava/UI/Applet/SwkbdAppletDialog.axaml
index 64b23f987..7e0836065 100644
--- a/src/Ryujinx.Ava/UI/Applet/SwkbdAppletDialog.axaml
+++ b/src/Ryujinx.Ava/UI/Applet/SwkbdAppletDialog.axaml
@@ -4,7 +4,9 @@
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+    xmlns:views="using:Ryujinx.Ava.UI.Controls"
     Width="400"
+    x:DataType="views:SwkbdAppletDialog"
     mc:Ignorable="d"
     Focusable="True">
     <Grid
@@ -34,13 +36,13 @@
             Grid.Row="1"
             Grid.Column="1"
             Margin="5"
-            Text="{ReflectionBinding MainText}"
+            Text="{Binding MainText}"
             TextWrapping="Wrap" />
         <TextBlock
             Grid.Row="2"
             Grid.Column="1"
             Margin="5"
-            Text="{ReflectionBinding SecondaryText}"
+            Text="{Binding SecondaryText}"
             TextWrapping="Wrap" />
         <TextBox
             Name="Input"
@@ -50,7 +52,7 @@
             VerticalAlignment="Center"
             Focusable="True"
             KeyUp="Message_KeyUp"
-            Text="{ReflectionBinding Message}"
+            Text="{Binding Message}"
             TextInput="Message_TextInput"
             TextWrapping="Wrap"
             UseFloatingWatermark="True" />
diff --git a/src/Ryujinx.Ava/UI/Applet/SwkbdAppletDialog.axaml.cs b/src/Ryujinx.Ava/UI/Applet/SwkbdAppletDialog.axaml.cs
index 210fc9a2b..5a4cd855f 100644
--- a/src/Ryujinx.Ava/UI/Applet/SwkbdAppletDialog.axaml.cs
+++ b/src/Ryujinx.Ava/UI/Applet/SwkbdAppletDialog.axaml.cs
@@ -136,10 +136,10 @@ namespace Ryujinx.Ava.UI.Controls
             string localeText;
             switch (mode)
             {
-                case KeyboardMode.NumbersOnly:
-                    localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeNumbersOnly);
+                case KeyboardMode.Numeric:
+                    localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeNumeric);
                     validationInfoText = string.IsNullOrEmpty(validationInfoText) ? localeText : string.Join("\n", validationInfoText, localeText);
-                    _checkInput = text => text.All(char.IsDigit);
+                    _checkInput = text => text.All(NumericCharacterValidation.IsNumeric);
                     break;
                 case KeyboardMode.Alphabet:
                     localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeAlphabet);
diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs
index f54e9df8f..d75572e65 100644
--- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs
+++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs
@@ -54,7 +54,7 @@ namespace Ryujinx.Ava.UI.Controls
         {
             if (sender is MenuItem { DataContext: MainWindowViewModel viewModel })
             {
-                OpenSaveDirectory(viewModel, SaveDataType.Account, userId: new UserId((ulong)viewModel.AccountManager.LastOpenedUser.UserId.High, (ulong)viewModel.AccountManager.LastOpenedUser.UserId.Low));
+                OpenSaveDirectory(viewModel, SaveDataType.Account, new UserId((ulong)viewModel.AccountManager.LastOpenedUser.UserId.High, (ulong)viewModel.AccountManager.LastOpenedUser.UserId.Low));
             }
         }
 
@@ -62,14 +62,14 @@ namespace Ryujinx.Ava.UI.Controls
         {
             var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
 
-            OpenSaveDirectory(viewModel, SaveDataType.Device, userId: default);
+            OpenSaveDirectory(viewModel, SaveDataType.Device, default);
         }
 
         public void OpenBcatSaveDirectory_Click(object sender, RoutedEventArgs args)
         {
             var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
 
-            OpenSaveDirectory(viewModel, SaveDataType.Bcat, userId: default);
+            OpenSaveDirectory(viewModel, SaveDataType.Bcat, default);
         }
 
         private static void OpenSaveDirectory(MainWindowViewModel viewModel, SaveDataType saveDataType, UserId userId)
@@ -158,11 +158,12 @@ namespace Ryujinx.Ava.UI.Controls
 
             if (viewModel?.SelectedApplication != null)
             {
-                UserResult result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DialogWarning],
-                                                                                       LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.TitleName),
-                                                                                       LocaleManager.Instance[LocaleKeys.InputDialogYes],
-                                                                                       LocaleManager.Instance[LocaleKeys.InputDialogNo],
-                                                                                       LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
+                UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
+                    LocaleManager.Instance[LocaleKeys.DialogWarning],
+                    LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.TitleName),
+                    LocaleManager.Instance[LocaleKeys.InputDialogYes],
+                    LocaleManager.Instance[LocaleKeys.InputDialogNo],
+                    LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
 
                 if (result == UserResult.Yes)
                 {
@@ -205,11 +206,12 @@ namespace Ryujinx.Ava.UI.Controls
 
             if (viewModel?.SelectedApplication != null)
             {
-                UserResult result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DialogWarning],
-                                                                                       LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.TitleName),
-                                                                                       LocaleManager.Instance[LocaleKeys.InputDialogYes],
-                                                                                       LocaleManager.Instance[LocaleKeys.InputDialogNo],
-                                                                                       LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
+                UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
+                    LocaleManager.Instance[LocaleKeys.DialogWarning],
+                    LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.TitleName),
+                    LocaleManager.Instance[LocaleKeys.InputDialogYes],
+                    LocaleManager.Instance[LocaleKeys.InputDialogNo],
+                    LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
 
                 if (result == UserResult.Yes)
                 {
@@ -335,13 +337,13 @@ namespace Ryujinx.Ava.UI.Controls
             }
         }
 
-        public void RunApplication_Click(object sender, RoutedEventArgs args)
+        public async void RunApplication_Click(object sender, RoutedEventArgs args)
         {
             var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
 
             if (viewModel?.SelectedApplication != null)
             {
-                viewModel.LoadApplication(viewModel.SelectedApplication.Path);
+                await viewModel.LoadApplication(viewModel.SelectedApplication.Path);
             }
         }
     }
diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml
index 214fc0755..bbdb4c4a7 100644
--- a/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml
+++ b/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml
@@ -46,7 +46,7 @@
                     <Setter Property="CornerRadius" Value="4" />
                 </Style>
                 <Style Selector="ListBoxItem:selected /template/ Rectangle#SelectionIndicator">
-                    <Setter Property="MinHeight" Value="{ReflectionBinding $parent[UserControl].DataContext.GridItemSelectorSize}" />
+                    <Setter Property="MinHeight" Value="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).GridItemSelectorSize}" />
                 </Style>
             </ListBox.Styles>
             <ListBox.ItemTemplate>
@@ -56,10 +56,10 @@
                             Margin="10"
                             HorizontalAlignment="Stretch"
                             VerticalAlignment="Stretch"
-                            Classes.huge="{ReflectionBinding $parent[UserControl].DataContext.IsGridHuge}"
-                            Classes.large="{ReflectionBinding $parent[UserControl].DataContext.IsGridLarge}"
-                            Classes.normal="{ReflectionBinding $parent[UserControl].DataContext.IsGridMedium}"
-                            Classes.small="{ReflectionBinding $parent[UserControl].DataContext.IsGridSmall}"
+                            Classes.huge="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridHuge}"
+                            Classes.large="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridLarge}"
+                            Classes.normal="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridMedium}"
+                            Classes.small="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridSmall}"
                             ClipToBounds="True"
                             CornerRadius="4">
                             <Grid>
@@ -78,7 +78,7 @@
                                     Margin="0,10,0,0"
                                     HorizontalAlignment="Stretch"
                                     VerticalAlignment="Stretch"
-                                    IsVisible="{ReflectionBinding $parent[UserControl].DataContext.ShowNames}">
+                                    IsVisible="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).ShowNames}">
                                     <TextBlock
                                         HorizontalAlignment="Center"
                                         VerticalAlignment="Center"
@@ -101,4 +101,4 @@
             </ListBox.ItemTemplate>
         </ListBox>
     </Grid>
-</UserControl>
\ No newline at end of file
+</UserControl>
diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml
index 75bbf9d0d..09011005b 100644
--- a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml
+++ b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml
@@ -42,7 +42,7 @@
             </ListBox.ItemsPanel>
             <ListBox.Styles>
                 <Style Selector="ListBoxItem:selected /template/ Rectangle#SelectionIndicator">
-                    <Setter Property="MinHeight" Value="{ReflectionBinding $parent[UserControl].DataContext.ListItemSelectorSize}" />
+                    <Setter Property="MinHeight" Value="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).ListItemSelectorSize}" />
                 </Style>
             </ListBox.Styles>
             <ListBox.ItemTemplate>
@@ -67,10 +67,10 @@
                                     Grid.RowSpan="3"
                                     Grid.Column="0"
                                     Margin="0"
-                                    Classes.huge="{ReflectionBinding $parent[UserControl].DataContext.IsGridHuge}"
-                                    Classes.large="{ReflectionBinding $parent[UserControl].DataContext.IsGridLarge}"
-                                    Classes.normal="{ReflectionBinding $parent[UserControl].DataContext.IsGridMedium}"
-                                    Classes.small="{ReflectionBinding $parent[UserControl].DataContext.IsGridSmall}"
+                                    Classes.huge="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridHuge}"
+                                    Classes.large="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridLarge}"
+                                    Classes.normal="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridMedium}"
+                                    Classes.small="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridSmall}"
                                     Source="{Binding Icon, Converter={StaticResource ByteImage}}" />
                                 <Border
                                     Grid.Column="2"
@@ -157,4 +157,4 @@
             </ListBox.ItemTemplate>
         </ListBox>
     </Grid>
-</UserControl>
\ No newline at end of file
+</UserControl>
diff --git a/src/Ryujinx.Ava/UI/ViewModels/AmiiboWindowViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/AmiiboWindowViewModel.cs
index 3138dd146..83624f5f3 100644
--- a/src/Ryujinx.Ava/UI/ViewModels/AmiiboWindowViewModel.cs
+++ b/src/Ryujinx.Ava/UI/ViewModels/AmiiboWindowViewModel.cs
@@ -327,7 +327,7 @@ namespace Ryujinx.Ava.UI.ViewModels
 
             string imageUrl = _amiiboList.Find(amiibo => amiibo.Equals(selected)).Image;
 
-            string usageString = "";
+            StringBuilder usageStringBuilder = new();
 
             for (int i = 0; i < _amiiboList.Count; i++)
             {
@@ -341,20 +341,19 @@ namespace Ryujinx.Ava.UI.ViewModels
                         {
                             foreach (AmiiboApiUsage usageItem in item.AmiiboUsage)
                             {
-                                usageString += Environment.NewLine +
-                                               $"- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}";
+                                usageStringBuilder.Append($"{Environment.NewLine}- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}");
 
                                 writable = usageItem.Write;
                             }
                         }
                     }
 
-                    if (usageString.Length == 0)
+                    if (usageStringBuilder.Length == 0)
                     {
-                        usageString = LocaleManager.Instance[LocaleKeys.Unknown] + ".";
+                        usageStringBuilder.Append($"{LocaleManager.Instance[LocaleKeys.Unknown]}.");
                     }
 
-                    Usage = $"{LocaleManager.Instance[LocaleKeys.Usage]} {(writable ? $" ({LocaleManager.Instance[LocaleKeys.Writable]})" : "")} : {usageString}";
+                    Usage = $"{LocaleManager.Instance[LocaleKeys.Usage]} {(writable ? $" ({LocaleManager.Instance[LocaleKeys.Writable]})" : "")} : {usageStringBuilder}";
                 }
             }
 
diff --git a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs
index 74b737511..b88bd3d9c 100644
--- a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs
+++ b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs
@@ -212,9 +212,9 @@ namespace Ryujinx.Ava.UI.ViewModels
                     {
                         Patterns = new[] { "*.nsp" },
                         AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nsp" },
-                        MimeTypes = new[] { "application/x-nx-nsp" }
-                    }
-                }
+                        MimeTypes = new[] { "application/x-nx-nsp" },
+                    },
+                },
             });
 
             foreach (var file in result)
diff --git a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs
index aa6e0326f..7a9e4df14 100644
--- a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs
+++ b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs
@@ -26,6 +26,7 @@ using Ryujinx.HLE.FileSystem;
 using Ryujinx.HLE.HOS;
 using Ryujinx.HLE.HOS.Services.Account.Acc;
 using Ryujinx.HLE.Ui;
+using Ryujinx.Input.HLE;
 using Ryujinx.Modules;
 using Ryujinx.Ui.App.Common;
 using Ryujinx.Ui.Common;
@@ -39,7 +40,6 @@ using System.IO;
 using System.Threading;
 using System.Threading.Tasks;
 using Image = SixLabors.ImageSharp.Image;
-using InputManager = Ryujinx.Input.HLE.InputManager;
 using Key = Ryujinx.Input.Key;
 using MissingKeyException = LibHac.Common.Keys.MissingKeyException;
 using ShaderCacheLoadingState = Ryujinx.Graphics.Gpu.Shader.ShaderCacheState;
@@ -1068,9 +1068,7 @@ namespace Ryujinx.Ava.UI.ViewModels
                 {
                     Logger.Error?.Print(LogClass.Application, ex.ToString());
 
-                    static async void Action() => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys);
-
-                    Dispatcher.UIThread.Post(Action);
+                    await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys);
                 }
             }
             catch (Exception ex)
@@ -1163,16 +1161,13 @@ namespace Ryujinx.Ava.UI.ViewModels
             AppHost?.DisposeContext();
         }
 
-        private void HandleRelaunch()
+        private async Task HandleRelaunch()
         {
             if (UserChannelPersistence.PreviousIndex != -1 && UserChannelPersistence.ShouldRestart)
             {
                 UserChannelPersistence.ShouldRestart = false;
 
-                Dispatcher.UIThread.Post(() =>
-                {
-                    LoadApplication(_currentEmulatedGamePath);
-                });
+                await LoadApplication(_currentEmulatedGamePath);
             }
             else
             {
@@ -1191,7 +1186,7 @@ namespace Ryujinx.Ava.UI.ViewModels
                     Application.Current.Styles.TryGetResource(args.VSyncEnabled
                         ? "VsyncEnabled"
                         : "VsyncDisabled",
-                        Avalonia.Application.Current.ActualThemeVariant,
+                        Application.Current.ActualThemeVariant,
                         out object color);
 
                     if (color is not null)
@@ -1283,7 +1278,12 @@ namespace Ryujinx.Ava.UI.ViewModels
             Glyph = Glyph.Grid;
         }
 
-        public async void InstallFirmwareFromFile()
+        public void SetAspectRatio(AspectRatio aspectRatio)
+        {
+            ConfigurationState.Instance.Graphics.AspectRatio.Value = aspectRatio;
+        }
+
+        public async Task InstallFirmwareFromFile()
         {
             var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
             {
@@ -1294,21 +1294,21 @@ namespace Ryujinx.Ava.UI.ViewModels
                     {
                         Patterns = new[] { "*.xci", "*.zip" },
                         AppleUniformTypeIdentifiers = new[] { "com.ryujinx.xci", "public.zip-archive" },
-                        MimeTypes = new[] { "application/x-nx-xci", "application/zip" }
+                        MimeTypes = new[] { "application/x-nx-xci", "application/zip" },
                     },
                     new("XCI")
                     {
                         Patterns = new[] { "*.xci" },
                         AppleUniformTypeIdentifiers = new[] { "com.ryujinx.xci" },
-                        MimeTypes = new[] { "application/x-nx-xci" }
+                        MimeTypes = new[] { "application/x-nx-xci" },
                     },
                     new("ZIP")
                     {
                         Patterns = new[] { "*.zip" },
                         AppleUniformTypeIdentifiers = new[] { "public.zip-archive" },
-                        MimeTypes = new[] { "application/zip" }
+                        MimeTypes = new[] { "application/zip" },
                     },
-                }
+                },
             });
 
             if (result.Count > 0)
@@ -1317,11 +1317,11 @@ namespace Ryujinx.Ava.UI.ViewModels
             }
         }
 
-        public async void InstallFirmwareFromFolder()
+        public async Task InstallFirmwareFromFolder()
         {
             var result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
             {
-                AllowMultiple = false
+                AllowMultiple = false,
             });
 
             if (result.Count > 0)
@@ -1352,7 +1352,7 @@ namespace Ryujinx.Ava.UI.ViewModels
             }
         }
 
-        public async void ExitCurrentState()
+        public async Task ExitCurrentState()
         {
             if (WindowState == WindowState.FullScreen)
             {
@@ -1377,7 +1377,7 @@ namespace Ryujinx.Ava.UI.ViewModels
             }
         }
 
-        public async void ManageProfiles()
+        public async Task ManageProfiles()
         {
             await NavigationDialogHost.Show(AccountManager, ContentManager, VirtualFileSystem, LibHacHorizonManager.RyujinxClient);
         }
@@ -1387,7 +1387,7 @@ namespace Ryujinx.Ava.UI.ViewModels
             AppHost.Device.System.SimulateWakeUpMessage();
         }
 
-        public async void OpenFile()
+        public async Task OpenFile()
         {
             var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
             {
@@ -1404,7 +1404,7 @@ namespace Ryujinx.Ava.UI.ViewModels
                             "com.ryujinx.xci",
                             "com.ryujinx.nca",
                             "com.ryujinx.nro",
-                            "com.ryujinx.nso"
+                            "com.ryujinx.nso",
                         },
                         MimeTypes = new[]
                         {
@@ -1412,63 +1412,63 @@ namespace Ryujinx.Ava.UI.ViewModels
                             "application/x-nx-xci",
                             "application/x-nx-nca",
                             "application/x-nx-nro",
-                            "application/x-nx-nso"
-                        }
+                            "application/x-nx-nso",
+                        },
                     },
                     new("NSP")
                     {
                         Patterns = new[] { "*.nsp" },
                         AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nsp" },
-                        MimeTypes = new[] { "application/x-nx-nsp" }
+                        MimeTypes = new[] { "application/x-nx-nsp" },
                     },
                     new("XCI")
                     {
                         Patterns = new[] { "*.xci" },
                         AppleUniformTypeIdentifiers = new[] { "com.ryujinx.xci" },
-                        MimeTypes = new[] { "application/x-nx-xci" }
+                        MimeTypes = new[] { "application/x-nx-xci" },
                     },
                     new("NCA")
                     {
                         Patterns = new[] { "*.nca" },
                         AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nca" },
-                        MimeTypes = new[] { "application/x-nx-nca" }
+                        MimeTypes = new[] { "application/x-nx-nca" },
                     },
                     new("NRO")
                     {
                         Patterns = new[] { "*.nro" },
                         AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nro" },
-                        MimeTypes = new[] { "application/x-nx-nro" }
+                        MimeTypes = new[] { "application/x-nx-nro" },
                     },
                     new("NSO")
                     {
                         Patterns = new[] { "*.nso" },
                         AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nso" },
-                        MimeTypes = new[] { "application/x-nx-nso" }
+                        MimeTypes = new[] { "application/x-nx-nso" },
                     },
-                }
+                },
             });
 
             if (result.Count > 0)
             {
-                LoadApplication(result[0].Path.LocalPath);
+                await LoadApplication(result[0].Path.LocalPath);
             }
         }
 
-        public async void OpenFolder()
+        public async Task OpenFolder()
         {
             var result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
             {
                 Title = LocaleManager.Instance[LocaleKeys.OpenFolderDialogTitle],
-                AllowMultiple = false
+                AllowMultiple = false,
             });
 
             if (result.Count > 0)
             {
-                LoadApplication(result[0].Path.LocalPath);
+                await LoadApplication(result[0].Path.LocalPath);
             }
         }
 
-        public async void LoadApplication(string path, bool startFullscreen = false, string titleName = "")
+        public async Task LoadApplication(string path, bool startFullscreen = false, string titleName = "")
         {
             if (AppHost != null)
             {
@@ -1505,35 +1505,30 @@ namespace Ryujinx.Ava.UI.ViewModels
                 this,
                 TopLevel);
 
-            async void Action()
+            if (!await AppHost.LoadGuestApplication())
             {
-                if (!await AppHost.LoadGuestApplication())
-                {
-                    AppHost.DisposeContext();
-                    AppHost = null;
+                AppHost.DisposeContext();
+                AppHost = null;
 
-                    return;
-                }
-
-                CanUpdate = false;
-
-                LoadHeading = TitleName = titleName;
-
-                if (string.IsNullOrWhiteSpace(titleName))
-                {
-                    LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, AppHost.Device.Processes.ActiveApplication.Name);
-                    TitleName = AppHost.Device.Processes.ActiveApplication.Name;
-                }
-
-                SwitchToRenderer(startFullscreen);
-
-                _currentEmulatedGamePath = path;
-
-                Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" };
-                gameThread.Start();
+                return;
             }
 
-            Dispatcher.UIThread.Post(Action);
+            CanUpdate = false;
+
+            LoadHeading = TitleName = titleName;
+
+            if (string.IsNullOrWhiteSpace(titleName))
+            {
+                LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, AppHost.Device.Processes.ActiveApplication.Name);
+                TitleName = AppHost.Device.Processes.ActiveApplication.Name;
+            }
+
+            SwitchToRenderer(startFullscreen);
+
+            _currentEmulatedGamePath = path;
+
+            Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" };
+            gameThread.Start();
         }
 
         public void SwitchToRenderer(bool startFullscreen)
@@ -1596,7 +1591,7 @@ namespace Ryujinx.Ava.UI.ViewModels
 
             IsGameRunning = false;
 
-            Dispatcher.UIThread.InvokeAsync(() =>
+            Dispatcher.UIThread.InvokeAsync(async () =>
             {
                 ShowMenuAndStatusBar = true;
                 ShowContent = true;
@@ -1609,7 +1604,7 @@ namespace Ryujinx.Ava.UI.ViewModels
 
                 AppHost = null;
 
-                HandleRelaunch();
+                await HandleRelaunch();
             });
 
             RendererHostControl.WindowCreated -= RendererHost_Created;
diff --git a/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs
index 2669e8a26..217b0c10e 100644
--- a/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs
+++ b/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs
@@ -10,6 +10,7 @@ using Ryujinx.Ava.UI.Helpers;
 using Ryujinx.Ava.UI.Windows;
 using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Configuration.Hid;
+using Ryujinx.Common.Configuration.Multiplayer;
 using Ryujinx.Common.GraphicsDriver;
 using Ryujinx.Common.Logging;
 using Ryujinx.Graphics.Vulkan;
@@ -54,6 +55,7 @@ namespace Ryujinx.Ava.UI.ViewModels
         public event Action CloseWindow;
         public event Action SaveSettingsEvent;
         private int _networkInterfaceIndex;
+        private int _multiplayerModeIndex;
 
         public int ResolutionScale
         {
@@ -76,14 +78,13 @@ namespace Ryujinx.Ava.UI.ViewModels
 
                 if (_graphicsBackendMultithreadingIndex != (int)ConfigurationState.Instance.Graphics.BackendThreading.Value)
                 {
-                    Dispatcher.UIThread.Post(async () =>
-                    {
-                        await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningMessage],
+                    Dispatcher.UIThread.InvokeAsync(() =>
+                         ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningMessage],
                             "",
                             "",
                             LocaleManager.Instance[LocaleKeys.InputDialogOk],
-                            LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningTitle]);
-                    });
+                            LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningTitle])
+                    );
                 }
 
                 OnPropertyChanged();
@@ -147,6 +148,7 @@ namespace Ryujinx.Ava.UI.ViewModels
         public bool EnableTextureRecompression { get; set; }
         public bool EnableMacroHLE { get; set; }
         public bool EnableColorSpacePassthrough { get; set; }
+        public bool ColorSpacePassthroughAvailable => IsMacOS;
         public bool EnableFileLog { get; set; }
         public bool EnableStub { get; set; }
         public bool EnableInfo { get; set; }
@@ -256,6 +258,11 @@ namespace Ryujinx.Ava.UI.ViewModels
             get => new(_networkInterfaces.Keys);
         }
 
+        public AvaloniaList<string> MultiplayerModes
+        {
+            get => new(Enum.GetNames<MultiplayerMode>());
+        }
+
         public KeyboardHotkeys KeyboardHotkeys
         {
             get => _keyboardHotkeys;
@@ -277,6 +284,16 @@ namespace Ryujinx.Ava.UI.ViewModels
             }
         }
 
+        public int MultiplayerModeIndex
+        {
+            get => _multiplayerModeIndex;
+            set
+            {
+                _multiplayerModeIndex = value;
+                ConfigurationState.Instance.Multiplayer.Mode.Value = (MultiplayerMode)_multiplayerModeIndex;
+            }
+        }
+
         public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this()
         {
             _virtualFileSystem = virtualFileSystem;
@@ -484,6 +501,8 @@ namespace Ryujinx.Ava.UI.ViewModels
             EnableFsAccessLog = config.Logger.EnableFsAccessLog;
             FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode;
             OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value;
+
+            MultiplayerModeIndex = (int)config.Multiplayer.Mode.Value;
         }
 
         public void SaveSettings()
@@ -586,6 +605,7 @@ namespace Ryujinx.Ava.UI.ViewModels
             config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel;
 
             config.Multiplayer.LanInterfaceId.Value = _networkInterfaces[NetworkInterfaceList[NetworkInterfaceIndex]];
+            config.Multiplayer.Mode.Value = (MultiplayerMode)MultiplayerModeIndex;
 
             config.ToFileFormat().SaveConfig(Program.ConfigurationPath);
 
diff --git a/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs
index e3bca205d..dd0b92a51 100644
--- a/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs
+++ b/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs
@@ -22,6 +22,7 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using System.Threading.Tasks;
 using Path = System.IO.Path;
 using SpanHelpers = LibHac.Common.SpanHelpers;
 
@@ -184,18 +185,12 @@ namespace Ryujinx.Ava.UI.ViewModels
                     }
                     else
                     {
-                        Dispatcher.UIThread.Post(async () =>
-                        {
-                            await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]);
-                        });
+                        Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]));
                     }
                 }
                 catch (Exception ex)
                 {
-                    Dispatcher.UIThread.Post(async () =>
-                    {
-                        await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadNcaErrorMessage, ex.Message, path));
-                    });
+                    Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadNcaErrorMessage, ex.Message, path)));
                 }
             }
         }
@@ -207,7 +202,7 @@ namespace Ryujinx.Ava.UI.ViewModels
             SortUpdates();
         }
 
-        public async void Add()
+        public async Task Add()
         {
             var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
             {
@@ -218,9 +213,9 @@ namespace Ryujinx.Ava.UI.ViewModels
                     {
                         Patterns = new[] { "*.nsp" },
                         AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nsp" },
-                        MimeTypes = new[] { "application/x-nx-nsp" }
-                    }
-                }
+                        MimeTypes = new[] { "application/x-nx-nsp" },
+                    },
+                },
             });
 
             foreach (var file in result)
diff --git a/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml
index 65a66d9e2..2ab42e6ee 100644
--- a/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml
+++ b/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml
@@ -465,6 +465,7 @@
                                         Maximum="1"
                                         TickFrequency="0.01"
                                         IsSnapToTickEnabled="True"
+                                        SmallChange="0.01"
                                         Minimum="0"
                                         Value="{ReflectionBinding Configuration.DeadzoneLeft, Mode=TwoWay}" />
                                     <TextBlock
@@ -484,6 +485,7 @@
                                         Maximum="2"
                                         TickFrequency="0.01"
                                         IsSnapToTickEnabled="True"
+                                        SmallChange="0.01"
                                         Minimum="0"
                                         Value="{ReflectionBinding Configuration.RangeLeft, Mode=TwoWay}" />
                                     <TextBlock
@@ -607,6 +609,7 @@
                                 Maximum="1"
                                 TickFrequency="0.01"
                                 IsSnapToTickEnabled="True"
+                                SmallChange="0.01"
                                 Minimum="0"
                                 Value="{ReflectionBinding Configuration.TriggerThreshold, Mode=TwoWay}" />
                             <TextBlock
@@ -1085,6 +1088,7 @@
                                         Maximum="1"
                                         TickFrequency="0.01"
                                         IsSnapToTickEnabled="True"
+                                        SmallChange="0.01"
                                         Padding="0"
                                         VerticalAlignment="Center"
                                         Minimum="0"
@@ -1106,6 +1110,7 @@
                                         Maximum="2"
                                         TickFrequency="0.01"
                                         IsSnapToTickEnabled="True"
+                                        SmallChange="0.01"
                                         Minimum="0"
                                         Value="{ReflectionBinding Configuration.RangeRight, Mode=TwoWay}" />
                                     <TextBlock
diff --git a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml
index 71d5d7460..a98f08825 100644
--- a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml
+++ b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml
@@ -29,6 +29,7 @@
                     MaxWidth="150"
                     TickFrequency="0.01"
                     IsSnapToTickEnabled="True"
+                    SmallChange="0.01"
                     Maximum="100"
                     Minimum="0"
                     Value="{Binding Sensitivity, Mode=TwoWay}" />
@@ -50,6 +51,7 @@
                     MaxWidth="150"
                     TickFrequency="0.01"
                     IsSnapToTickEnabled="True"
+                    SmallChange="0.01"
                     Maximum="100"
                     Minimum="0"
                     Value="{Binding GyroDeadzone, Mode=TwoWay}" />
diff --git a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml
index 16190d391..f633c0ed2 100644
--- a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml
+++ b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml
@@ -26,6 +26,7 @@
                     Width="200"
                     TickFrequency="0.01"
                     IsSnapToTickEnabled="True"
+                    SmallChange="0.01"
                     Maximum="10"
                     Minimum="0"
                     Value="{Binding StrongRumble, Mode=TwoWay}" />
@@ -47,6 +48,7 @@
                     Maximum="10"
                     TickFrequency="0.01"
                     IsSnapToTickEnabled="True"
+                    SmallChange="0.01"
                     Minimum="0"
                     Value="{Binding WeakRumble, Mode=TwoWay}" />
                 <TextBlock
diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs
index af8c4dab9..4f2d262da 100644
--- a/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs
+++ b/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs
@@ -17,7 +17,6 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
-using System.Threading.Tasks;
 
 namespace Ryujinx.Ava.UI.Views.Main
 {
@@ -107,20 +106,14 @@ namespace Ryujinx.Ava.UI.Views.Main
             await Window.ViewModel.AppHost?.ShowExitPrompt();
         }
 
-        private async void PauseEmulation_Click(object sender, RoutedEventArgs e)
+        private void PauseEmulation_Click(object sender, RoutedEventArgs e)
         {
-            await Task.Run(() =>
-            {
-                Window.ViewModel.AppHost?.Pause();
-            });
+            Window.ViewModel.AppHost?.Pause();
         }
 
-        private async void ResumeEmulation_Click(object sender, RoutedEventArgs e)
+        private void ResumeEmulation_Click(object sender, RoutedEventArgs e)
         {
-            await Task.Run(() =>
-            {
-                Window.ViewModel.AppHost?.Resume();
-            });
+            Window.ViewModel.AppHost?.Resume();
         }
 
         public async void OpenSettings(object sender, RoutedEventArgs e)
@@ -132,13 +125,13 @@ namespace Ryujinx.Ava.UI.Views.Main
             ViewModel.LoadConfigurableHotKeys();
         }
 
-        public void OpenMiiApplet(object sender, RoutedEventArgs e)
+        public async void OpenMiiApplet(object sender, RoutedEventArgs e)
         {
             string contentPath = ViewModel.ContentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program);
 
             if (!string.IsNullOrEmpty(contentPath))
             {
-                ViewModel.LoadApplication(contentPath, false, "Mii Applet");
+                await ViewModel.LoadApplication(contentPath, false, "Mii Applet");
             }
         }
 
@@ -196,8 +189,7 @@ namespace Ryujinx.Ava.UI.Views.Main
         {
             if (FileAssociationHelper.Install())
             {
-                await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogInstallFileTypesSuccessMessage],
-                    string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
+                await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogInstallFileTypesSuccessMessage], string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
             }
             else
             {
@@ -209,8 +201,7 @@ namespace Ryujinx.Ava.UI.Views.Main
         {
             if (FileAssociationHelper.Uninstall())
             {
-                await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogUninstallFileTypesSuccessMessage],
-                    string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
+                await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogUninstallFileTypesSuccessMessage], string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
             }
             else
             {
diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml b/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml
index 58e06a1c2..32524740b 100644
--- a/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml
+++ b/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml
@@ -6,6 +6,7 @@
     xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
     xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
     xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
+    xmlns:config="clr-namespace:Ryujinx.Common.Configuration;assembly=Ryujinx.Common"
     mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
     x:Class="Ryujinx.Ava.UI.Views.Main.MainStatusBarView"
     x:DataType="viewModels:MainWindowViewModel">
@@ -112,15 +113,52 @@
                 Background="Gray"
                 BorderThickness="1"
                 IsVisible="{Binding !ShowLoadProgress}" />
-            <TextBlock
+            <SplitButton
                 Name="AspectRatioStatus"
-                Margin="5,0,5,0"
+                Padding="5,0,5,0"
                 HorizontalAlignment="Left"
                 VerticalAlignment="Center"
+                Background="Transparent"
+                BorderThickness="0"
+                CornerRadius="0"
                 IsVisible="{Binding !ShowLoadProgress}"
-                PointerReleased="AspectRatioStatus_PointerReleased"
-                Text="{Binding AspectRatioStatusText}"
-                TextAlignment="Left" />
+                Content="{Binding AspectRatioStatusText}"
+                Click="AspectRatioStatus_OnClick"
+                ToolTip.Tip="{locale:Locale AspectRatioTooltip}">
+                <SplitButton.Styles>
+                    <Style Selector="Border#SeparatorBorder">
+                        <Setter Property="Opacity" Value="0" />
+                    </Style>
+                </SplitButton.Styles>
+                <SplitButton.Flyout>
+                    <MenuFlyout Placement="Bottom" ShowMode="TransientWithDismissOnPointerMoveAway">
+                        <MenuItem
+                            Header="{locale:Locale SettingsTabGraphicsAspectRatio4x3}"
+                            Command="{Binding SetAspectRatio}"
+                            CommandParameter="{x:Static config:AspectRatio.Fixed4x3}"/>
+                        <MenuItem
+                            Header="{locale:Locale SettingsTabGraphicsAspectRatio16x9}"
+                            Command="{Binding SetAspectRatio}"
+                            CommandParameter="{x:Static config:AspectRatio.Fixed16x9}"/>
+                        <MenuItem
+                            Header="{locale:Locale SettingsTabGraphicsAspectRatio16x10}"
+                            Command="{Binding SetAspectRatio}"
+                            CommandParameter="{x:Static config:AspectRatio.Fixed16x10}"/>
+                        <MenuItem
+                            Header="{locale:Locale SettingsTabGraphicsAspectRatio21x9}"
+                            Command="{Binding SetAspectRatio}"
+                            CommandParameter="{x:Static config:AspectRatio.Fixed21x9}"/>
+                        <MenuItem
+                            Header="{locale:Locale SettingsTabGraphicsAspectRatio32x9}"
+                            Command="{Binding SetAspectRatio}"
+                            CommandParameter="{x:Static config:AspectRatio.Fixed32x9}"/>
+                        <MenuItem
+                            Header="{locale:Locale SettingsTabGraphicsAspectRatioStretch}"
+                            Command="{Binding SetAspectRatio}"
+                            CommandParameter="{x:Static config:AspectRatio.Stretched}"/>
+                    </MenuFlyout>
+                </SplitButton.Flyout>
+            </SplitButton>
             <Border
                 Width="2"
                 Height="12"
diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml.cs
index a0acc2779..44426295b 100644
--- a/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml.cs
+++ b/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml.cs
@@ -43,10 +43,9 @@ namespace Ryujinx.Ava.UI.Views.Main
             ConfigurationState.Instance.System.EnableDockedMode.Value = !ConfigurationState.Instance.System.EnableDockedMode.Value;
         }
 
-        private void AspectRatioStatus_PointerReleased(object sender, PointerReleasedEventArgs e)
+        private void AspectRatioStatus_OnClick(object sender, RoutedEventArgs e)
         {
             AspectRatio aspectRatio = ConfigurationState.Instance.Graphics.AspectRatio.Value;
-
             ConfigurationState.Instance.Graphics.AspectRatio.Value = (int)aspectRatio + 1 > Enum.GetNames(typeof(AspectRatio)).Length - 1 ? AspectRatio.Fixed4x3 : aspectRatio + 1;
         }
 
diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml b/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml
index f5a177424..34624b222 100644
--- a/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml
+++ b/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml
@@ -56,6 +56,7 @@
             Margin="5,-10,5,0"
             VerticalAlignment="Center"
             IsSnapToTickEnabled="True"
+            SmallChange="1"
             Maximum="4"
             Minimum="1"
             TickFrequency="1"
diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml
index 1e3bb4ba4..d5039c131 100644
--- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml
+++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml
@@ -77,6 +77,7 @@
                             <TextBlock Text="{locale:Locale SettingsEnableMacroHLE}" />
                         </CheckBox>
                         <CheckBox IsChecked="{Binding EnableColorSpacePassthrough}"
+                                  IsVisible="{Binding ColorSpacePassthroughAvailable}"
                             ToolTip.Tip="{locale:Locale SettingsEnableColorSpacePassthroughTooltip}">
                             <TextBlock Text="{locale:Locale SettingsEnableColorSpacePassthrough}" />
                         </CheckBox>
diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsNetworkView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsNetworkView.axaml
index 6ce1bb94f..9bb814631 100644
--- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsNetworkView.axaml
+++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsNetworkView.axaml
@@ -23,21 +23,34 @@
                 HorizontalAlignment="Stretch"
                 Orientation="Vertical"
                 Spacing="10">
+                <TextBlock Classes="h1" Text="{locale:Locale SettingsTabNetworkMultiplayer}" />
+                <StackPanel Margin="10,0,0,0" Orientation="Horizontal">
+                    <TextBlock VerticalAlignment="Center"
+                        Text="{locale:Locale MultiplayerMode}"
+                        ToolTip.Tip="{locale:Locale MultiplayerModeTooltip}"
+                        Width="200" />
+                    <ComboBox SelectedIndex="{Binding MultiplayerModeIndex}"
+                        ToolTip.Tip="{locale:Locale MultiplayerModeTooltip}"
+                        HorizontalContentAlignment="Left"
+                        ItemsSource="{Binding MultiplayerModes}"
+                        Width="250" />
+                </StackPanel>
+                <Separator Height="1" />
                 <TextBlock Classes="h1" Text="{locale:Locale SettingsTabNetworkConnection}" />
                 <CheckBox Margin="10,0,0,0" IsChecked="{Binding EnableInternetAccess}">
                     <TextBlock Text="{locale:Locale SettingsTabSystemEnableInternetAccess}"
-                               ToolTip.Tip="{locale:Locale EnableInternetAccessTooltip}" />
+                        ToolTip.Tip="{locale:Locale EnableInternetAccessTooltip}" />
                 </CheckBox>
                 <StackPanel Margin="10,0,0,0" Orientation="Horizontal">
                     <TextBlock VerticalAlignment="Center"
-                               Text="{locale:Locale SettingsTabNetworkInterface}"
-                               ToolTip.Tip="{locale:Locale NetworkInterfaceTooltip}"
-                               Width="200" />
+                        Text="{locale:Locale SettingsTabNetworkInterface}"
+                        ToolTip.Tip="{locale:Locale NetworkInterfaceTooltip}"
+                        Width="200" />
                     <ComboBox SelectedIndex="{Binding NetworkInterfaceIndex}"
-                              ToolTip.Tip="{locale:Locale NetworkInterfaceTooltip}"
-                              HorizontalContentAlignment="Left"
-                              ItemsSource="{Binding NetworkInterfaceList}"
-                              Width="250" />
+                        ToolTip.Tip="{locale:Locale NetworkInterfaceTooltip}"
+                        HorizontalContentAlignment="Left"
+                        ItemsSource="{Binding NetworkInterfaceList}"
+                        Width="250" />
                 </StackPanel>
             </StackPanel>
         </Border>
diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsUIView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Settings/SettingsUIView.axaml.cs
index a38a88655..6d9299dda 100644
--- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsUIView.axaml.cs
+++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsUIView.axaml.cs
@@ -34,7 +34,7 @@ namespace Ryujinx.Ava.UI.Views.Settings
                 {
                     var result = await window.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
                     {
-                        AllowMultiple = false
+                        AllowMultiple = false,
                     });
 
                     if (result.Count > 0)
@@ -75,9 +75,9 @@ namespace Ryujinx.Ava.UI.Views.Settings
                     {
                         Patterns = new[] { "*.xaml" },
                         AppleUniformTypeIdentifiers = new[] { "com.ryujinx.xaml" },
-                        MimeTypes = new[] { "application/xaml+xml" }
-                    }
-                }
+                        MimeTypes = new[] { "application/xaml+xml" },
+                    },
+                },
             });
 
             if (result.Count > 0)
diff --git a/src/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml.cs b/src/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml.cs
index e9bf4408c..fabfaa4e8 100644
--- a/src/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml.cs
+++ b/src/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml.cs
@@ -75,9 +75,9 @@ namespace Ryujinx.Ava.UI.Views.User
                     {
                         Patterns = new[] { "*.jpg", "*.jpeg", "*.png", "*.bmp" },
                         AppleUniformTypeIdentifiers = new[] { "public.jpeg", "public.png", "com.microsoft.bmp" },
-                        MimeTypes = new[] { "image/jpeg", "image/png", "image/bmp" }
-                    }
-                }
+                        MimeTypes = new[] { "image/jpeg", "image/png", "image/bmp" },
+                    },
+                },
             });
 
             if (result.Count > 0)
diff --git a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml
index b9cbcb9cc..8a5da5cc2 100644
--- a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml
+++ b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml
@@ -11,6 +11,7 @@
     Height="500"
     MinWidth="500"
     MinHeight="500"
+    x:DataType="window:CheatWindow"
     WindowStartupLocation="CenterOwner"
     mc:Ignorable="d"
     Focusable="True">
@@ -40,7 +41,7 @@
             HorizontalAlignment="Center"
             VerticalAlignment="Center"
             LineHeight="18"
-            Text="{ReflectionBinding Heading}"
+            Text="{Binding Heading}"
             TextAlignment="Center"
             TextWrapping="Wrap" />
         <TextBlock
@@ -61,7 +62,7 @@
             MinWidth="160"
             HorizontalAlignment="Center"
             VerticalAlignment="Center"
-            Text="{ReflectionBinding BuildId}"
+            Text="{Binding BuildId}"
             IsReadOnly="True" />
         <Border
             Grid.Row="3"
@@ -77,7 +78,7 @@
                 MinHeight="300"
                 HorizontalAlignment="Stretch"
                 VerticalAlignment="Stretch"
-                ItemsSource="{ReflectionBinding LoadedCheats}">
+                ItemsSource="{Binding LoadedCheats}">
                 <TreeView.Styles>
                     <Styles>
                         <Style Selector="TreeViewItem:empty /template/ ItemsPresenter">
@@ -120,18 +121,18 @@
                     Name="SaveButton"
                     MinWidth="90"
                     Margin="5"
-                    Command="{ReflectionBinding Save}"
-                    IsVisible="{ReflectionBinding !NoCheatsFound}">
+                    Command="{Binding Save}"
+                    IsVisible="{Binding !NoCheatsFound}">
                     <TextBlock Text="{locale:Locale SettingsButtonSave}" />
                 </Button>
                 <Button
                     Name="CancelButton"
                     MinWidth="90"
                     Margin="5"
-                    Command="{ReflectionBinding Close}">
+                    Command="{Binding Close}">
                     <TextBlock Text="{locale:Locale InputDialogCancel}" />
                 </Button>
             </DockPanel>
         </DockPanel>
     </Grid>
-</window:StyleableWindow>
\ No newline at end of file
+</window:StyleableWindow>
diff --git a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs
index df6b123bd..fde249a0c 100644
--- a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs
+++ b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs
@@ -17,7 +17,7 @@ namespace Ryujinx.Ava.UI.Windows
         private readonly string _enabledCheatsPath;
         public bool NoCheatsFound { get; }
 
-        private AvaloniaList<CheatsList> LoadedCheats { get; }
+        public AvaloniaList<CheatsList> LoadedCheats { get; }
 
         public string Heading { get; }
         public string BuildId { get; }
diff --git a/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml b/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml
index 1f57f34cc..99cf28e77 100644
--- a/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml
+++ b/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml
@@ -39,14 +39,14 @@
                         Name="EnableAllButton"
                         MinWidth="90"
                         Margin="5"
-                        Command="{ReflectionBinding EnableAll}">
+                        Command="{Binding EnableAll}">
                         <TextBlock Text="{locale:Locale DlcManagerEnableAllButton}" />
                     </Button>
                     <Button
                         Name="DisableAllButton"
                         MinWidth="90"
                         Margin="5"
-                        Command="{ReflectionBinding DisableAll}">
+                        Command="{Binding DisableAll}">
                         <TextBlock Text="{locale:Locale DlcManagerDisableAllButton}" />
                     </Button>
                 </StackPanel>
@@ -157,14 +157,14 @@
                     Name="AddButton"
                     MinWidth="90"
                     Margin="5"
-                    Command="{ReflectionBinding Add}">
+                    Command="{Binding Add}">
                     <TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" />
                 </Button>
                 <Button
                     Name="RemoveAllButton"
                     MinWidth="90"
                     Margin="5"
-                    Command="{ReflectionBinding RemoveAll}">
+                    Command="{Binding RemoveAll}">
                     <TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" />
                 </Button>
             </StackPanel>
@@ -189,4 +189,4 @@
             </StackPanel>
         </Panel>
     </Grid>
-</UserControl>
\ No newline at end of file
+</UserControl>
diff --git a/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs
index b7aa0dcaf..6c35061e8 100644
--- a/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs
+++ b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs
@@ -15,6 +15,7 @@ using Ryujinx.Graphics.Gpu;
 using Ryujinx.HLE.FileSystem;
 using Ryujinx.HLE.HOS;
 using Ryujinx.HLE.HOS.Services.Account.Acc;
+using Ryujinx.Input.HLE;
 using Ryujinx.Input.SDL2;
 using Ryujinx.Modules;
 using Ryujinx.Ui.App.Common;
@@ -24,8 +25,8 @@ using Ryujinx.Ui.Common.Helper;
 using System;
 using System.IO;
 using System.Runtime.Versioning;
+using System.Threading;
 using System.Threading.Tasks;
-using InputManager = Ryujinx.Input.HLE.InputManager;
 
 namespace Ryujinx.Ava.UI.Windows
 {
@@ -79,35 +80,11 @@ namespace Ryujinx.Ava.UI.Windows
 
             if (Program.PreviewerDetached)
             {
-                Initialize();
-
                 InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL2GamepadDriver());
 
-                ViewModel.Initialize(
-                    ContentManager,
-                    StorageProvider,
-                    ApplicationLibrary,
-                    VirtualFileSystem,
-                    AccountManager,
-                    InputManager,
-                    _userChannelPersistence,
-                    LibHacHorizonManager,
-                    UiHandler,
-                    ShowLoading,
-                    SwitchToGameControl,
-                    SetMainContent,
-                    this);
-
-                ViewModel.RefreshFirmwareStatus();
-
-                LoadGameList();
-
                 this.GetObservable(IsActiveProperty).Subscribe(IsActiveChanged);
                 this.ScalingChanged += OnScalingChanged;
             }
-
-            ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated;
-            ApplicationLibrary.ApplicationAdded += ApplicationLibrary_ApplicationAdded;
         }
 
         protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
@@ -122,36 +99,17 @@ namespace Ryujinx.Ava.UI.Windows
             ViewModel.IsActive = obj;
         }
 
-        public void LoadGameList()
-        {
-            if (_isLoading)
-            {
-                return;
-            }
-
-            _isLoading = true;
-
-            LoadApplications();
-
-            _isLoading = false;
-        }
-
         private void OnScalingChanged(object sender, EventArgs e)
         {
             Program.DesktopScaleFactor = this.RenderScaling;
         }
 
-        public void AddApplication(ApplicationData applicationData)
-        {
-            Dispatcher.UIThread.InvokeAsync(() =>
-            {
-                ViewModel.Applications.Add(applicationData);
-            });
-        }
-
         private void ApplicationLibrary_ApplicationAdded(object sender, ApplicationAddedEventArgs e)
         {
-            AddApplication(e.AppData);
+            Dispatcher.UIThread.Post(() =>
+            {
+                ViewModel.Applications.Add(e.AppData);
+            });
         }
 
         private void ApplicationLibrary_ApplicationCountUpdated(object sender, ApplicationCountUpdatedEventArgs e)
@@ -183,7 +141,7 @@ namespace Ryujinx.Ava.UI.Windows
 
                 string path = new FileInfo(args.Application.Path).FullName;
 
-                ViewModel.LoadApplication(path);
+                ViewModel.LoadApplication(path).Wait();
             }
 
             args.Handled = true;
@@ -202,13 +160,10 @@ namespace Ryujinx.Ava.UI.Windows
             ViewModel.ShowContent = true;
             ViewModel.IsLoadingIndeterminate = false;
 
-            Dispatcher.UIThread.InvokeAsync(() =>
+            if (startFullscreen && ViewModel.WindowState != WindowState.FullScreen)
             {
-                if (startFullscreen && ViewModel.WindowState != WindowState.FullScreen)
-                {
-                    ViewModel.ToggleFullscreen();
-                }
-            });
+                ViewModel.ToggleFullscreen();
+            }
         }
 
         public void ShowLoading(bool startFullscreen = false)
@@ -217,13 +172,10 @@ namespace Ryujinx.Ava.UI.Windows
             ViewModel.ShowLoadProgress = true;
             ViewModel.IsLoadingIndeterminate = true;
 
-            Dispatcher.UIThread.InvokeAsync(() =>
+            if (startFullscreen && ViewModel.WindowState != WindowState.FullScreen)
             {
-                if (startFullscreen && ViewModel.WindowState != WindowState.FullScreen)
-                {
-                    ViewModel.ToggleFullscreen();
-                }
-            });
+                ViewModel.ToggleFullscreen();
+            }
         }
 
         private void Initialize()
@@ -251,11 +203,11 @@ namespace Ryujinx.Ava.UI.Windows
 
             VirtualFileSystem.ReloadKeySet();
 
-            ApplicationHelper.Initialize(VirtualFileSystem, AccountManager, LibHacHorizonManager.RyujinxClient, this);
+            ApplicationHelper.Initialize(VirtualFileSystem, AccountManager, LibHacHorizonManager.RyujinxClient);
         }
 
         [SupportedOSPlatform("linux")]
-        private static async void ShowVmMaxMapCountWarning()
+        private static async Task ShowVmMaxMapCountWarning()
         {
             LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LinuxVmMaxMapCountWarningTextSecondary,
                 LinuxHelper.VmMaxMapCount, LinuxHelper.RecommendedVmMaxMapCount);
@@ -267,7 +219,7 @@ namespace Ryujinx.Ava.UI.Windows
         }
 
         [SupportedOSPlatform("linux")]
-        private static async void ShowVmMaxMapCountDialog()
+        private static async Task ShowVmMaxMapCountDialog()
         {
             LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LinuxVmMaxMapCountDialogTextPrimary,
                 LinuxHelper.RecommendedVmMaxMapCount);
@@ -313,33 +265,46 @@ namespace Ryujinx.Ava.UI.Windows
 
         private void CheckLaunchState()
         {
-            if (ShowKeyErrorOnLoad)
-            {
-                ShowKeyErrorOnLoad = false;
-
-                Dispatcher.UIThread.Post(async () => await
-                    UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys));
-            }
-
             if (OperatingSystem.IsLinux() && LinuxHelper.VmMaxMapCount < LinuxHelper.RecommendedVmMaxMapCount)
             {
                 Logger.Warning?.Print(LogClass.Application, $"The value of vm.max_map_count is lower than {LinuxHelper.RecommendedVmMaxMapCount}. ({LinuxHelper.VmMaxMapCount})");
 
                 if (LinuxHelper.PkExecPath is not null)
                 {
-                    Dispatcher.UIThread.Post(ShowVmMaxMapCountDialog);
+                    Dispatcher.UIThread.Post(async () =>
+                    {
+                        if (OperatingSystem.IsLinux())
+                        {
+                            await ShowVmMaxMapCountDialog();
+                        }
+                    });
                 }
                 else
                 {
-                    Dispatcher.UIThread.Post(ShowVmMaxMapCountWarning);
+                    Dispatcher.UIThread.Post(async () =>
+                    {
+                        if (OperatingSystem.IsLinux())
+                        {
+                            await ShowVmMaxMapCountWarning();
+                        }
+                    });
                 }
             }
 
-            if (_deferLoad)
+            if (!ShowKeyErrorOnLoad)
             {
-                _deferLoad = false;
+                if (_deferLoad)
+                {
+                    _deferLoad = false;
 
-                ViewModel.LoadApplication(_launchPath, _startFullscreen);
+                    ViewModel.LoadApplication(_launchPath, _startFullscreen).Wait();
+                }
+            }
+            else
+            {
+                ShowKeyErrorOnLoad = false;
+
+                Dispatcher.UIThread.Post(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys));
             }
 
             if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false))
@@ -372,7 +337,7 @@ namespace Ryujinx.Ava.UI.Windows
             ViewModel.WindowHeight = ConfigurationState.Instance.Ui.WindowStartup.WindowSizeHeight * Program.WindowScaleFactor;
             ViewModel.WindowWidth = ConfigurationState.Instance.Ui.WindowStartup.WindowSizeWidth * Program.WindowScaleFactor;
 
-            ViewModel.WindowState = ConfigurationState.Instance.Ui.WindowStartup.WindowMaximized.Value is true ? WindowState.Maximized : WindowState.Normal;
+            ViewModel.WindowState = ConfigurationState.Instance.Ui.WindowStartup.WindowMaximized.Value ? WindowState.Maximized : WindowState.Normal;
 
             if (CheckScreenBounds(savedPoint))
             {
@@ -415,6 +380,30 @@ namespace Ryujinx.Ava.UI.Windows
         {
             base.OnOpened(e);
 
+            Initialize();
+
+            ViewModel.Initialize(
+                ContentManager,
+                StorageProvider,
+                ApplicationLibrary,
+                VirtualFileSystem,
+                AccountManager,
+                InputManager,
+                _userChannelPersistence,
+                LibHacHorizonManager,
+                UiHandler,
+                ShowLoading,
+                SwitchToGameControl,
+                SetMainContent,
+                this);
+
+            ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated;
+            ApplicationLibrary.ApplicationAdded += ApplicationLibrary_ApplicationAdded;
+
+            ViewModel.RefreshFirmwareStatus();
+
+            LoadApplications();
+
             CheckLaunchState();
         }
 
@@ -515,18 +504,15 @@ namespace Ryujinx.Ava.UI.Windows
             });
         }
 
-        public async void LoadApplications()
+        public void LoadApplications()
         {
-            await Dispatcher.UIThread.InvokeAsync(() =>
-            {
-                ViewModel.Applications.Clear();
+            ViewModel.Applications.Clear();
 
-                StatusBarView.LoadProgressBar.IsVisible = true;
-                ViewModel.StatusBarProgressMaximum = 0;
-                ViewModel.StatusBarProgressValue = 0;
+            StatusBarView.LoadProgressBar.IsVisible = true;
+            ViewModel.StatusBarProgressMaximum = 0;
+            ViewModel.StatusBarProgressValue = 0;
 
-                LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarGamesLoaded, 0, 0);
-            });
+            LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarGamesLoaded, 0, 0);
 
             ReloadGameList();
         }
@@ -559,9 +545,17 @@ namespace Ryujinx.Ava.UI.Windows
 
             _isLoading = true;
 
-            ApplicationLibrary.LoadApplications(ConfigurationState.Instance.Ui.GameDirs.Value, ConfigurationState.Instance.System.Language);
+            Thread applicationLibraryThread = new(() =>
+            {
+                ApplicationLibrary.LoadApplications(ConfigurationState.Instance.Ui.GameDirs, ConfigurationState.Instance.System.Language);
 
-            _isLoading = false;
+                _isLoading = false;
+            })
+            {
+                Name = "GUI.ApplicationLibraryThread",
+                IsBackground = true,
+            };
+            applicationLibraryThread.Start();
         }
     }
 }
diff --git a/src/Ryujinx.Ava/UI/Windows/StyleableWindow.cs b/src/Ryujinx.Ava/UI/Windows/StyleableWindow.cs
index 43e42804a..3fedd5a2b 100644
--- a/src/Ryujinx.Ava/UI/Windows/StyleableWindow.cs
+++ b/src/Ryujinx.Ava/UI/Windows/StyleableWindow.cs
@@ -3,7 +3,6 @@ using Avalonia.Controls.Primitives;
 using Avalonia.Media.Imaging;
 using Avalonia.Platform;
 using Ryujinx.Ui.Common.Configuration;
-using System;
 using System.IO;
 using System.Reflection;
 
@@ -25,11 +24,6 @@ namespace Ryujinx.Ava.UI.Windows
             IconImage = new Bitmap(stream);
         }
 
-        protected override void OnOpened(EventArgs e)
-        {
-            base.OnOpened(e);
-        }
-
         protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
         {
             base.OnApplyTemplate(e);
diff --git a/src/Ryujinx.Common/Collections/IntervalTree.cs b/src/Ryujinx.Common/Collections/IntervalTree.cs
index 2ea260a5f..baab579e7 100644
--- a/src/Ryujinx.Common/Collections/IntervalTree.cs
+++ b/src/Ryujinx.Common/Collections/IntervalTree.cs
@@ -192,7 +192,7 @@ namespace Ryujinx.Common.Collections
                     {
                         if (start.CompareTo(overlap.End) < 0)
                         {
-                            if (overlaps.Length >= overlapCount)
+                            if (overlaps.Length <= overlapCount)
                             {
                                 Array.Resize(ref overlaps, overlapCount + ArrayGrowthSize);
                             }
diff --git a/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs b/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs
new file mode 100644
index 000000000..167429433
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs
@@ -0,0 +1,7 @@
+namespace Ryujinx.Common.Configuration.Multiplayer
+{
+    public enum MultiplayerMode
+    {
+        Disabled,
+    }
+}
diff --git a/src/Ryujinx.Common/Memory/StructArrayHelpers.cs b/src/Ryujinx.Common/Memory/StructArrayHelpers.cs
index 1e8976ca4..807bd69c3 100644
--- a/src/Ryujinx.Common/Memory/StructArrayHelpers.cs
+++ b/src/Ryujinx.Common/Memory/StructArrayHelpers.cs
@@ -756,6 +756,18 @@ namespace Ryujinx.Common.Memory
         public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
     }
 
+    public struct Array96<T> : IArray<T> where T : unmanaged
+    {
+        T _e0;
+        Array64<T> _other;
+        Array31<T> _other2;
+        public readonly int Length => 96;
+        public ref T this[int index] => ref AsSpan()[index];
+
+        [Pure]
+        public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
+    }
+
     public struct Array127<T> : IArray<T> where T : unmanaged
     {
         T _e0;
@@ -791,5 +803,34 @@ namespace Ryujinx.Common.Memory
         [Pure]
         public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
     }
+
+    public struct Array140<T> : IArray<T> where T : unmanaged
+    {
+        T _e0;
+        Array64<T> _other;
+        Array64<T> _other2;
+        Array11<T> _other3;
+        public readonly int Length => 140;
+        public ref T this[int index] => ref AsSpan()[index];
+
+        [Pure]
+        public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
+    }
+
+    public struct Array384<T> : IArray<T> where T : unmanaged
+    {
+        T _e0;
+        Array64<T> _other;
+        Array64<T> _other2;
+        Array64<T> _other3;
+        Array64<T> _other4;
+        Array64<T> _other5;
+        Array63<T> _other6;
+        public readonly int Length => 384;
+        public ref T this[int index] => ref AsSpan()[index];
+
+        [Pure]
+        public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
+    }
 }
 #pragma warning restore CS0169, IDE0051
diff --git a/src/Ryujinx.Common/ReactiveObject.cs b/src/Ryujinx.Common/ReactiveObject.cs
index d2624c365..ac7d2c4d8 100644
--- a/src/Ryujinx.Common/ReactiveObject.cs
+++ b/src/Ryujinx.Common/ReactiveObject.cs
@@ -5,7 +5,7 @@ namespace Ryujinx.Common
 {
     public class ReactiveObject<T>
     {
-        private readonly ReaderWriterLock _readerWriterLock = new();
+        private readonly ReaderWriterLockSlim _readerWriterLock = new();
         private bool _isInitialized;
         private T _value;
 
@@ -15,15 +15,15 @@ namespace Ryujinx.Common
         {
             get
             {
-                _readerWriterLock.AcquireReaderLock(Timeout.Infinite);
+                _readerWriterLock.EnterReadLock();
                 T value = _value;
-                _readerWriterLock.ReleaseReaderLock();
+                _readerWriterLock.ExitReadLock();
 
                 return value;
             }
             set
             {
-                _readerWriterLock.AcquireWriterLock(Timeout.Infinite);
+                _readerWriterLock.EnterWriteLock();
 
                 T oldValue = _value;
 
@@ -32,7 +32,7 @@ namespace Ryujinx.Common
                 _isInitialized = true;
                 _value = value;
 
-                _readerWriterLock.ReleaseWriterLock();
+                _readerWriterLock.ExitWriteLock();
 
                 if (!oldIsInitialized || oldValue == null || !oldValue.Equals(_value))
                 {
diff --git a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs
index e48ff2693..78fb342b1 100644
--- a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs
+++ b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs
@@ -1,4 +1,6 @@
-using System.Net.NetworkInformation;
+using System.Buffers.Binary;
+using System.Net;
+using System.Net.NetworkInformation;
 
 namespace Ryujinx.Common.Utilities
 {
@@ -62,5 +64,15 @@ namespace Ryujinx.Common.Utilities
 
             return (targetProperties, targetAddressInfo);
         }
+
+        public static uint ConvertIpv4Address(IPAddress ipAddress)
+        {
+            return BinaryPrimitives.ReadUInt32BigEndian(ipAddress.GetAddressBytes());
+        }
+
+        public static uint ConvertIpv4Address(string ipAddress)
+        {
+            return ConvertIpv4Address(IPAddress.Parse(ipAddress));
+        }
     }
 }
diff --git a/src/Ryujinx.Cpu/AppleHv/HvAddressSpace.cs b/src/Ryujinx.Cpu/AppleHv/HvAddressSpace.cs
index 4785a3f05..eb7c0ef08 100644
--- a/src/Ryujinx.Cpu/AppleHv/HvAddressSpace.cs
+++ b/src/Ryujinx.Cpu/AppleHv/HvAddressSpace.cs
@@ -1,9 +1,11 @@
 using Ryujinx.Cpu.AppleHv.Arm;
 using Ryujinx.Memory;
 using System;
+using System.Runtime.Versioning;
 
 namespace Ryujinx.Cpu.AppleHv
 {
+    [SupportedOSPlatform("macos")]
     class HvAddressSpace : IDisposable
     {
         private const ulong KernelRegionBase = unchecked((ulong)-(1L << 39));
diff --git a/src/Ryujinx.Cpu/AppleHv/HvAddressSpaceRange.cs b/src/Ryujinx.Cpu/AppleHv/HvAddressSpaceRange.cs
index 876334307..7754431fa 100644
--- a/src/Ryujinx.Cpu/AppleHv/HvAddressSpaceRange.cs
+++ b/src/Ryujinx.Cpu/AppleHv/HvAddressSpaceRange.cs
@@ -2,10 +2,12 @@ using Ryujinx.Cpu.AppleHv.Arm;
 using System;
 using System.Diagnostics;
 using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
 using System.Threading;
 
 namespace Ryujinx.Cpu.AppleHv
 {
+    [SupportedOSPlatform("macos")]
     class HvAddressSpaceRange : IDisposable
     {
         private const ulong AllocationGranule = 1UL << 14;
diff --git a/src/Ryujinx.Cpu/AppleHv/HvApi.cs b/src/Ryujinx.Cpu/AppleHv/HvApi.cs
index 4f7484981..e6e08111f 100644
--- a/src/Ryujinx.Cpu/AppleHv/HvApi.cs
+++ b/src/Ryujinx.Cpu/AppleHv/HvApi.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
 
 namespace Ryujinx.Cpu.AppleHv
 {
@@ -12,10 +13,18 @@ namespace Ryujinx.Cpu.AppleHv
 #pragma warning restore CS0649
     }
 
+    enum HvExitReason : uint
+    {
+        Canceled,
+        Exception,
+        VTimerActivated,
+        Unknown,
+    }
+
     struct HvVcpuExit
     {
 #pragma warning disable CS0649 // Field is never assigned to
-        public uint Reason;
+        public HvExitReason Reason;
         public HvVcpuExitException Exception;
 #pragma warning restore CS0649
     }
@@ -255,6 +264,7 @@ namespace Ryujinx.Cpu.AppleHv
         }
     }
 
+    [SupportedOSPlatform("macos")]
     static partial class HvApi
     {
         public const string LibraryName = "/System/Library/Frameworks/Hypervisor.framework/Hypervisor";
diff --git a/src/Ryujinx.Cpu/AppleHv/HvCpuContext.cs b/src/Ryujinx.Cpu/AppleHv/HvCpuContext.cs
index a58f83591..2c4ff2b64 100644
--- a/src/Ryujinx.Cpu/AppleHv/HvCpuContext.cs
+++ b/src/Ryujinx.Cpu/AppleHv/HvCpuContext.cs
@@ -1,7 +1,9 @@
 using ARMeilleure.Memory;
+using System.Runtime.Versioning;
 
 namespace Ryujinx.Cpu.AppleHv
 {
+    [SupportedOSPlatform("macos")]
     class HvCpuContext : ICpuContext
     {
         private readonly ITickSource _tickSource;
diff --git a/src/Ryujinx.Cpu/AppleHv/HvEngine.cs b/src/Ryujinx.Cpu/AppleHv/HvEngine.cs
index 2967857f9..c3c1a4484 100644
--- a/src/Ryujinx.Cpu/AppleHv/HvEngine.cs
+++ b/src/Ryujinx.Cpu/AppleHv/HvEngine.cs
@@ -1,7 +1,9 @@
 using ARMeilleure.Memory;
+using System.Runtime.Versioning;
 
 namespace Ryujinx.Cpu.AppleHv
 {
+    [SupportedOSPlatform("macos")]
     public class HvEngine : ICpuEngine
     {
         private readonly ITickSource _tickSource;
diff --git a/src/Ryujinx.Cpu/AppleHv/HvExecutionContext.cs b/src/Ryujinx.Cpu/AppleHv/HvExecutionContext.cs
index 2c9afdc4e..fc2b76d15 100644
--- a/src/Ryujinx.Cpu/AppleHv/HvExecutionContext.cs
+++ b/src/Ryujinx.Cpu/AppleHv/HvExecutionContext.cs
@@ -2,9 +2,12 @@ using ARMeilleure.State;
 using Ryujinx.Cpu.AppleHv.Arm;
 using Ryujinx.Memory.Tracking;
 using System;
+using System.Runtime.Versioning;
+using System.Threading;
 
 namespace Ryujinx.Cpu.AppleHv
 {
+    [SupportedOSPlatform("macos")]
     class HvExecutionContext : IExecutionContext
     {
         /// <inheritdoc/>
@@ -67,6 +70,8 @@ namespace Ryujinx.Cpu.AppleHv
 
         private readonly ExceptionCallbacks _exceptionCallbacks;
 
+        private int _interruptRequested;
+
         public HvExecutionContext(ICounter counter, ExceptionCallbacks exceptionCallbacks)
         {
             _counter = counter;
@@ -111,7 +116,15 @@ namespace Ryujinx.Cpu.AppleHv
         /// <inheritdoc/>
         public void RequestInterrupt()
         {
-            _impl.RequestInterrupt();
+            if (Interlocked.Exchange(ref _interruptRequested, 1) == 0 && _impl is HvExecutionContextVcpu impl)
+            {
+                impl.RequestInterrupt();
+            }
+        }
+
+        private bool GetAndClearInterruptRequested()
+        {
+            return Interlocked.Exchange(ref _interruptRequested, 0) != 0;
         }
 
         /// <inheritdoc/>
@@ -131,9 +144,9 @@ namespace Ryujinx.Cpu.AppleHv
             {
                 HvApi.hv_vcpu_run(vcpu.Handle).ThrowOnError();
 
-                uint reason = vcpu.ExitInfo->Reason;
+                HvExitReason reason = vcpu.ExitInfo->Reason;
 
-                if (reason == 1)
+                if (reason == HvExitReason.Exception)
                 {
                     uint hvEsr = (uint)vcpu.ExitInfo->Exception.Syndrome;
                     ExceptionClass hvEc = (ExceptionClass)(hvEsr >> 26);
@@ -146,14 +159,22 @@ namespace Ryujinx.Cpu.AppleHv
                     address = SynchronousException(memoryManager, ref vcpu);
                     HvApi.hv_vcpu_set_reg(vcpu.Handle, HvReg.PC, address).ThrowOnError();
                 }
-                else if (reason == 0)
+                else if (reason == HvExitReason.Canceled || reason == HvExitReason.VTimerActivated)
                 {
-                    if (_impl.GetAndClearInterruptRequested())
+                    if (GetAndClearInterruptRequested())
                     {
                         ReturnToPool(vcpu);
                         InterruptHandler();
                         vcpu = RentFromPool(memoryManager.AddressSpace, vcpu);
                     }
+
+                    if (reason == HvExitReason.VTimerActivated)
+                    {
+                        vcpu.EnableAndUpdateVTimer();
+
+                        // Unmask VTimer interrupts.
+                        HvApi.hv_vcpu_set_vtimer_mask(vcpu.Handle, false).ThrowOnError();
+                    }
                 }
                 else
                 {
diff --git a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextShadow.cs b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextShadow.cs
index 78ffcbe4b..6ce8e1800 100644
--- a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextShadow.cs
+++ b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextShadow.cs
@@ -46,14 +46,5 @@ namespace Ryujinx.Cpu.AppleHv
         {
             _v[index] = value;
         }
-
-        public void RequestInterrupt()
-        {
-        }
-
-        public bool GetAndClearInterruptRequested()
-        {
-            return false;
-        }
     }
 }
diff --git a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs
index d9ad637f8..bb232940d 100644
--- a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs
+++ b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs
@@ -2,10 +2,11 @@ using ARMeilleure.State;
 using Ryujinx.Memory;
 using System;
 using System.Runtime.InteropServices;
-using System.Threading;
+using System.Runtime.Versioning;
 
 namespace Ryujinx.Cpu.AppleHv
 {
+    [SupportedOSPlatform("macos")]
     class HvExecutionContextVcpu : IHvExecutionContext
     {
         private static readonly MemoryBlock _setSimdFpRegFuncMem;
@@ -135,7 +136,6 @@ namespace Ryujinx.Cpu.AppleHv
         }
 
         private readonly ulong _vcpu;
-        private int _interruptRequested;
 
         public HvExecutionContextVcpu(ulong vcpu)
         {
@@ -181,16 +181,8 @@ namespace Ryujinx.Cpu.AppleHv
 
         public void RequestInterrupt()
         {
-            if (Interlocked.Exchange(ref _interruptRequested, 1) == 0)
-            {
-                ulong vcpu = _vcpu;
-                HvApi.hv_vcpus_exit(ref vcpu, 1);
-            }
-        }
-
-        public bool GetAndClearInterruptRequested()
-        {
-            return Interlocked.Exchange(ref _interruptRequested, 0) != 0;
+            ulong vcpu = _vcpu;
+            HvApi.hv_vcpus_exit(ref vcpu, 1);
         }
     }
 }
diff --git a/src/Ryujinx.Cpu/AppleHv/HvMemoryBlockAllocation.cs b/src/Ryujinx.Cpu/AppleHv/HvMemoryBlockAllocation.cs
index 3c3f087ab..855d313c5 100644
--- a/src/Ryujinx.Cpu/AppleHv/HvMemoryBlockAllocation.cs
+++ b/src/Ryujinx.Cpu/AppleHv/HvMemoryBlockAllocation.cs
@@ -1,8 +1,10 @@
 using Ryujinx.Memory;
 using System;
+using System.Runtime.Versioning;
 
 namespace Ryujinx.Cpu.AppleHv
 {
+    [SupportedOSPlatform("macos")]
     readonly struct HvMemoryBlockAllocation : IDisposable
     {
         private readonly HvMemoryBlockAllocator _owner;
diff --git a/src/Ryujinx.Cpu/AppleHv/HvMemoryBlockAllocator.cs b/src/Ryujinx.Cpu/AppleHv/HvMemoryBlockAllocator.cs
index ac184cb9a..4e3723d55 100644
--- a/src/Ryujinx.Cpu/AppleHv/HvMemoryBlockAllocator.cs
+++ b/src/Ryujinx.Cpu/AppleHv/HvMemoryBlockAllocator.cs
@@ -1,7 +1,9 @@
 using Ryujinx.Memory;
+using System.Runtime.Versioning;
 
 namespace Ryujinx.Cpu.AppleHv
 {
+    [SupportedOSPlatform("macos")]
     class HvMemoryBlockAllocator : PrivateMemoryAllocatorImpl<HvMemoryBlockAllocator.Block>
     {
         public class Block : PrivateMemoryAllocator.Block
diff --git a/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs b/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs
index 01c685d4b..d5ce817a4 100644
--- a/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs
+++ b/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs
@@ -7,6 +7,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
 using System.Threading;
 
 namespace Ryujinx.Cpu.AppleHv
@@ -14,6 +15,7 @@ namespace Ryujinx.Cpu.AppleHv
     /// <summary>
     /// Represents a CPU memory manager which maps guest virtual memory directly onto the Hypervisor page table.
     /// </summary>
+    [SupportedOSPlatform("macos")]
     public class HvMemoryManager : MemoryManagerBase, IMemoryManager, IVirtualMemoryManagerTracked, IWritableBlock
     {
         public const int PageBits = 12;
diff --git a/src/Ryujinx.Cpu/AppleHv/HvVcpu.cs b/src/Ryujinx.Cpu/AppleHv/HvVcpu.cs
index 9c2cc0ff3..ee91c478b 100644
--- a/src/Ryujinx.Cpu/AppleHv/HvVcpu.cs
+++ b/src/Ryujinx.Cpu/AppleHv/HvVcpu.cs
@@ -1,7 +1,15 @@
+using System.Diagnostics;
+using System.Runtime.Versioning;
+
 namespace Ryujinx.Cpu.AppleHv
 {
+    [SupportedOSPlatform("macos")]
     unsafe class HvVcpu
     {
+        private const ulong InterruptIntervalNs = 16 * 1000000; // 16 ms
+
+        private static ulong _interruptTimeDeltaTicks = 0;
+
         public readonly ulong Handle;
         public readonly HvVcpuExit* ExitInfo;
         public readonly IHvExecutionContext ShadowContext;
@@ -21,5 +29,28 @@ namespace Ryujinx.Cpu.AppleHv
             NativeContext = nativeContext;
             IsEphemeral = isEphemeral;
         }
+
+        public void EnableAndUpdateVTimer()
+        {
+            // We need to ensure interrupts will be serviced,
+            // and for that we set up the VTime to trigger an interrupt at fixed intervals.
+
+            ulong deltaTicks = _interruptTimeDeltaTicks;
+
+            if (deltaTicks == 0)
+            {
+                // Calculate our time delta in ticks based on the current clock frequency.
+
+                int result = TimeApi.mach_timebase_info(out var timeBaseInfo);
+
+                Debug.Assert(result == 0);
+
+                deltaTicks = ((InterruptIntervalNs * timeBaseInfo.Denom) + (timeBaseInfo.Numer - 1)) / timeBaseInfo.Numer;
+                _interruptTimeDeltaTicks = deltaTicks;
+            }
+
+            HvApi.hv_vcpu_set_sys_reg(Handle, HvSysReg.CNTV_CTL_EL0, 1).ThrowOnError();
+            HvApi.hv_vcpu_set_sys_reg(Handle, HvSysReg.CNTV_CVAL_EL0, TimeApi.mach_absolute_time() + deltaTicks).ThrowOnError();
+        }
     }
 }
diff --git a/src/Ryujinx.Cpu/AppleHv/HvVcpuPool.cs b/src/Ryujinx.Cpu/AppleHv/HvVcpuPool.cs
index fe01dce3c..2edcd7e4e 100644
--- a/src/Ryujinx.Cpu/AppleHv/HvVcpuPool.cs
+++ b/src/Ryujinx.Cpu/AppleHv/HvVcpuPool.cs
@@ -1,8 +1,10 @@
 using System;
+using System.Runtime.Versioning;
 using System.Threading;
 
 namespace Ryujinx.Cpu.AppleHv
 {
+    [SupportedOSPlatform("macos")]
     class HvVcpuPool
     {
         // Since there's a limit on the number of VCPUs we can create,
@@ -81,6 +83,8 @@ namespace Ryujinx.Cpu.AppleHv
 
             HvVcpu vcpu = new(vcpuHandle, exitInfo, shadowContext, nativeContext, isEphemeral);
 
+            vcpu.EnableAndUpdateVTimer();
+
             return vcpu;
         }
 
diff --git a/src/Ryujinx.Cpu/AppleHv/HvVm.cs b/src/Ryujinx.Cpu/AppleHv/HvVm.cs
index 1f15022fd..c4f107532 100644
--- a/src/Ryujinx.Cpu/AppleHv/HvVm.cs
+++ b/src/Ryujinx.Cpu/AppleHv/HvVm.cs
@@ -1,8 +1,10 @@
 using Ryujinx.Memory;
 using System;
+using System.Runtime.Versioning;
 
 namespace Ryujinx.Cpu.AppleHv
 {
+    [SupportedOSPlatform("macos")]
     static class HvVm
     {
         // This alignment allows us to use larger blocks on the page table.
diff --git a/src/Ryujinx.Cpu/AppleHv/IHvExecutionContext.cs b/src/Ryujinx.Cpu/AppleHv/IHvExecutionContext.cs
index 7990ab72a..54b73acc6 100644
--- a/src/Ryujinx.Cpu/AppleHv/IHvExecutionContext.cs
+++ b/src/Ryujinx.Cpu/AppleHv/IHvExecutionContext.cs
@@ -2,7 +2,7 @@ using ARMeilleure.State;
 
 namespace Ryujinx.Cpu.AppleHv
 {
-    public interface IHvExecutionContext
+    interface IHvExecutionContext
     {
         ulong Pc { get; set; }
         ulong ElrEl1 { get; set; }
@@ -39,8 +39,5 @@ namespace Ryujinx.Cpu.AppleHv
                 SetV(i, context.GetV(i));
             }
         }
-
-        void RequestInterrupt();
-        bool GetAndClearInterruptRequested();
     }
 }
diff --git a/src/Ryujinx.Cpu/AppleHv/TimeApi.cs b/src/Ryujinx.Cpu/AppleHv/TimeApi.cs
new file mode 100644
index 000000000..85bc77178
--- /dev/null
+++ b/src/Ryujinx.Cpu/AppleHv/TimeApi.cs
@@ -0,0 +1,21 @@
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Cpu.AppleHv
+{
+    struct MachTimebaseInfo
+    {
+        public uint Numer;
+        public uint Denom;
+    }
+
+    [SupportedOSPlatform("macos")]
+    static partial class TimeApi
+    {
+        [LibraryImport("libc", SetLastError = true)]
+        public static partial ulong mach_absolute_time();
+
+        [LibraryImport("libc", SetLastError = true)]
+        public static partial int mach_timebase_info(out MachTimebaseInfo info);
+    }
+}
diff --git a/src/Ryujinx.Graphics.GAL/Capabilities.cs b/src/Ryujinx.Graphics.GAL/Capabilities.cs
index 523293d25..769967d63 100644
--- a/src/Ryujinx.Graphics.GAL/Capabilities.cs
+++ b/src/Ryujinx.Graphics.GAL/Capabilities.cs
@@ -39,6 +39,7 @@ namespace Ryujinx.Graphics.GAL
         public readonly bool SupportsShaderBarrierDivergence;
         public readonly bool SupportsShaderFloat64;
         public readonly bool SupportsTextureShadowLod;
+        public readonly bool SupportsVertexStoreAndAtomics;
         public readonly bool SupportsViewportIndexVertexTessellation;
         public readonly bool SupportsViewportMask;
         public readonly bool SupportsViewportSwizzle;
@@ -53,7 +54,9 @@ namespace Ryujinx.Graphics.GAL
 
         public readonly int MaximumComputeSharedMemorySize;
         public readonly float MaximumSupportedAnisotropy;
+        public readonly int ShaderSubgroupSize;
         public readonly int StorageBufferOffsetAlignment;
+        public readonly int TextureBufferOffsetAlignment;
 
         public readonly int GatherBiasPrecision;
 
@@ -91,6 +94,7 @@ namespace Ryujinx.Graphics.GAL
             bool supportsShaderBarrierDivergence,
             bool supportsShaderFloat64,
             bool supportsTextureShadowLod,
+            bool supportsVertexStoreAndAtomics,
             bool supportsViewportIndexVertexTessellation,
             bool supportsViewportMask,
             bool supportsViewportSwizzle,
@@ -103,7 +107,9 @@ namespace Ryujinx.Graphics.GAL
             uint maximumImagesPerStage,
             int maximumComputeSharedMemorySize,
             float maximumSupportedAnisotropy,
+            int shaderSubgroupSize,
             int storageBufferOffsetAlignment,
+            int textureBufferOffsetAlignment,
             int gatherBiasPrecision)
         {
             Api = api;
@@ -139,6 +145,7 @@ namespace Ryujinx.Graphics.GAL
             SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence;
             SupportsShaderFloat64 = supportsShaderFloat64;
             SupportsTextureShadowLod = supportsTextureShadowLod;
+            SupportsVertexStoreAndAtomics = supportsVertexStoreAndAtomics;
             SupportsViewportIndexVertexTessellation = supportsViewportIndexVertexTessellation;
             SupportsViewportMask = supportsViewportMask;
             SupportsViewportSwizzle = supportsViewportSwizzle;
@@ -151,7 +158,9 @@ namespace Ryujinx.Graphics.GAL
             MaximumImagesPerStage = maximumImagesPerStage;
             MaximumComputeSharedMemorySize = maximumComputeSharedMemorySize;
             MaximumSupportedAnisotropy = maximumSupportedAnisotropy;
+            ShaderSubgroupSize = shaderSubgroupSize;
             StorageBufferOffsetAlignment = storageBufferOffsetAlignment;
+            TextureBufferOffsetAlignment = textureBufferOffsetAlignment;
             GatherBiasPrecision = gatherBiasPrecision;
         }
     }
diff --git a/src/Ryujinx.Graphics.GAL/Format.cs b/src/Ryujinx.Graphics.GAL/Format.cs
index f6feec1c7..99c89dcec 100644
--- a/src/Ryujinx.Graphics.GAL/Format.cs
+++ b/src/Ryujinx.Graphics.GAL/Format.cs
@@ -335,6 +335,45 @@ namespace Ryujinx.Graphics.GAL
             return 1;
         }
 
+        /// <summary>
+        /// Checks if the texture format is a depth or depth-stencil format.
+        /// </summary>
+        /// <param name="format">Texture format</param>
+        /// <returns>True if the format is a depth or depth-stencil format, false otherwise</returns>
+        public static bool HasDepth(this Format format)
+        {
+            switch (format)
+            {
+                case Format.D16Unorm:
+                case Format.D24UnormS8Uint:
+                case Format.S8UintD24Unorm:
+                case Format.D32Float:
+                case Format.D32FloatS8Uint:
+                    return true;
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Checks if the texture format is a stencil or depth-stencil format.
+        /// </summary>
+        /// <param name="format">Texture format</param>
+        /// <returns>True if the format is a stencil or depth-stencil format, false otherwise</returns>
+        public static bool HasStencil(this Format format)
+        {
+            switch (format)
+            {
+                case Format.D24UnormS8Uint:
+                case Format.S8UintD24Unorm:
+                case Format.D32FloatS8Uint:
+                case Format.S8Uint:
+                    return true;
+            }
+
+            return false;
+        }
+
         /// <summary>
         /// Checks if the texture format is valid to use as image format.
         /// </summary>
diff --git a/src/Ryujinx.Graphics.GAL/ResourceLayout.cs b/src/Ryujinx.Graphics.GAL/ResourceLayout.cs
index 200292eee..84bca5b41 100644
--- a/src/Ryujinx.Graphics.GAL/ResourceLayout.cs
+++ b/src/Ryujinx.Graphics.GAL/ResourceLayout.cs
@@ -15,14 +15,6 @@ namespace Ryujinx.Graphics.GAL
         BufferImage,
     }
 
-    public enum ResourceAccess : byte
-    {
-        None = 0,
-        Read = 1,
-        Write = 2,
-        ReadWrite = Read | Write,
-    }
-
     [Flags]
     public enum ResourceStages : byte
     {
@@ -81,19 +73,17 @@ namespace Ryujinx.Graphics.GAL
         public int Binding { get; }
         public ResourceType Type { get; }
         public ResourceStages Stages { get; }
-        public ResourceAccess Access { get; }
 
-        public ResourceUsage(int binding, ResourceType type, ResourceStages stages, ResourceAccess access)
+        public ResourceUsage(int binding, ResourceType type, ResourceStages stages)
         {
             Binding = binding;
             Type = type;
             Stages = stages;
-            Access = access;
         }
 
         public override int GetHashCode()
         {
-            return HashCode.Combine(Binding, Type, Stages, Access);
+            return HashCode.Combine(Binding, Type, Stages);
         }
 
         public override bool Equals(object obj)
@@ -103,7 +93,7 @@ namespace Ryujinx.Graphics.GAL
 
         public bool Equals(ResourceUsage other)
         {
-            return Binding == other.Binding && Type == other.Type && Stages == other.Stages && Access == other.Access;
+            return Binding == other.Binding && Type == other.Type && Stages == other.Stages;
         }
 
         public static bool operator ==(ResourceUsage left, ResourceUsage right)
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/DeviceStateWithShadow.cs b/src/Ryujinx.Graphics.Gpu/Engine/DeviceStateWithShadow.cs
index 74a9aa049..a2e5b1164 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/DeviceStateWithShadow.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/DeviceStateWithShadow.cs
@@ -32,6 +32,11 @@ namespace Ryujinx.Graphics.Gpu.Engine
         /// </summary>
         public ref TState State => ref _state.State;
 
+        /// <summary>
+        /// Current shadow state.
+        /// </summary>
+        public ref TState ShadowState => ref _shadowState.State;
+
         /// <summary>
         /// Creates a new instance of the device state, with shadow state.
         /// </summary>
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoProcessor.cs b/src/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoProcessor.cs
index 6ba1bc22e..180e2a6b6 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoProcessor.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoProcessor.cs
@@ -13,7 +13,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.GPFifo
     /// <summary>
     /// Represents a GPU General Purpose FIFO command processor.
     /// </summary>
-    class GPFifoProcessor
+    class GPFifoProcessor : IDisposable
     {
         private const int MacrosCount = 0x80;
         private const int MacroIndexMask = MacrosCount - 1;
@@ -327,5 +327,19 @@ namespace Ryujinx.Graphics.Gpu.Engine.GPFifo
         {
             _3dClass.PerformDeferredDraws();
         }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                _3dClass.Dispose();
+            }
+        }
+
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
     }
 }
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLE.cs b/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLE.cs
index a4c4dd106..7d9e1ec02 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLE.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLE.cs
@@ -1,7 +1,10 @@
 using Ryujinx.Common.Logging;
+using Ryujinx.Common.Memory;
 using Ryujinx.Graphics.Device;
 using Ryujinx.Graphics.GAL;
 using Ryujinx.Graphics.Gpu.Engine.GPFifo;
+using Ryujinx.Graphics.Gpu.Engine.Threed;
+using Ryujinx.Graphics.Gpu.Engine.Types;
 using System;
 using System.Collections.Generic;
 
@@ -15,9 +18,18 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
         private const int ColorLayerCountOffset = 0x818;
         private const int ColorStructSize = 0x40;
         private const int ZetaLayerCountOffset = 0x1230;
+        private const int UniformBufferBindVertexOffset = 0x2410;
+        private const int FirstVertexOffset = 0x1434;
 
         private const int IndirectIndexedDataEntrySize = 0x14;
 
+        private const int LogicOpOffset = 0x19c4;
+        private const int ShaderIdScratchOffset = 0x3470;
+        private const int ShaderAddressScratchOffset = 0x3488;
+        private const int UpdateConstantBufferAddressesBase = 0x34a8;
+        private const int UpdateConstantBufferSizesBase = 0x34bc;
+        private const int UpdateConstantBufferAddressCbu = 0x3460;
+
         private readonly GPFifoProcessor _processor;
         private readonly MacroHLEFunctionName _functionName;
 
@@ -49,6 +61,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
         {
             switch (_functionName)
             {
+                case MacroHLEFunctionName.BindShaderProgram:
+                    BindShaderProgram(state, arg0);
+                    break;
                 case MacroHLEFunctionName.ClearColor:
                     ClearColor(state, arg0);
                     break;
@@ -58,6 +73,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
                 case MacroHLEFunctionName.DrawArraysInstanced:
                     DrawArraysInstanced(state, arg0);
                     break;
+                case MacroHLEFunctionName.DrawElements:
+                    DrawElements(state, arg0);
+                    break;
                 case MacroHLEFunctionName.DrawElementsInstanced:
                     DrawElementsInstanced(state, arg0);
                     break;
@@ -67,6 +85,21 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
                 case MacroHLEFunctionName.MultiDrawElementsIndirectCount:
                     MultiDrawElementsIndirectCount(state, arg0);
                     break;
+                case MacroHLEFunctionName.UpdateBlendState:
+                    UpdateBlendState(state, arg0);
+                    break;
+                case MacroHLEFunctionName.UpdateColorMasks:
+                    UpdateColorMasks(state, arg0);
+                    break;
+                case MacroHLEFunctionName.UpdateUniformBufferState:
+                    UpdateUniformBufferState(state, arg0);
+                    break;
+                case MacroHLEFunctionName.UpdateUniformBufferStateCbu:
+                    UpdateUniformBufferStateCbu(state, arg0);
+                    break;
+                case MacroHLEFunctionName.UpdateUniformBufferStateCbuV2:
+                    UpdateUniformBufferStateCbuV2(state, arg0);
+                    break;
                 default:
                     throw new NotImplementedException(_functionName.ToString());
             }
@@ -75,6 +108,149 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
             Fifo.Clear();
         }
 
+        /// <summary>
+        /// Binds a shader program with the index in arg0.
+        /// </summary>
+        /// <param name="state">GPU state at the time of the call</param>
+        /// <param name="arg0">First argument of the call</param>
+        private void BindShaderProgram(IDeviceState state, int arg0)
+        {
+            int scratchOffset = ShaderIdScratchOffset + arg0 * 4;
+
+            int lastId = state.Read(scratchOffset);
+            int id = FetchParam().Word;
+            int offset = FetchParam().Word;
+
+            if (lastId == id)
+            {
+                FetchParam();
+                FetchParam();
+
+                return;
+            }
+
+            _processor.ThreedClass.SetShaderOffset(arg0, (uint)offset);
+
+            // Removes overflow on the method address into the increment portion.
+            // Present in the original macro.
+            int addrMask = unchecked((int)0xfffc0fff) << 2;
+
+            state.Write(scratchOffset & addrMask, id);
+            state.Write((ShaderAddressScratchOffset + arg0 * 4) & addrMask, offset);
+
+            int stage = FetchParam().Word;
+            uint cbAddress = (uint)FetchParam().Word;
+
+            _processor.ThreedClass.UpdateUniformBufferState(65536, cbAddress >> 24, cbAddress << 8);
+
+            int stageOffset = (stage & 0x7f) << 3;
+
+            state.Write((UniformBufferBindVertexOffset + stageOffset * 4) & addrMask, 17);
+        }
+
+        /// <summary>
+        /// Updates uniform buffer state for update or bind.
+        /// </summary>
+        /// <param name="state">GPU state at the time of the call</param>
+        /// <param name="arg0">First argument of the call</param>
+        private void UpdateUniformBufferState(IDeviceState state, int arg0)
+        {
+            uint address = (uint)state.Read(UpdateConstantBufferAddressesBase + arg0 * 4);
+            int size = state.Read(UpdateConstantBufferSizesBase + arg0 * 4);
+
+            _processor.ThreedClass.UpdateUniformBufferState(size, address >> 24, address << 8);
+        }
+
+        /// <summary>
+        /// Updates uniform buffer state for update.
+        /// </summary>
+        /// <param name="state">GPU state at the time of the call</param>
+        /// <param name="arg0">First argument of the call</param>
+        private void UpdateUniformBufferStateCbu(IDeviceState state, int arg0)
+        {
+            uint address = (uint)state.Read(UpdateConstantBufferAddressCbu);
+
+            UniformBufferState ubState = new()
+            {
+                Address = new()
+                {
+                    High = address >> 24,
+                    Low = address << 8
+                },
+                Size = 24320,
+                Offset = arg0 << 2
+            };
+
+            _processor.ThreedClass.UpdateUniformBufferState(ubState);
+        }
+
+        /// <summary>
+        /// Updates uniform buffer state for update.
+        /// </summary>
+        /// <param name="state">GPU state at the time of the call</param>
+        /// <param name="arg0">First argument of the call</param>
+        private void UpdateUniformBufferStateCbuV2(IDeviceState state, int arg0)
+        {
+            uint address = (uint)state.Read(UpdateConstantBufferAddressCbu);
+
+            UniformBufferState ubState = new()
+            {
+                Address = new()
+                {
+                    High = address >> 24,
+                    Low = address << 8
+                },
+                Size = 28672,
+                Offset = arg0 << 2
+            };
+
+            _processor.ThreedClass.UpdateUniformBufferState(ubState);
+        }
+
+        /// <summary>
+        /// Updates blend enable using the given argument.
+        /// </summary>
+        /// <param name="state">GPU state at the time of the call</param>
+        /// <param name="arg0">First argument of the call</param>
+        private void UpdateBlendState(IDeviceState state, int arg0)
+        {
+            state.Write(LogicOpOffset, 0);
+
+            Array8<Boolean32> enable = new();
+
+            for (int i = 0; i < 8; i++)
+            {
+                enable[i] = new Boolean32((uint)(arg0 >> (i + 8)) & 1);
+            }
+
+            _processor.ThreedClass.UpdateBlendEnable(ref enable);
+        }
+
+        /// <summary>
+        /// Updates color masks using the given argument and three pushed arguments.
+        /// </summary>
+        /// <param name="state">GPU state at the time of the call</param>
+        /// <param name="arg0">First argument of the call</param>
+        private void UpdateColorMasks(IDeviceState state, int arg0)
+        {
+            Array8<RtColorMask> masks = new();
+
+            int index = 0;
+
+            for (int i = 0; i < 4; i++)
+            {
+                masks[index++] = new RtColorMask((uint)arg0 & 0x1fff);
+                masks[index++] = new RtColorMask(((uint)arg0 >> 16) & 0x1fff);
+
+                if (i != 3)
+                {
+                    arg0 = FetchParam().Word;
+                }
+            }
+
+            _processor.ThreedClass.UpdateColorMasks(ref masks);
+        }
+
         /// <summary>
         /// Clears one bound color target.
         /// </summary>
@@ -129,6 +305,36 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
                 indexed: false);
         }
 
+        /// <summary>
+        /// Performs a indexed draw.
+        /// </summary>
+        /// <param name="state">GPU state at the time of the call</param>
+        /// <param name="arg0">First argument of the call</param>
+        private void DrawElements(IDeviceState state, int arg0)
+        {
+            var topology = (PrimitiveTopology)arg0;
+
+            var indexAddressHigh = FetchParam();
+            var indexAddressLow = FetchParam();
+            var indexType = FetchParam();
+            var firstIndex = 0;
+            var indexCount = FetchParam();
+
+            _processor.ThreedClass.UpdateIndexBuffer(
+                (uint)indexAddressHigh.Word,
+                (uint)indexAddressLow.Word,
+                (IndexType)indexType.Word);
+
+            _processor.ThreedClass.Draw(
+                topology,
+                indexCount.Word,
+                1,
+                firstIndex,
+                state.Read(FirstVertexOffset),
+                0,
+                indexed: true);
+        }
+
         /// <summary>
         /// Performs a indexed draw.
         /// </summary>
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLEFunctionName.cs b/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLEFunctionName.cs
index 9e71761b4..8dca52262 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLEFunctionName.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLEFunctionName.cs
@@ -6,11 +6,19 @@
     enum MacroHLEFunctionName
     {
         None,
+        BindShaderProgram,
         ClearColor,
         ClearDepthStencil,
         DrawArraysInstanced,
+        DrawElements,
         DrawElementsInstanced,
         DrawElementsIndirect,
         MultiDrawElementsIndirectCount,
+
+        UpdateBlendState,
+        UpdateColorMasks,
+        UpdateUniformBufferState,
+        UpdateUniformBufferStateCbu,
+        UpdateUniformBufferStateCbuV2
     }
 }
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLETable.cs b/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLETable.cs
index 5630756cb..9a496164d 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLETable.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLETable.cs
@@ -46,12 +46,19 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
 
         private static readonly TableEntry[] _table = new TableEntry[]
         {
+            new(MacroHLEFunctionName.BindShaderProgram, new Hash128(0x5d5efb912369f60b, 0x69131ed5019f08ef), 0x68),
             new(MacroHLEFunctionName.ClearColor, new Hash128(0xA9FB28D1DC43645A, 0xB177E5D2EAE67FB0), 0x28),
             new(MacroHLEFunctionName.ClearDepthStencil, new Hash128(0x1B96CB77D4879F4F, 0x8557032FE0C965FB), 0x24),
             new(MacroHLEFunctionName.DrawArraysInstanced, new Hash128(0x197FB416269DBC26, 0x34288C01DDA82202), 0x48),
+            new(MacroHLEFunctionName.DrawElements, new Hash128(0x3D7F32AE6C2702A7, 0x9353C9F41C1A244D), 0x20),
             new(MacroHLEFunctionName.DrawElementsInstanced, new Hash128(0x1A501FD3D54EC8E0, 0x6CF570CF79DA74D6), 0x5c),
             new(MacroHLEFunctionName.DrawElementsIndirect, new Hash128(0x86A3E8E903AF8F45, 0xD35BBA07C23860A4), 0x7c),
             new(MacroHLEFunctionName.MultiDrawElementsIndirectCount, new Hash128(0x890AF57ED3FB1C37, 0x35D0C95C61F5386F), 0x19C),
+            new(MacroHLEFunctionName.UpdateBlendState, new Hash128(0x40F6D4E7B08D7640, 0x82167BEEAECB959F), 0x28),
+            new(MacroHLEFunctionName.UpdateColorMasks, new Hash128(0x9EE32420B8441DFD, 0x6E7724759A57333E), 0x24),
+            new(MacroHLEFunctionName.UpdateUniformBufferState, new Hash128(0x8EE66706049CB0B0, 0x51C1CF906EC86F7C), 0x20),
+            new(MacroHLEFunctionName.UpdateUniformBufferStateCbu, new Hash128(0xA4592676A3E581A0, 0xA39E77FE19FE04AC), 0x18),
+            new(MacroHLEFunctionName.UpdateUniformBufferStateCbuV2, new Hash128(0x392FA750489983D4, 0x35BACE455155D2C3), 0x18)
         };
 
         /// <summary>
@@ -62,18 +69,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
         /// <returns>True if the host supports the HLE macro, false otherwise</returns>
         private static bool IsMacroHLESupported(Capabilities caps, MacroHLEFunctionName name)
         {
-            if (name == MacroHLEFunctionName.ClearColor ||
-                name == MacroHLEFunctionName.ClearDepthStencil ||
-                name == MacroHLEFunctionName.DrawArraysInstanced ||
-                name == MacroHLEFunctionName.DrawElementsInstanced ||
-                name == MacroHLEFunctionName.DrawElementsIndirect)
-            {
-                return true;
-            }
-            else if (name == MacroHLEFunctionName.MultiDrawElementsIndirectCount)
+            if (name == MacroHLEFunctionName.MultiDrawElementsIndirectCount)
             {
                 return caps.SupportsIndirectParameters;
             }
+            else if (name != MacroHLEFunctionName.None)
+            {
+                return true;
+            }
 
             return false;
         }
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/SetMmeShadowRamControlMode.cs b/src/Ryujinx.Graphics.Gpu/Engine/SetMmeShadowRamControlMode.cs
index ebb0ff33e..b9a5c74a3 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/SetMmeShadowRamControlMode.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/SetMmeShadowRamControlMode.cs
@@ -10,4 +10,22 @@ namespace Ryujinx.Graphics.Gpu.Engine
         MethodPassthrough = 2,
         MethodReplay = 3,
     }
+
+    static class SetMmeShadowRamControlModeExtensions
+    {
+        public static bool IsTrack(this SetMmeShadowRamControlMode mode)
+        {
+            return mode == SetMmeShadowRamControlMode.MethodTrack || mode == SetMmeShadowRamControlMode.MethodTrackWithFilter;
+        }
+
+        public static bool IsPassthrough(this SetMmeShadowRamControlMode mode)
+        {
+            return mode == SetMmeShadowRamControlMode.MethodPassthrough;
+        }
+
+        public static bool IsReplay(this SetMmeShadowRamControlMode mode)
+        {
+            return mode == SetMmeShadowRamControlMode.MethodReplay;
+        }
+    }
 }
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VertexInfoBufferUpdater.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VertexInfoBufferUpdater.cs
new file mode 100644
index 000000000..65f556fcb
--- /dev/null
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VertexInfoBufferUpdater.cs
@@ -0,0 +1,141 @@
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.Gpu.Memory;
+using Ryujinx.Graphics.Shader;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
+{
+    /// <summary>
+    /// Vertex info buffer data updater.
+    /// </summary>
+    class VertexInfoBufferUpdater : BufferUpdater
+    {
+        private VertexInfoBuffer _data;
+
+        /// <summary>
+        /// Creates a new instance of the vertex info buffer updater.
+        /// </summary>
+        /// <param name="renderer">Renderer that the vertex info buffer will be used with</param>
+        public VertexInfoBufferUpdater(IRenderer renderer) : base(renderer)
+        {
+        }
+
+        /// <summary>
+        /// Sets vertex data related counts.
+        /// </summary>
+        /// <param name="vertexCount">Number of vertices used on the draw</param>
+        /// <param name="instanceCount">Number of draw instances</param>
+        /// <param name="firstVertex">Index of the first vertex on the vertex buffer</param>
+        /// <param name="firstInstance">Index of the first instanced vertex on the vertex buffer</param>
+        public void SetVertexCounts(int vertexCount, int instanceCount, int firstVertex, int firstInstance)
+        {
+            if (_data.VertexCounts.X != vertexCount)
+            {
+                _data.VertexCounts.X = vertexCount;
+                MarkDirty(VertexInfoBuffer.VertexCountsOffset, sizeof(int));
+            }
+
+            if (_data.VertexCounts.Y != instanceCount)
+            {
+                _data.VertexCounts.Y = instanceCount;
+                MarkDirty(VertexInfoBuffer.VertexCountsOffset + sizeof(int), sizeof(int));
+            }
+
+            if (_data.VertexCounts.Z != firstVertex)
+            {
+                _data.VertexCounts.Z = firstVertex;
+                MarkDirty(VertexInfoBuffer.VertexCountsOffset + sizeof(int) * 2, sizeof(int));
+            }
+
+            if (_data.VertexCounts.W != firstInstance)
+            {
+                _data.VertexCounts.W = firstInstance;
+                MarkDirty(VertexInfoBuffer.VertexCountsOffset + sizeof(int) * 3, sizeof(int));
+            }
+        }
+
+        /// <summary>
+        /// Sets vertex data related counts.
+        /// </summary>
+        /// <param name="primitivesCount">Number of primitives consumed by the geometry shader</param>
+        public void SetGeometryCounts(int primitivesCount)
+        {
+            if (_data.GeometryCounts.X != primitivesCount)
+            {
+                _data.GeometryCounts.X = primitivesCount;
+                MarkDirty(VertexInfoBuffer.GeometryCountsOffset, sizeof(int));
+            }
+        }
+
+        /// <summary>
+        /// Sets a vertex stride and related data.
+        /// </summary>
+        /// <param name="index">Index of the vertex stride to be updated</param>
+        /// <param name="stride">Stride divided by the component or format size</param>
+        /// <param name="componentCount">Number of components that the format has</param>
+        public void SetVertexStride(int index, int stride, int componentCount)
+        {
+            if (_data.VertexStrides[index].X != stride)
+            {
+                _data.VertexStrides[index].X = stride;
+                MarkDirty(VertexInfoBuffer.VertexStridesOffset + index * Unsafe.SizeOf<Vector4<int>>(), sizeof(int));
+            }
+
+            for (int c = 1; c < 4; c++)
+            {
+                int value = c < componentCount ? 1 : 0;
+
+                ref int currentValue = ref GetElementRef(ref _data.VertexStrides[index], c);
+
+                if (currentValue != value)
+                {
+                    currentValue = value;
+                    MarkDirty(VertexInfoBuffer.VertexStridesOffset + index * Unsafe.SizeOf<Vector4<int>>() + c * sizeof(int), sizeof(int));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Sets a vertex offset and related data.
+        /// </summary>
+        /// <param name="index">Index of the vertex offset to be updated</param>
+        /// <param name="offset">Offset divided by the component or format size</param>
+        /// <param name="divisor">If the draw is instanced, should have the vertex divisor value, otherwise should be zero</param>
+        public void SetVertexOffset(int index, int offset, int divisor)
+        {
+            if (_data.VertexOffsets[index].X != offset)
+            {
+                _data.VertexOffsets[index].X = offset;
+                MarkDirty(VertexInfoBuffer.VertexOffsetsOffset + index * Unsafe.SizeOf<Vector4<int>>(), sizeof(int));
+            }
+
+            if (_data.VertexOffsets[index].Y != divisor)
+            {
+                _data.VertexOffsets[index].Y = divisor;
+                MarkDirty(VertexInfoBuffer.VertexOffsetsOffset + index * Unsafe.SizeOf<Vector4<int>>() + sizeof(int), sizeof(int));
+            }
+        }
+
+        /// <summary>
+        /// Sets the offset of the index buffer.
+        /// </summary>
+        /// <param name="offset">Offset divided by the component size</param>
+        public void SetIndexBufferOffset(int offset)
+        {
+            if (_data.GeometryCounts.W != offset)
+            {
+                _data.GeometryCounts.W = offset;
+                MarkDirty(VertexInfoBuffer.GeometryCountsOffset + sizeof(int) * 3, sizeof(int));
+            }
+        }
+
+        /// <summary>
+        /// Submits all pending buffer updates to the GPU.
+        /// </summary>
+        public void Commit()
+        {
+            Commit(MemoryMarshal.Cast<VertexInfoBuffer, byte>(MemoryMarshal.CreateSpan(ref _data, 1)));
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsCompute.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsCompute.cs
new file mode 100644
index 000000000..cbbfd251d
--- /dev/null
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsCompute.cs
@@ -0,0 +1,96 @@
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.Gpu.Shader;
+using System;
+
+namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
+{
+    /// <summary>
+    /// Vertex, tessellation and geometry as compute shader draw manager.
+    /// </summary>
+    class VtgAsCompute : IDisposable
+    {
+        private readonly GpuContext _context;
+        private readonly GpuChannel _channel;
+        private readonly DeviceStateWithShadow<ThreedClassState> _state;
+        private readonly VtgAsComputeContext _vacContext;
+
+        /// <summary>
+        /// Creates a new instance of the vertex, tessellation and geometry as compute shader draw manager.
+        /// </summary>
+        /// <param name="context">GPU context</param>
+        /// <param name="channel">GPU channel</param>
+        /// <param name="state">3D engine state</param>
+        public VtgAsCompute(GpuContext context, GpuChannel channel, DeviceStateWithShadow<ThreedClassState> state)
+        {
+            _context = context;
+            _channel = channel;
+            _state = state;
+            _vacContext = new(context);
+        }
+
+        /// <summary>
+        /// Emulates the pre-rasterization stages of a draw operation using a compute shader.
+        /// </summary>
+        /// <param name="engine">3D engine</param>
+        /// <param name="vertexAsCompute">Vertex shader converted to compute</param>
+        /// <param name="geometryAsCompute">Optional geometry shader converted to compute</param>
+        /// <param name="vertexPassthroughProgram">Fragment shader with a vertex passthrough shader to feed the compute output into the fragment stage</param>
+        /// <param name="topology">Primitive topology of the draw</param>
+        /// <param name="count">Index or vertex count of the draw</param>
+        /// <param name="instanceCount">Instance count</param>
+        /// <param name="firstIndex">First index on the index buffer, for indexed draws</param>
+        /// <param name="firstVertex">First vertex on the vertex buffer</param>
+        /// <param name="firstInstance">First instance</param>
+        /// <param name="indexed">Whether the draw is indexed</param>
+        public void DrawAsCompute(
+            ThreedClass engine,
+            ShaderAsCompute vertexAsCompute,
+            ShaderAsCompute geometryAsCompute,
+            IProgram vertexPassthroughProgram,
+            PrimitiveTopology topology,
+            int count,
+            int instanceCount,
+            int firstIndex,
+            int firstVertex,
+            int firstInstance,
+            bool indexed)
+        {
+            VtgAsComputeState state = new(
+                _context,
+                _channel,
+                _state,
+                _vacContext,
+                engine,
+                vertexAsCompute,
+                geometryAsCompute,
+                vertexPassthroughProgram,
+                topology,
+                count,
+                instanceCount,
+                firstIndex,
+                firstVertex,
+                firstInstance,
+                indexed);
+
+            state.RunVertex();
+            state.RunGeometry();
+            state.RunFragment();
+
+            _vacContext.FreeBuffers();
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                _vacContext.Dispose();
+            }
+        }
+
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs
new file mode 100644
index 000000000..f9cb40b0d
--- /dev/null
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs
@@ -0,0 +1,651 @@
+using Ryujinx.Common;
+using Ryujinx.Graphics.GAL;
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
+{
+    /// <summary>
+    /// Vertex, tessellation and geometry as compute shader context.
+    /// </summary>
+    class VtgAsComputeContext : IDisposable
+    {
+        private const int DummyBufferSize = 16;
+
+        private readonly GpuContext _context;
+
+        /// <summary>
+        /// Cache of buffer textures used for vertex and index buffers.
+        /// </summary>
+        private class BufferTextureCache : IDisposable
+        {
+            private readonly Dictionary<Format, ITexture> _cache;
+
+            /// <summary>
+            /// Creates a new instance of the buffer texture cache.
+            /// </summary>
+            public BufferTextureCache()
+            {
+                _cache = new();
+            }
+
+            /// <summary>
+            /// Gets a cached or creates and caches a buffer texture with the specified format.
+            /// </summary>
+            /// <param name="renderer">Renderer where the texture will be used</param>
+            /// <param name="format">Format of the buffer texture</param>
+            /// <returns>Buffer texture</returns>
+            public ITexture Get(IRenderer renderer, Format format)
+            {
+                if (!_cache.TryGetValue(format, out ITexture bufferTexture))
+                {
+                    bufferTexture = renderer.CreateTexture(new TextureCreateInfo(
+                        1,
+                        1,
+                        1,
+                        1,
+                        1,
+                        1,
+                        1,
+                        1,
+                        format,
+                        DepthStencilMode.Depth,
+                        Target.TextureBuffer,
+                        SwizzleComponent.Red,
+                        SwizzleComponent.Green,
+                        SwizzleComponent.Blue,
+                        SwizzleComponent.Alpha));
+
+                    _cache.Add(format, bufferTexture);
+                }
+
+                return bufferTexture;
+            }
+
+            protected virtual void Dispose(bool disposing)
+            {
+                if (disposing)
+                {
+                    foreach (var texture in _cache.Values)
+                    {
+                        texture.Release();
+                    }
+
+                    _cache.Clear();
+                }
+            }
+
+            public void Dispose()
+            {
+                Dispose(true);
+                GC.SuppressFinalize(this);
+            }
+        }
+
+        /// <summary>
+        /// Buffer state.
+        /// </summary>
+        private struct Buffer
+        {
+            /// <summary>
+            /// Buffer handle.
+            /// </summary>
+            public BufferHandle Handle;
+
+            /// <summary>
+            /// Current free buffer offset.
+            /// </summary>
+            public int Offset;
+
+            /// <summary>
+            /// Total buffer size in bytes.
+            /// </summary>
+            public int Size;
+        }
+
+        /// <summary>
+        /// Index buffer state.
+        /// </summary>
+        private readonly struct IndexBuffer
+        {
+            /// <summary>
+            /// Buffer handle.
+            /// </summary>
+            public BufferHandle Handle { get; }
+
+            /// <summary>
+            /// Index count.
+            /// </summary>
+            public int Count { get; }
+
+            /// <summary>
+            /// Size in bytes.
+            /// </summary>
+            public int Size { get; }
+
+            /// <summary>
+            /// Creates a new index buffer state.
+            /// </summary>
+            /// <param name="handle">Buffer handle</param>
+            /// <param name="count">Index count</param>
+            /// <param name="size">Size in bytes</param>
+            public IndexBuffer(BufferHandle handle, int count, int size)
+            {
+                Handle = handle;
+                Count = count;
+                Size = size;
+            }
+
+            /// <summary>
+            /// Creates a full range starting from the beggining of the buffer.
+            /// </summary>
+            /// <returns>Range</returns>
+            public readonly BufferRange ToRange()
+            {
+                return new BufferRange(Handle, 0, Size);
+            }
+
+            /// <summary>
+            /// Creates a range starting from the beggining of the buffer, with the specified size.
+            /// </summary>
+            /// <param name="size">Size in bytes of the range</param>
+            /// <returns>Range</returns>
+            public readonly BufferRange ToRange(int size)
+            {
+                return new BufferRange(Handle, 0, size);
+            }
+        }
+
+        private readonly BufferTextureCache[] _bufferTextures;
+        private BufferHandle _dummyBuffer;
+        private Buffer _vertexDataBuffer;
+        private Buffer _geometryVertexDataBuffer;
+        private Buffer _geometryIndexDataBuffer;
+        private BufferHandle _sequentialIndexBuffer;
+        private int _sequentialIndexBufferCount;
+
+        private readonly Dictionary<PrimitiveTopology, IndexBuffer> _topologyRemapBuffers;
+
+        /// <summary>
+        /// Vertex information buffer updater.
+        /// </summary>
+        public VertexInfoBufferUpdater VertexInfoBufferUpdater { get; }
+
+        /// <summary>
+        /// Creates a new instance of the vertex, tessellation and geometry as compute shader context.
+        /// </summary>
+        /// <param name="context"></param>
+        public VtgAsComputeContext(GpuContext context)
+        {
+            _context = context;
+            _bufferTextures = new BufferTextureCache[Constants.TotalVertexBuffers + 2];
+            _topologyRemapBuffers = new();
+            VertexInfoBufferUpdater = new(context.Renderer);
+        }
+
+        /// <summary>
+        /// Gets the number of complete primitives that can be formed with a given vertex count, for a given topology.
+        /// </summary>
+        /// <param name="primitiveType">Topology</param>
+        /// <param name="count">Vertex count</param>
+        /// <returns>Total of complete primitives</returns>
+        public static int GetPrimitivesCount(PrimitiveTopology primitiveType, int count)
+        {
+            return primitiveType switch
+            {
+                PrimitiveTopology.Lines => count / 2,
+                PrimitiveTopology.LinesAdjacency => count / 4,
+                PrimitiveTopology.LineLoop => count > 1 ? count : 0,
+                PrimitiveTopology.LineStrip => Math.Max(count - 1, 0),
+                PrimitiveTopology.LineStripAdjacency => Math.Max(count - 3, 0),
+                PrimitiveTopology.Triangles => count / 3,
+                PrimitiveTopology.TrianglesAdjacency => count / 6,
+                PrimitiveTopology.TriangleStrip or
+                PrimitiveTopology.TriangleFan or
+                PrimitiveTopology.Polygon => Math.Max(count - 2, 0),
+                PrimitiveTopology.TriangleStripAdjacency => Math.Max(count - 2, 0) / 2,
+                PrimitiveTopology.Quads => (count / 4) * 2, // In triangles.
+                PrimitiveTopology.QuadStrip => Math.Max((count - 2) / 2, 0) * 2, // In triangles.
+                _ => count,
+            };
+        }
+
+        /// <summary>
+        /// Gets the total of vertices that a single primitive has, for the specified topology.
+        /// </summary>
+        /// <param name="primitiveType">Topology</param>
+        /// <returns>Vertex count</returns>
+        private static int GetVerticesPerPrimitive(PrimitiveTopology primitiveType)
+        {
+            return primitiveType switch
+            {
+                PrimitiveTopology.Lines or
+                PrimitiveTopology.LineLoop or
+                PrimitiveTopology.LineStrip => 2,
+                PrimitiveTopology.LinesAdjacency or
+                PrimitiveTopology.LineStripAdjacency => 4,
+                PrimitiveTopology.Triangles or
+                PrimitiveTopology.TriangleStrip or
+                PrimitiveTopology.TriangleFan or
+                PrimitiveTopology.Polygon => 3,
+                PrimitiveTopology.TrianglesAdjacency or
+                PrimitiveTopology.TriangleStripAdjacency => 6,
+                PrimitiveTopology.Quads or
+                PrimitiveTopology.QuadStrip => 3, // 2 triangles.
+                _ => 1,
+            };
+        }
+
+        /// <summary>
+        /// Gets a cached or creates a new buffer that can be used to map linear indices to ones
+        /// of a specified topology, and build complete primitives.
+        /// </summary>
+        /// <param name="topology">Topology</param>
+        /// <param name="count">Number of input vertices that needs to be mapped using that buffer</param>
+        /// <returns>Remap buffer range</returns>
+        public BufferRange GetOrCreateTopologyRemapBuffer(PrimitiveTopology topology, int count)
+        {
+            if (!_topologyRemapBuffers.TryGetValue(topology, out IndexBuffer buffer) || buffer.Count < count)
+            {
+                if (buffer.Handle != BufferHandle.Null)
+                {
+                    _context.Renderer.DeleteBuffer(buffer.Handle);
+                }
+
+                buffer = CreateTopologyRemapBuffer(topology, count);
+                _topologyRemapBuffers[topology] = buffer;
+
+                return buffer.ToRange();
+            }
+
+            return buffer.ToRange(Math.Max(GetPrimitivesCount(topology, count) * GetVerticesPerPrimitive(topology), 1) * sizeof(uint));
+        }
+
+        /// <summary>
+        /// Creates a new topology remap buffer.
+        /// </summary>
+        /// <param name="topology">Topology</param>
+        /// <param name="count">Maximum of vertices that will be accessed</param>
+        /// <returns>Remap buffer range</returns>
+        private IndexBuffer CreateTopologyRemapBuffer(PrimitiveTopology topology, int count)
+        {
+            // Size can't be zero as creating zero sized buffers is invalid.
+            Span<int> data = new int[Math.Max(GetPrimitivesCount(topology, count) * GetVerticesPerPrimitive(topology), 1)];
+
+            switch (topology)
+            {
+                case PrimitiveTopology.Points:
+                case PrimitiveTopology.Lines:
+                case PrimitiveTopology.LinesAdjacency:
+                case PrimitiveTopology.Triangles:
+                case PrimitiveTopology.TrianglesAdjacency:
+                case PrimitiveTopology.Patches:
+                    for (int index = 0; index < data.Length; index++)
+                    {
+                        data[index] = index;
+                    }
+                    break;
+                case PrimitiveTopology.LineLoop:
+                    data[^1] = 0;
+
+                    for (int index = 0; index < ((data.Length - 1) & ~1); index += 2)
+                    {
+                        data[index] = index >> 1;
+                        data[index + 1] = (index >> 1) + 1;
+                    }
+                    break;
+                case PrimitiveTopology.LineStrip:
+                    for (int index = 0; index < ((data.Length - 1) & ~1); index += 2)
+                    {
+                        data[index] = index >> 1;
+                        data[index + 1] = (index >> 1) + 1;
+                    }
+                    break;
+                case PrimitiveTopology.TriangleStrip:
+                    int tsTrianglesCount = data.Length / 3;
+                    int tsOutIndex = 3;
+
+                    if (tsTrianglesCount > 0)
+                    {
+                        data[0] = 0;
+                        data[1] = 1;
+                        data[2] = 2;
+                    }
+
+                    for (int tri = 1; tri < tsTrianglesCount; tri++)
+                    {
+                        int baseIndex = tri * 3;
+
+                        if ((tri & 1) != 0)
+                        {
+                            data[baseIndex] = tsOutIndex - 1;
+                            data[baseIndex + 1] = tsOutIndex - 2;
+                            data[baseIndex + 2] = tsOutIndex++;
+                        }
+                        else
+                        {
+                            data[baseIndex] = tsOutIndex - 2;
+                            data[baseIndex + 1] = tsOutIndex - 1;
+                            data[baseIndex + 2] = tsOutIndex++;
+                        }
+                    }
+                    break;
+                case PrimitiveTopology.TriangleFan:
+                case PrimitiveTopology.Polygon:
+                    int tfTrianglesCount = data.Length / 3;
+                    int tfOutIndex = 1;
+
+                    for (int index = 0; index < tfTrianglesCount * 3; index += 3)
+                    {
+                        data[index] = 0;
+                        data[index + 1] = tfOutIndex;
+                        data[index + 2] = ++tfOutIndex;
+                    }
+                    break;
+                case PrimitiveTopology.Quads:
+                    int qQuadsCount = data.Length / 6;
+
+                    for (int quad = 0; quad < qQuadsCount; quad++)
+                    {
+                        int index = quad * 6;
+                        int qIndex = quad * 4;
+
+                        data[index] = qIndex;
+                        data[index + 1] = qIndex + 1;
+                        data[index + 2] = qIndex + 2;
+                        data[index + 3] = qIndex;
+                        data[index + 4] = qIndex + 2;
+                        data[index + 5] = qIndex + 3;
+                    }
+                    break;
+                case PrimitiveTopology.QuadStrip:
+                    int qsQuadsCount = data.Length / 6;
+
+                    if (qsQuadsCount > 0)
+                    {
+                        data[0] = 0;
+                        data[1] = 1;
+                        data[2] = 2;
+                        data[3] = 0;
+                        data[4] = 2;
+                        data[5] = 3;
+                    }
+
+                    for (int quad = 1; quad < qsQuadsCount; quad++)
+                    {
+                        int index = quad * 6;
+                        int qIndex = quad * 2;
+
+                        data[index] = qIndex + 1;
+                        data[index + 1] = qIndex;
+                        data[index + 2] = qIndex + 2;
+                        data[index + 3] = qIndex + 1;
+                        data[index + 4] = qIndex + 2;
+                        data[index + 5] = qIndex + 3;
+                    }
+                    break;
+                case PrimitiveTopology.LineStripAdjacency:
+                    for (int index = 0; index < ((data.Length - 3) & ~3); index += 4)
+                    {
+                        int lIndex = index >> 2;
+
+                        data[index] = lIndex;
+                        data[index + 1] = lIndex + 1;
+                        data[index + 2] = lIndex + 2;
+                        data[index + 3] = lIndex + 3;
+                    }
+                    break;
+                case PrimitiveTopology.TriangleStripAdjacency:
+                    int tsaTrianglesCount = data.Length / 6;
+                    int tsaOutIndex = 6;
+
+                    if (tsaTrianglesCount > 0)
+                    {
+                        data[0] = 0;
+                        data[1] = 1;
+                        data[2] = 2;
+                        data[3] = 3;
+                        data[4] = 4;
+                        data[5] = 5;
+                    }
+
+                    for (int tri = 1; tri < tsaTrianglesCount; tri++)
+                    {
+                        int baseIndex = tri * 6;
+
+                        if ((tri & 1) != 0)
+                        {
+                            data[baseIndex] = tsaOutIndex - 2;
+                            data[baseIndex + 1] = tsaOutIndex - 1;
+                            data[baseIndex + 2] = tsaOutIndex - 4;
+                            data[baseIndex + 3] = tsaOutIndex - 3;
+                            data[baseIndex + 4] = tsaOutIndex++;
+                            data[baseIndex + 5] = tsaOutIndex++;
+                        }
+                        else
+                        {
+                            data[baseIndex] = tsaOutIndex - 4;
+                            data[baseIndex + 1] = tsaOutIndex - 3;
+                            data[baseIndex + 2] = tsaOutIndex - 2;
+                            data[baseIndex + 3] = tsaOutIndex - 1;
+                            data[baseIndex + 4] = tsaOutIndex++;
+                            data[baseIndex + 5] = tsaOutIndex++;
+                        }
+                    }
+                    break;
+            }
+
+            ReadOnlySpan<byte> dataBytes = MemoryMarshal.Cast<int, byte>(data);
+
+            BufferHandle buffer = _context.Renderer.CreateBuffer(dataBytes.Length);
+            _context.Renderer.SetBufferData(buffer, 0, dataBytes);
+
+            return new IndexBuffer(buffer, count, dataBytes.Length);
+        }
+
+        /// <summary>
+        /// Gets a buffer texture with a given format, for the given index.
+        /// </summary>
+        /// <param name="index">Index of the buffer texture</param>
+        /// <param name="format">Format of the buffer texture</param>
+        /// <returns>Buffer texture</returns>
+        public ITexture EnsureBufferTexture(int index, Format format)
+        {
+            return (_bufferTextures[index] ??= new()).Get(_context.Renderer, format);
+        }
+
+        /// <summary>
+        /// Gets the offset and size of usable storage on the output vertex buffer.
+        /// </summary>
+        /// <param name="size">Size in bytes that will be used</param>
+        /// <returns>Usable offset and size on the buffer</returns>
+        public (int, int) GetVertexDataBuffer(int size)
+        {
+            return EnsureBuffer(ref _vertexDataBuffer, size);
+        }
+
+        /// <summary>
+        /// Gets the offset and size of usable storage on the output geometry shader vertex buffer.
+        /// </summary>
+        /// <param name="size">Size in bytes that will be used</param>
+        /// <returns>Usable offset and size on the buffer</returns>
+        public (int, int) GetGeometryVertexDataBuffer(int size)
+        {
+            return EnsureBuffer(ref _geometryVertexDataBuffer, size);
+        }
+
+        /// <summary>
+        /// Gets the offset and size of usable storage on the output geometry shader index buffer.
+        /// </summary>
+        /// <param name="size">Size in bytes that will be used</param>
+        /// <returns>Usable offset and size on the buffer</returns>
+        public (int, int) GetGeometryIndexDataBuffer(int size)
+        {
+            return EnsureBuffer(ref _geometryIndexDataBuffer, size);
+        }
+
+        /// <summary>
+        /// Gets a range of the output vertex buffer for binding.
+        /// </summary>
+        /// <param name="offset">Offset of the range</param>
+        /// <param name="size">Size of the range in bytes</param>
+        /// <param name="write">Indicates if the buffer contents will be modified</param>
+        /// <returns>Range</returns>
+        public BufferRange GetVertexDataBufferRange(int offset, int size, bool write)
+        {
+            return new BufferRange(_vertexDataBuffer.Handle, offset, size, write);
+        }
+
+        /// <summary>
+        /// Gets a range of the output geometry shader vertex buffer for binding.
+        /// </summary>
+        /// <param name="offset">Offset of the range</param>
+        /// <param name="size">Size of the range in bytes</param>
+        /// <param name="write">Indicates if the buffer contents will be modified</param>
+        /// <returns>Range</returns>
+        public BufferRange GetGeometryVertexDataBufferRange(int offset, int size, bool write)
+        {
+            return new BufferRange(_geometryVertexDataBuffer.Handle, offset, size, write);
+        }
+
+        /// <summary>
+        /// Gets a range of the output geometry shader index buffer for binding.
+        /// </summary>
+        /// <param name="offset">Offset of the range</param>
+        /// <param name="size">Size of the range in bytes</param>
+        /// <param name="write">Indicates if the buffer contents will be modified</param>
+        /// <returns>Range</returns>
+        public BufferRange GetGeometryIndexDataBufferRange(int offset, int size, bool write)
+        {
+            return new BufferRange(_geometryIndexDataBuffer.Handle, offset, size, write);
+        }
+
+        /// <summary>
+        /// Gets the range for a dummy 16 bytes buffer, filled with zeros.
+        /// </summary>
+        /// <returns>Dummy buffer range</returns>
+        public BufferRange GetDummyBufferRange()
+        {
+            if (_dummyBuffer == BufferHandle.Null)
+            {
+                _dummyBuffer = _context.Renderer.CreateBuffer(DummyBufferSize);
+                _context.Renderer.Pipeline.ClearBuffer(_dummyBuffer, 0, DummyBufferSize, 0);
+            }
+
+            return new BufferRange(_dummyBuffer, 0, DummyBufferSize);
+        }
+
+        /// <summary>
+        /// Gets the range for a sequential index buffer, with ever incrementing index values.
+        /// </summary>
+        /// <param name="count">Minimum number of indices that the buffer should have</param>
+        /// <returns>Buffer handle</returns>
+        public BufferHandle GetSequentialIndexBuffer(int count)
+        {
+            if (_sequentialIndexBufferCount < count)
+            {
+                if (_sequentialIndexBuffer != BufferHandle.Null)
+                {
+                    _context.Renderer.DeleteBuffer(_sequentialIndexBuffer);
+                }
+
+                _sequentialIndexBuffer = _context.Renderer.CreateBuffer(count * sizeof(uint));
+                _sequentialIndexBufferCount = count;
+
+                Span<int> data = new int[count];
+
+                for (int index = 0; index < count; index++)
+                {
+                    data[index] = index;
+                }
+
+                _context.Renderer.SetBufferData(_sequentialIndexBuffer, 0, MemoryMarshal.Cast<int, byte>(data));
+            }
+
+            return _sequentialIndexBuffer;
+        }
+
+        /// <summary>
+        /// Ensure that a buffer exists, is large enough, and allocates a sub-region of the specified size inside the buffer.
+        /// </summary>
+        /// <param name="buffer">Buffer state</param>
+        /// <param name="size">Required size in bytes</param>
+        /// <returns>Allocated offset and size</returns>
+        private (int, int) EnsureBuffer(ref Buffer buffer, int size)
+        {
+            int newSize = buffer.Offset + size;
+
+            if (buffer.Size < newSize)
+            {
+                if (buffer.Handle != BufferHandle.Null)
+                {
+                    _context.Renderer.DeleteBuffer(buffer.Handle);
+                }
+
+                buffer.Handle = _context.Renderer.CreateBuffer(newSize);
+                buffer.Size = newSize;
+            }
+
+            int offset = buffer.Offset;
+
+            buffer.Offset = BitUtils.AlignUp(newSize, _context.Capabilities.StorageBufferOffsetAlignment);
+
+            return (offset, size);
+        }
+
+        /// <summary>
+        /// Frees all buffer sub-regions that were previously allocated.
+        /// </summary>
+        public void FreeBuffers()
+        {
+            _vertexDataBuffer.Offset = 0;
+            _geometryVertexDataBuffer.Offset = 0;
+            _geometryIndexDataBuffer.Offset = 0;
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                for (int index = 0; index < _bufferTextures.Length; index++)
+                {
+                    _bufferTextures[index]?.Dispose();
+                    _bufferTextures[index] = null;
+                }
+
+                DestroyIfNotNull(ref _dummyBuffer);
+                DestroyIfNotNull(ref _vertexDataBuffer.Handle);
+                DestroyIfNotNull(ref _geometryVertexDataBuffer.Handle);
+                DestroyIfNotNull(ref _geometryIndexDataBuffer.Handle);
+                DestroyIfNotNull(ref _sequentialIndexBuffer);
+
+                foreach (var indexBuffer in _topologyRemapBuffers.Values)
+                {
+                    _context.Renderer.DeleteBuffer(indexBuffer.Handle);
+                }
+
+                _topologyRemapBuffers.Clear();
+            }
+        }
+
+        /// <summary>
+        /// Deletes a buffer if the handle is valid (not null), then sets the handle to null.
+        /// </summary>
+        /// <param name="handle">Buffer handle</param>
+        private void DestroyIfNotNull(ref BufferHandle handle)
+        {
+            if (handle != BufferHandle.Null)
+            {
+                _context.Renderer.DeleteBuffer(handle);
+                handle = BufferHandle.Null;
+            }
+        }
+
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs
new file mode 100644
index 000000000..d1a333a71
--- /dev/null
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs
@@ -0,0 +1,535 @@
+using Ryujinx.Common;
+using Ryujinx.Common.Logging;
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.Gpu.Engine.Types;
+using Ryujinx.Graphics.Gpu.Image;
+using Ryujinx.Graphics.Gpu.Shader;
+using Ryujinx.Graphics.Shader;
+using Ryujinx.Graphics.Shader.Translation;
+using System;
+
+namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
+{
+    /// <summary>
+    /// Vertex, tessellation and geometry as compute shader state.
+    /// </summary>
+    struct VtgAsComputeState
+    {
+        private const int ComputeLocalSize = 32;
+
+        private readonly GpuContext _context;
+        private readonly GpuChannel _channel;
+        private readonly DeviceStateWithShadow<ThreedClassState> _state;
+        private readonly VtgAsComputeContext _vacContext;
+        private readonly ThreedClass _engine;
+        private readonly ShaderAsCompute _vertexAsCompute;
+        private readonly ShaderAsCompute _geometryAsCompute;
+        private readonly IProgram _vertexPassthroughProgram;
+        private readonly PrimitiveTopology _topology;
+        private readonly int _count;
+        private readonly int _instanceCount;
+        private readonly int _firstIndex;
+        private readonly int _firstVertex;
+        private readonly int _firstInstance;
+        private readonly bool _indexed;
+
+        private readonly int _vertexDataOffset;
+        private readonly int _vertexDataSize;
+        private readonly int _geometryVertexDataOffset;
+        private readonly int _geometryVertexDataSize;
+        private readonly int _geometryIndexDataOffset;
+        private readonly int _geometryIndexDataSize;
+        private readonly int _geometryIndexDataCount;
+
+        /// <summary>
+        /// Creates a new vertex, tessellation and geometry as compute shader state.
+        /// </summary>
+        /// <param name="context">GPU context</param>
+        /// <param name="channel">GPU channel</param>
+        /// <param name="state">3D engine state</param>
+        /// <param name="vacContext">Vertex as compute context</param>
+        /// <param name="engine">3D engine</param>
+        /// <param name="vertexAsCompute">Vertex shader converted to compute</param>
+        /// <param name="geometryAsCompute">Optional geometry shader converted to compute</param>
+        /// <param name="vertexPassthroughProgram">Fragment shader with a vertex passthrough shader to feed the compute output into the fragment stage</param>
+        /// <param name="topology">Primitive topology of the draw</param>
+        /// <param name="count">Index or vertex count of the draw</param>
+        /// <param name="instanceCount">Instance count</param>
+        /// <param name="firstIndex">First index on the index buffer, for indexed draws</param>
+        /// <param name="firstVertex">First vertex on the vertex buffer</param>
+        /// <param name="firstInstance">First instance</param>
+        /// <param name="indexed">Whether the draw is indexed</param>
+        public VtgAsComputeState(
+            GpuContext context,
+            GpuChannel channel,
+            DeviceStateWithShadow<ThreedClassState> state,
+            VtgAsComputeContext vacContext,
+            ThreedClass engine,
+            ShaderAsCompute vertexAsCompute,
+            ShaderAsCompute geometryAsCompute,
+            IProgram vertexPassthroughProgram,
+            PrimitiveTopology topology,
+            int count,
+            int instanceCount,
+            int firstIndex,
+            int firstVertex,
+            int firstInstance,
+            bool indexed)
+        {
+            _context = context;
+            _channel = channel;
+            _state = state;
+            _vacContext = vacContext;
+            _engine = engine;
+            _vertexAsCompute = vertexAsCompute;
+            _geometryAsCompute = geometryAsCompute;
+            _vertexPassthroughProgram = vertexPassthroughProgram;
+            _topology = topology;
+            _count = count;
+            _instanceCount = instanceCount;
+            _firstIndex = firstIndex;
+            _firstVertex = firstVertex;
+            _firstInstance = firstInstance;
+            _indexed = indexed;
+
+            int vertexDataSize = vertexAsCompute.Reservations.OutputSizeInBytesPerInvocation * count * instanceCount;
+
+            (_vertexDataOffset, _vertexDataSize) = _vacContext.GetVertexDataBuffer(vertexDataSize);
+
+            if (geometryAsCompute != null)
+            {
+                int totalPrimitivesCount = VtgAsComputeContext.GetPrimitivesCount(topology, count * instanceCount);
+                int maxCompleteStrips = GetMaxCompleteStrips(geometryAsCompute.Info.GeometryVerticesPerPrimitive, geometryAsCompute.Info.GeometryMaxOutputVertices);
+                int totalVerticesCount = totalPrimitivesCount * geometryAsCompute.Info.GeometryMaxOutputVertices * geometryAsCompute.Info.ThreadsPerInputPrimitive;
+                int geometryVbDataSize = totalVerticesCount * geometryAsCompute.Reservations.OutputSizeInBytesPerInvocation;
+                int geometryIbDataCount = totalVerticesCount + totalPrimitivesCount * maxCompleteStrips;
+                int geometryIbDataSize = geometryIbDataCount * sizeof(uint);
+
+                (_geometryVertexDataOffset, _geometryVertexDataSize) = vacContext.GetGeometryVertexDataBuffer(geometryVbDataSize);
+                (_geometryIndexDataOffset, _geometryIndexDataSize) = vacContext.GetGeometryIndexDataBuffer(geometryIbDataSize);
+
+                _geometryIndexDataCount = geometryIbDataCount;
+            }
+        }
+
+        /// <summary>
+        /// Emulates the vertex stage using compute.
+        /// </summary>
+        public readonly void RunVertex()
+        {
+            _context.Renderer.Pipeline.SetProgram(_vertexAsCompute.HostProgram);
+
+            int primitivesCount = VtgAsComputeContext.GetPrimitivesCount(_topology, _count);
+
+            _vacContext.VertexInfoBufferUpdater.SetVertexCounts(_count, _instanceCount, _firstVertex, _firstInstance);
+            _vacContext.VertexInfoBufferUpdater.SetGeometryCounts(primitivesCount);
+
+            for (int index = 0; index < Constants.TotalVertexAttribs; index++)
+            {
+                var vertexAttrib = _state.State.VertexAttribState[index];
+
+                if (!FormatTable.TryGetSingleComponentAttribFormat(vertexAttrib.UnpackFormat(), out Format format, out int componentsCount))
+                {
+                    Logger.Debug?.Print(LogClass.Gpu, $"Invalid attribute format 0x{vertexAttrib.UnpackFormat():X}.");
+
+                    format = vertexAttrib.UnpackType() switch
+                    {
+                        VertexAttribType.Sint => Format.R32Sint,
+                        VertexAttribType.Uint => Format.R32Uint,
+                        _ => Format.R32Float
+                    };
+
+                    componentsCount = 4;
+                }
+
+                if (vertexAttrib.UnpackIsConstant())
+                {
+                    _vacContext.VertexInfoBufferUpdater.SetVertexStride(index, 0, componentsCount);
+                    _vacContext.VertexInfoBufferUpdater.SetVertexOffset(index, 0, 0);
+                    SetDummyBufferTexture(_vertexAsCompute.Reservations, index, format);
+                    continue;
+                }
+
+                int bufferIndex = vertexAttrib.UnpackBufferIndex();
+
+                GpuVa endAddress = _state.State.VertexBufferEndAddress[bufferIndex];
+                var vertexBuffer = _state.State.VertexBufferState[bufferIndex];
+                bool instanced = _state.State.VertexBufferInstanced[bufferIndex];
+
+                ulong address = vertexBuffer.Address.Pack();
+
+                if (!vertexBuffer.UnpackEnable() || !_channel.MemoryManager.IsMapped(address))
+                {
+                    _vacContext.VertexInfoBufferUpdater.SetVertexStride(index, 0, componentsCount);
+                    _vacContext.VertexInfoBufferUpdater.SetVertexOffset(index, 0, 0);
+                    SetDummyBufferTexture(_vertexAsCompute.Reservations, index, format);
+                    continue;
+                }
+
+                int vbStride = vertexBuffer.UnpackStride();
+                ulong vbSize = GetVertexBufferSize(address, endAddress.Pack(), vbStride, _indexed, instanced, _firstVertex, _count);
+
+                ulong oldVbSize = vbSize;
+
+                ulong attributeOffset = (ulong)vertexAttrib.UnpackOffset();
+                int componentSize = format.GetScalarSize();
+
+                address += attributeOffset;
+
+                ulong misalign = address & ((ulong)_context.Capabilities.TextureBufferOffsetAlignment - 1);
+
+                vbSize = Align(vbSize - attributeOffset + misalign, componentSize);
+
+                SetBufferTexture(_vertexAsCompute.Reservations, index, format, address - misalign, vbSize);
+
+                _vacContext.VertexInfoBufferUpdater.SetVertexStride(index, vbStride / componentSize, componentsCount);
+                _vacContext.VertexInfoBufferUpdater.SetVertexOffset(index, (int)misalign / componentSize, instanced ? vertexBuffer.Divisor : 0);
+            }
+
+            if (_indexed)
+            {
+                SetIndexBufferTexture(_vertexAsCompute.Reservations, _firstIndex, _count, out int ibOffset);
+                _vacContext.VertexInfoBufferUpdater.SetIndexBufferOffset(ibOffset);
+            }
+            else
+            {
+                SetSequentialIndexBufferTexture(_vertexAsCompute.Reservations, _count);
+                _vacContext.VertexInfoBufferUpdater.SetIndexBufferOffset(0);
+            }
+
+            int vertexInfoBinding = _vertexAsCompute.Reservations.VertexInfoConstantBufferBinding;
+            BufferRange vertexInfoRange = new(_vacContext.VertexInfoBufferUpdater.Handle, 0, VertexInfoBuffer.RequiredSize);
+            _context.Renderer.Pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(vertexInfoBinding, vertexInfoRange) });
+
+            int vertexDataBinding = _vertexAsCompute.Reservations.VertexOutputStorageBufferBinding;
+            BufferRange vertexDataRange = _vacContext.GetVertexDataBufferRange(_vertexDataOffset, _vertexDataSize, write: true);
+            _context.Renderer.Pipeline.SetStorageBuffers(stackalloc[] { new BufferAssignment(vertexDataBinding, vertexDataRange) });
+
+            _vacContext.VertexInfoBufferUpdater.Commit();
+
+            _context.Renderer.Pipeline.DispatchCompute(
+                BitUtils.DivRoundUp(_count, ComputeLocalSize),
+                BitUtils.DivRoundUp(_instanceCount, ComputeLocalSize),
+                1);
+        }
+
+        /// <summary>
+        /// Emulates the geometry stage using compute, if it exists, otherwise does nothing.
+        /// </summary>
+        public readonly void RunGeometry()
+        {
+            if (_geometryAsCompute == null)
+            {
+                return;
+            }
+
+            int primitivesCount = VtgAsComputeContext.GetPrimitivesCount(_topology, _count);
+
+            _vacContext.VertexInfoBufferUpdater.SetVertexCounts(_count, _instanceCount, _firstVertex, _firstInstance);
+            _vacContext.VertexInfoBufferUpdater.SetGeometryCounts(primitivesCount);
+            _vacContext.VertexInfoBufferUpdater.Commit();
+
+            int vertexInfoBinding = _vertexAsCompute.Reservations.VertexInfoConstantBufferBinding;
+            BufferRange vertexInfoRange = new(_vacContext.VertexInfoBufferUpdater.Handle, 0, VertexInfoBuffer.RequiredSize);
+            _context.Renderer.Pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(vertexInfoBinding, vertexInfoRange) });
+
+            int vertexDataBinding = _vertexAsCompute.Reservations.VertexOutputStorageBufferBinding;
+
+            // Wait until compute is done.
+            // TODO: Batch compute and draw operations to avoid pipeline stalls.
+            _context.Renderer.Pipeline.Barrier();
+            _context.Renderer.Pipeline.SetProgram(_geometryAsCompute.HostProgram);
+
+            SetTopologyRemapBufferTexture(_geometryAsCompute.Reservations, _topology, _count);
+
+            int geometryVbBinding = _geometryAsCompute.Reservations.GeometryVertexOutputStorageBufferBinding;
+            int geometryIbBinding = _geometryAsCompute.Reservations.GeometryIndexOutputStorageBufferBinding;
+
+            BufferRange vertexDataRange = _vacContext.GetVertexDataBufferRange(_vertexDataOffset, _vertexDataSize, write: false);
+            BufferRange vertexBuffer = _vacContext.GetGeometryVertexDataBufferRange(_geometryVertexDataOffset, _geometryVertexDataSize, write: true);
+            BufferRange indexBuffer = _vacContext.GetGeometryIndexDataBufferRange(_geometryIndexDataOffset, _geometryIndexDataSize, write: true);
+
+            _context.Renderer.Pipeline.SetStorageBuffers(stackalloc[]
+            {
+                new BufferAssignment(vertexDataBinding, vertexDataRange),
+                new BufferAssignment(geometryVbBinding, vertexBuffer),
+                new BufferAssignment(geometryIbBinding, indexBuffer),
+            });
+
+            _context.Renderer.Pipeline.DispatchCompute(
+                BitUtils.DivRoundUp(primitivesCount, ComputeLocalSize),
+                BitUtils.DivRoundUp(_instanceCount, ComputeLocalSize),
+                _geometryAsCompute.Info.ThreadsPerInputPrimitive);
+        }
+
+        /// <summary>
+        /// Performs a draw using the data produced on the vertex, tessellation and geometry stages,
+        /// if rasterizer discard is disabled.
+        /// </summary>
+        public readonly void RunFragment()
+        {
+            bool tfEnabled = _state.State.TfEnable;
+
+            if (!_state.State.RasterizeEnable && (!tfEnabled || !_context.Capabilities.SupportsTransformFeedback))
+            {
+                // No need to run fragment if rasterizer discard is enabled,
+                // and we are emulating transform feedback or transform feedback is disabled.
+
+                // Note: We might skip geometry shader here, but right now, this is fine,
+                // because the only cases that triggers VTG to compute are geometry shader
+                // being not supported, or the vertex pipeline doing store operations.
+                // If the geometry shader does not do any store and rasterizer discard is enabled, the geometry shader can be skipped.
+                // If the geometry shader does have stores, it would have been converted to compute too if stores are not supported.
+
+                return;
+            }
+
+            int vertexDataBinding = _vertexAsCompute.Reservations.VertexOutputStorageBufferBinding;
+
+            _context.Renderer.Pipeline.Barrier();
+
+            _vacContext.VertexInfoBufferUpdater.SetVertexCounts(_count, _instanceCount, _firstVertex, _firstInstance);
+            _vacContext.VertexInfoBufferUpdater.Commit();
+
+            if (_geometryAsCompute != null)
+            {
+                BufferRange vertexBuffer = _vacContext.GetGeometryVertexDataBufferRange(_geometryVertexDataOffset, _geometryVertexDataSize, write: false);
+                BufferRange indexBuffer = _vacContext.GetGeometryIndexDataBufferRange(_geometryIndexDataOffset, _geometryIndexDataSize, write: false);
+
+                _context.Renderer.Pipeline.SetProgram(_vertexPassthroughProgram);
+                _context.Renderer.Pipeline.SetIndexBuffer(indexBuffer, IndexType.UInt);
+                _context.Renderer.Pipeline.SetStorageBuffers(stackalloc[] { new BufferAssignment(vertexDataBinding, vertexBuffer) });
+
+                _context.Renderer.Pipeline.SetPrimitiveRestart(true, -1);
+                _context.Renderer.Pipeline.SetPrimitiveTopology(GetGeometryOutputTopology(_geometryAsCompute.Info.GeometryVerticesPerPrimitive));
+
+                _context.Renderer.Pipeline.DrawIndexed(_geometryIndexDataCount, 1, 0, 0, 0);
+
+                _engine.ForceStateDirtyByIndex(StateUpdater.IndexBufferStateIndex);
+                _engine.ForceStateDirtyByIndex(StateUpdater.PrimitiveRestartStateIndex);
+            }
+            else
+            {
+                BufferRange vertexDataRange = _vacContext.GetVertexDataBufferRange(_vertexDataOffset, _vertexDataSize, write: false);
+
+                _context.Renderer.Pipeline.SetProgram(_vertexPassthroughProgram);
+                _context.Renderer.Pipeline.SetStorageBuffers(stackalloc[] { new BufferAssignment(vertexDataBinding, vertexDataRange) });
+                _context.Renderer.Pipeline.Draw(_count, _instanceCount, 0, 0);
+            }
+        }
+
+        /// <summary>
+        /// Gets a strip primitive topology from the vertices per primitive count.
+        /// </summary>
+        /// <param name="verticesPerPrimitive">Vertices per primitive count</param>
+        /// <returns>Primitive topology</returns>
+        private static PrimitiveTopology GetGeometryOutputTopology(int verticesPerPrimitive)
+        {
+            return verticesPerPrimitive switch
+            {
+                3 => PrimitiveTopology.TriangleStrip,
+                2 => PrimitiveTopology.LineStrip,
+                _ => PrimitiveTopology.Points,
+            };
+        }
+
+        /// <summary>
+        /// Gets the maximum number of complete primitive strips for a vertex count.
+        /// </summary>
+        /// <param name="verticesPerPrimitive">Vertices per primitive count</param>
+        /// <param name="maxOutputVertices">Maximum geometry shader output vertices count</param>
+        /// <returns>Maximum number of complete primitive strips</returns>
+        private static int GetMaxCompleteStrips(int verticesPerPrimitive, int maxOutputVertices)
+        {
+            return maxOutputVertices / verticesPerPrimitive;
+        }
+
+        /// <summary>
+        /// Binds a dummy buffer as vertex buffer into a buffer texture.
+        /// </summary>
+        /// <param name="reservations">Shader resource binding reservations</param>
+        /// <param name="index">Buffer texture index</param>
+        /// <param name="format">Buffer texture format</param>
+        private readonly void SetDummyBufferTexture(ResourceReservations reservations, int index, Format format)
+        {
+            ITexture bufferTexture = _vacContext.EnsureBufferTexture(index + 2, format);
+            bufferTexture.SetStorage(_vacContext.GetDummyBufferRange());
+
+            _context.Renderer.Pipeline.SetTextureAndSampler(ShaderStage.Compute, reservations.GetVertexBufferTextureBinding(index), bufferTexture, null);
+        }
+
+        /// <summary>
+        /// Binds a vertex buffer into a buffer texture.
+        /// </summary>
+        /// <param name="reservations">Shader resource binding reservations</param>
+        /// <param name="index">Buffer texture index</param>
+        /// <param name="format">Buffer texture format</param>
+        /// <param name="address">Address of the vertex buffer</param>
+        /// <param name="size">Size of the buffer in bytes</param>
+        private readonly void SetBufferTexture(ResourceReservations reservations, int index, Format format, ulong address, ulong size)
+        {
+            var memoryManager = _channel.MemoryManager;
+
+            address = memoryManager.Translate(address);
+            BufferRange range = memoryManager.Physical.BufferCache.GetBufferRange(address, size);
+
+            ITexture bufferTexture = _vacContext.EnsureBufferTexture(index + 2, format);
+            bufferTexture.SetStorage(range);
+
+            _context.Renderer.Pipeline.SetTextureAndSampler(ShaderStage.Compute, reservations.GetVertexBufferTextureBinding(index), bufferTexture, null);
+        }
+
+        /// <summary>
+        /// Binds the index buffer into a buffer texture.
+        /// </summary>
+        /// <param name="reservations">Shader resource binding reservations</param>
+        /// <param name="firstIndex">First index of the index buffer</param>
+        /// <param name="count">Index count</param>
+        /// <param name="misalignedOffset">Offset that should be added when accessing the buffer texture on the shader</param>
+        private readonly void SetIndexBufferTexture(ResourceReservations reservations, int firstIndex, int count, out int misalignedOffset)
+        {
+            ulong address = _state.State.IndexBufferState.Address.Pack();
+            ulong indexOffset = (ulong)firstIndex;
+            ulong size = (ulong)count;
+
+            int shift = 0;
+            Format format = Format.R8Uint;
+
+            switch (_state.State.IndexBufferState.Type)
+            {
+                case IndexType.UShort:
+                    shift = 1;
+                    format = Format.R16Uint;
+                    break;
+                case IndexType.UInt:
+                    shift = 2;
+                    format = Format.R32Uint;
+                    break;
+            }
+
+            indexOffset <<= shift;
+            size <<= shift;
+
+            var memoryManager = _channel.MemoryManager;
+
+            address = memoryManager.Translate(address + indexOffset);
+            ulong misalign = address & ((ulong)_context.Capabilities.TextureBufferOffsetAlignment - 1);
+            BufferRange range = memoryManager.Physical.BufferCache.GetBufferRange(address - misalign, size + misalign);
+            misalignedOffset = (int)misalign >> shift;
+
+            SetIndexBufferTexture(reservations, range, format);
+        }
+
+        /// <summary>
+        /// Sets the host buffer texture for the index buffer.
+        /// </summary>
+        /// <param name="reservations">Shader resource binding reservations</param>
+        /// <param name="range">Index buffer range</param>
+        /// <param name="format">Index buffer format</param>
+        private readonly void SetIndexBufferTexture(ResourceReservations reservations, BufferRange range, Format format)
+        {
+            ITexture bufferTexture = _vacContext.EnsureBufferTexture(0, format);
+            bufferTexture.SetStorage(range);
+
+            _context.Renderer.Pipeline.SetTextureAndSampler(ShaderStage.Compute, reservations.IndexBufferTextureBinding, bufferTexture, null);
+        }
+
+        /// <summary>
+        /// Sets the host buffer texture for the topology remap buffer.
+        /// </summary>
+        /// <param name="reservations">Shader resource binding reservations</param>
+        /// <param name="topology">Input topology</param>
+        /// <param name="count">Input vertex count</param>
+        private readonly void SetTopologyRemapBufferTexture(ResourceReservations reservations, PrimitiveTopology topology, int count)
+        {
+            ITexture bufferTexture = _vacContext.EnsureBufferTexture(1, Format.R32Uint);
+            bufferTexture.SetStorage(_vacContext.GetOrCreateTopologyRemapBuffer(topology, count));
+
+            _context.Renderer.Pipeline.SetTextureAndSampler(ShaderStage.Compute, reservations.TopologyRemapBufferTextureBinding, bufferTexture, null);
+        }
+
+        /// <summary>
+        /// Sets the host buffer texture to a generated sequential index buffer.
+        /// </summary>
+        /// <param name="reservations">Shader resource binding reservations</param>
+        /// <param name="count">Vertex count</param>
+        private readonly void SetSequentialIndexBufferTexture(ResourceReservations reservations, int count)
+        {
+            BufferHandle sequentialIndexBuffer = _vacContext.GetSequentialIndexBuffer(count);
+
+            ITexture bufferTexture = _vacContext.EnsureBufferTexture(0, Format.R32Uint);
+            bufferTexture.SetStorage(new BufferRange(sequentialIndexBuffer, 0, count * sizeof(uint)));
+
+            _context.Renderer.Pipeline.SetTextureAndSampler(ShaderStage.Compute, reservations.IndexBufferTextureBinding, bufferTexture, null);
+        }
+
+        /// <summary>
+        /// Gets the size of a vertex buffer based on the current 3D engine state.
+        /// </summary>
+        /// <param name="vbAddress">Vertex buffer address</param>
+        /// <param name="vbEndAddress">Vertex buffer end address (exclusive)</param>
+        /// <param name="vbStride">Vertex buffer stride</param>
+        /// <param name="indexed">Whether the draw is indexed</param>
+        /// <param name="instanced">Whether the draw is instanced</param>
+        /// <param name="firstVertex">First vertex index</param>
+        /// <param name="vertexCount">Vertex count</param>
+        /// <returns>Size of the vertex buffer, in bytes</returns>
+        private readonly ulong GetVertexBufferSize(ulong vbAddress, ulong vbEndAddress, int vbStride, bool indexed, bool instanced, int firstVertex, int vertexCount)
+        {
+            IndexType indexType = _state.State.IndexBufferState.Type;
+            bool indexTypeSmall = indexType == IndexType.UByte || indexType == IndexType.UShort;
+            ulong vbSize = vbEndAddress - vbAddress + 1;
+            ulong size;
+
+            if (indexed || vbStride == 0 || instanced)
+            {
+                // This size may be (much) larger than the real vertex buffer size.
+                // Avoid calculating it this way, unless we don't have any other option.
+
+                size = vbSize;
+
+                if (vbStride > 0 && indexTypeSmall && indexed && !instanced)
+                {
+                    // If the index type is a small integer type, then we might be still able
+                    // to reduce the vertex buffer size based on the maximum possible index value.
+
+                    ulong maxVertexBufferSize = indexType == IndexType.UByte ? 0x100UL : 0x10000UL;
+
+                    maxVertexBufferSize += _state.State.FirstVertex;
+                    maxVertexBufferSize *= (uint)vbStride;
+
+                    size = Math.Min(size, maxVertexBufferSize);
+                }
+            }
+            else
+            {
+                // For non-indexed draws, we can guess the size from the vertex count
+                // and stride.
+
+                int firstInstance = (int)_state.State.FirstInstance;
+
+                size = Math.Min(vbSize, (ulong)((firstInstance + firstVertex + vertexCount) * vbStride));
+            }
+
+            return size;
+        }
+
+        /// <summary>
+        /// Aligns a size to a given alignment value.
+        /// </summary>
+        /// <param name="size">Size</param>
+        /// <param name="alignment">Alignment</param>
+        /// <returns>Aligned size</returns>
+        private static ulong Align(ulong size, int alignment)
+        {
+            ulong align = (ulong)alignment;
+
+            size += align - 1;
+
+            size /= align;
+            size *= align;
+
+            return size;
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs
index d7ee24b19..1c31312ce 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs
@@ -1,5 +1,7 @@
 using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw;
 using Ryujinx.Graphics.Gpu.Engine.Types;
+using Ryujinx.Graphics.Gpu.Image;
 using Ryujinx.Graphics.Gpu.Memory;
 using System;
 
@@ -8,7 +10,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
     /// <summary>
     /// Draw manager.
     /// </summary>
-    class DrawManager
+    class DrawManager : IDisposable
     {
         // Since we don't know the index buffer size for indirect draws,
         // we must assume a minimum and maximum size and use that for buffer data update purposes.
@@ -20,6 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
         private readonly DeviceStateWithShadow<ThreedClassState> _state;
         private readonly DrawState _drawState;
         private readonly SpecializationStateUpdater _currentSpecState;
+        private readonly VtgAsCompute _vtgAsCompute;
         private bool _topologySet;
 
         private bool _instancedDrawPending;
@@ -53,6 +56,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
             _state = state;
             _drawState = drawState;
             _currentSpecState = spec;
+            _vtgAsCompute = new(context, channel, state);
         }
 
         /// <summary>
@@ -127,7 +131,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
             {
                 if (renderEnable == ConditionalRenderEnabled.False)
                 {
-                    PerformDeferredDraws();
+                    PerformDeferredDraws(engine);
                 }
 
                 _drawState.DrawIndexed = false;
@@ -190,13 +194,13 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
 
                 _channel.BufferManager.SetIndexBuffer(br, IndexType.UInt);
 
-                _context.Renderer.Pipeline.DrawIndexed(inlineIndexCount, 1, firstIndex, firstVertex, firstInstance);
+                DrawImpl(engine, inlineIndexCount, 1, firstIndex, firstVertex, firstInstance, indexed: true);
             }
             else if (_drawState.DrawIndexed)
             {
                 int firstVertex = (int)_state.State.FirstVertex;
 
-                _context.Renderer.Pipeline.DrawIndexed(indexCount, 1, firstIndex, firstVertex, firstInstance);
+                DrawImpl(engine, indexCount, 1, firstIndex, firstVertex, firstInstance, indexed: true);
             }
             else
             {
@@ -204,7 +208,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
                 var drawState = _state.State.VertexBufferDrawState;
 #pragma warning restore IDE0059
 
-                _context.Renderer.Pipeline.Draw(drawVertexCount, 1, drawFirstVertex, firstInstance);
+                DrawImpl(engine, drawVertexCount, 1, 0, drawFirstVertex, firstInstance, indexed: false);
             }
 
             _drawState.DrawIndexed = false;
@@ -219,24 +223,26 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
         /// Starts draw.
         /// This sets primitive type and instanced draw parameters.
         /// </summary>
+        /// <param name="engine">3D engine where this method is being called</param>
         /// <param name="argument">Method call argument</param>
-        public void DrawBegin(int argument)
+        public void DrawBegin(ThreedClass engine, int argument)
         {
             bool incrementInstance = (argument & (1 << 26)) != 0;
             bool resetInstance = (argument & (1 << 27)) == 0;
 
             PrimitiveType type = (PrimitiveType)(argument & 0xffff);
-            DrawBegin(incrementInstance, resetInstance, type);
+            DrawBegin(engine, incrementInstance, resetInstance, type);
         }
 
         /// <summary>
         /// Starts draw.
         /// This sets primitive type and instanced draw parameters.
         /// </summary>
+        /// <param name="engine">3D engine where this method is being called</param>
         /// <param name="incrementInstance">Indicates if the current instance should be incremented</param>
         /// <param name="resetInstance">Indicates if the current instance should be set to zero</param>
         /// <param name="primitiveType">Primitive type</param>
-        private void DrawBegin(bool incrementInstance, bool resetInstance, PrimitiveType primitiveType)
+        private void DrawBegin(ThreedClass engine, bool incrementInstance, bool resetInstance, PrimitiveType primitiveType)
         {
             if (incrementInstance)
             {
@@ -244,7 +250,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
             }
             else if (resetInstance)
             {
-                PerformDeferredDraws();
+                PerformDeferredDraws(engine);
 
                 _instanceIndex = 0;
             }
@@ -364,7 +370,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
         /// <param name="instanced">True to increment the current instance value, false otherwise</param>
         private void DrawIndexBufferBeginEndInstance(ThreedClass engine, int argument, bool instanced)
         {
-            DrawBegin(instanced, !instanced, (PrimitiveType)((argument >> 28) & 0xf));
+            DrawBegin(engine, instanced, !instanced, (PrimitiveType)((argument >> 28) & 0xf));
 
             int firstIndex = argument & 0xffff;
             int indexCount = (argument >> 16) & 0xfff;
@@ -409,7 +415,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
         /// <param name="instanced">True to increment the current instance value, false otherwise</param>
         private void DrawVertexArrayBeginEndInstance(ThreedClass engine, int argument, bool instanced)
         {
-            DrawBegin(instanced, !instanced, (PrimitiveType)((argument >> 28) & 0xf));
+            DrawBegin(engine, instanced, !instanced, (PrimitiveType)((argument >> 28) & 0xf));
 
             int firstVertex = argument & 0xffff;
             int vertexCount = (argument >> 16) & 0xfff;
@@ -541,23 +547,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
 
             engine.UpdateState();
 
-            if (instanceCount > 1)
-            {
-                // Must be called after UpdateState as it assumes the shader state
-                // has already been set, and that bindings have been updated already.
-
-                _channel.BufferManager.SetInstancedDrawVertexCount(count);
-            }
+            DrawImpl(engine, count, instanceCount, firstIndex, firstVertex, firstInstance, indexed);
 
             if (indexed)
             {
-                _context.Renderer.Pipeline.DrawIndexed(count, instanceCount, firstIndex, firstVertex, firstInstance);
                 _state.State.FirstVertex = 0;
             }
-            else
-            {
-                _context.Renderer.Pipeline.Draw(count, instanceCount, firstVertex, firstInstance);
-            }
 
             _state.State.FirstInstance = 0;
 
@@ -569,6 +564,67 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
             }
         }
 
+        /// <summary>
+        /// Performs a indexed or non-indexed draw.
+        /// </summary>
+        /// <param name="engine">3D engine where this method is being called</param>
+        /// <param name="count">Index count for indexed draws, vertex count for non-indexed draws</param>
+        /// <param name="instanceCount">Instance count</param>
+        /// <param name="firstIndex">First index on the index buffer for indexed draws, ignored for non-indexed draws</param>
+        /// <param name="firstVertex">First vertex on the vertex buffer</param>
+        /// <param name="firstInstance">First instance</param>
+        /// <param name="indexed">True if the draw is indexed, false otherwise</param>
+        private void DrawImpl(
+            ThreedClass engine,
+            int count,
+            int instanceCount,
+            int firstIndex,
+            int firstVertex,
+            int firstInstance,
+            bool indexed)
+        {
+            if (instanceCount > 1)
+            {
+                _channel.BufferManager.SetInstancedDrawVertexCount(count);
+            }
+
+            if (_drawState.VertexAsCompute != null)
+            {
+                _vtgAsCompute.DrawAsCompute(
+                    engine,
+                    _drawState.VertexAsCompute,
+                    _drawState.GeometryAsCompute,
+                    _drawState.VertexPassthrough,
+                    _drawState.Topology,
+                    count,
+                    instanceCount,
+                    firstIndex,
+                    firstVertex,
+                    firstInstance,
+                    indexed);
+
+                if (_drawState.GeometryAsCompute != null)
+                {
+                    // Geometry draws need to change the topology, so we need to set it here again
+                    // if we are going to do a regular draw.
+                    // Would have been better to do that on the callee, but doing it here
+                    // avoids having to pass the draw manager instance.
+                    ForceStateDirty();
+                }
+            }
+            else
+            {
+                if (indexed)
+                {
+                    _context.Renderer.Pipeline.DrawIndexed(count, instanceCount, firstIndex, firstVertex, firstInstance);
+                }
+                else
+                {
+                    _context.Renderer.Pipeline.Draw(count, instanceCount, firstVertex, firstInstance);
+                }
+            }
+        }
+
         /// <summary>
         /// Performs a indirect draw, with parameters from a GPU buffer.
         /// </summary>
@@ -667,43 +723,42 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
         /// Once we detect the last instanced draw, then we perform the host instanced draw,
         /// with the accumulated instance count.
         /// </summary>
-        public void PerformDeferredDraws()
+        /// <param name="engine">3D engine where this method is being called</param>
+        public void PerformDeferredDraws(ThreedClass engine)
         {
             // Perform any pending instanced draw.
             if (_instancedDrawPending)
             {
                 _instancedDrawPending = false;
 
+                int instanceCount = _instanceIndex + 1;
+                int firstInstance = _instancedFirstInstance;
                 bool indexedInline = _instancedIndexedInline;
 
                 if (_instancedIndexed || indexedInline)
                 {
+                    int indexCount = _instancedIndexCount;
+
                     if (indexedInline)
                     {
                         int inlineIndexCount = _drawState.IbStreamer.GetAndResetInlineIndexCount(_context.Renderer);
                         BufferRange br = new(_drawState.IbStreamer.GetInlineIndexBuffer(), 0, inlineIndexCount * 4);
 
                         _channel.BufferManager.SetIndexBuffer(br, IndexType.UInt);
+                        indexCount = inlineIndexCount;
                     }
 
-                    _channel.BufferManager.SetInstancedDrawVertexCount(_instancedIndexCount);
+                    int firstIndex = _instancedFirstIndex;
+                    int firstVertex = _instancedFirstVertex;
 
-                    _context.Renderer.Pipeline.DrawIndexed(
-                        _instancedIndexCount,
-                        _instanceIndex + 1,
-                        _instancedFirstIndex,
-                        _instancedFirstVertex,
-                        _instancedFirstInstance);
+                    DrawImpl(engine, indexCount, instanceCount, firstIndex, firstVertex, firstInstance, indexed: true);
                 }
                 else
                 {
-                    _channel.BufferManager.SetInstancedDrawVertexCount(_instancedDrawStateCount);
+                    int vertexCount = _instancedDrawStateCount;
+                    int firstVertex = _instancedDrawStateFirst;
 
-                    _context.Renderer.Pipeline.Draw(
-                        _instancedDrawStateCount,
-                        _instanceIndex + 1,
-                        _instancedDrawStateFirst,
-                        _instancedFirstInstance);
+                    DrawImpl(engine, vertexCount, instanceCount, 0, firstVertex, firstInstance, indexed: false);
                 }
             }
         }
@@ -752,25 +807,69 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
                 updateFlags |= RenderTargetUpdateFlags.Layered;
             }
 
-            if (clearDepth || clearStencil)
+            bool clearDS = clearDepth || clearStencil;
+
+            if (clearDS)
             {
                 updateFlags |= RenderTargetUpdateFlags.UpdateDepthStencil;
             }
 
-            engine.UpdateRenderTargetState(updateFlags, singleUse: componentMask != 0 ? index : -1);
-
             // If there is a mismatch on the host clip region and the one explicitly defined by the guest
             // on the screen scissor state, then we need to force only one texture to be bound to avoid
             // host clipping.
             var screenScissorState = _state.State.ScreenScissorState;
 
+            bool clearAffectedByStencilMask = (_state.State.ClearFlags & 1) != 0;
+            bool clearAffectedByScissor = (_state.State.ClearFlags & 0x100) != 0;
+
+            if (clearDS || componentMask == 15)
+            {
+                // A full clear if scissor is disabled, or it matches the screen scissor state.
+
+                bool fullClear = screenScissorState.X == 0 && screenScissorState.Y == 0;
+
+                if (fullClear && clearAffectedByScissor && _state.State.ScissorState[0].Enable)
+                {
+                    ref var scissorState = ref _state.State.ScissorState[0];
+
+                    fullClear = scissorState.X1 == screenScissorState.X &&
+                        scissorState.Y1 == screenScissorState.Y &&
+                        scissorState.X2 >= screenScissorState.X + screenScissorState.Width &&
+                        scissorState.Y2 >= screenScissorState.Y + screenScissorState.Height;
+                }
+
+                if (fullClear && clearDS)
+                {
+                    // Must clear all aspects of the depth-stencil format.
+
+                    FormatInfo dsFormat = _state.State.RtDepthStencilState.Format.Convert();
+
+                    bool hasDepth = dsFormat.Format.HasDepth();
+                    bool hasStencil = dsFormat.Format.HasStencil();
+
+                    if (hasStencil && (!clearStencil || (clearAffectedByStencilMask && _state.State.StencilTestState.FrontMask != 0xff)))
+                    {
+                        fullClear = false;
+                    }
+                    else if (hasDepth && !clearDepth)
+                    {
+                        fullClear = false;
+                    }
+                }
+
+                if (fullClear)
+                {
+                    updateFlags |= RenderTargetUpdateFlags.DiscardClip;
+                }
+            }
+
+            engine.UpdateRenderTargetState(updateFlags, singleUse: componentMask != 0 ? index : -1);
+
             // Must happen after UpdateRenderTargetState to have up-to-date clip region values.
             bool clipMismatch = (screenScissorState.X | screenScissorState.Y) != 0 ||
                                 screenScissorState.Width != _channel.TextureManager.ClipRegionWidth ||
                                 screenScissorState.Height != _channel.TextureManager.ClipRegionHeight;
 
-            bool clearAffectedByStencilMask = (_state.State.ClearFlags & 1) != 0;
-            bool clearAffectedByScissor = (_state.State.ClearFlags & 0x100) != 0;
             bool needsCustomScissor = !clearAffectedByScissor || clipMismatch;
 
             // Scissor and rasterizer discard also affect clears.
@@ -866,5 +965,19 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
                 _context.Renderer.Pipeline.EndHostConditionalRendering();
             }
         }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                _vtgAsCompute.Dispose();
+            }
+        }
+
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
     }
 }
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawState.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawState.cs
index 12099aef9..cb43b0025 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawState.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawState.cs
@@ -1,4 +1,5 @@
 using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.Gpu.Shader;
 
 namespace Ryujinx.Graphics.Gpu.Engine.Threed
 {
@@ -61,5 +62,20 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
         /// Index buffer data streamer for inline index buffer updates, such as those used in legacy OpenGL.
         /// </summary>
         public IbStreamer IbStreamer = new();
+
+        /// <summary>
+        /// If the vertex shader is emulated on compute, this should be set to the compute program, otherwise it should be null.
+        /// </summary>
+        public ShaderAsCompute VertexAsCompute;
+
+        /// <summary>
+        /// If a geometry shader exists and is emulated on compute, this should be set to the compute program, otherwise it should be null.
+        /// </summary>
+        public ShaderAsCompute GeometryAsCompute;
+
+        /// <summary>
+        /// If the vertex shader is emulated on compute, this should be set to the passthrough vertex program, otherwise it should be null.
+        /// </summary>
+        public IProgram VertexPassthrough;
     }
 }
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/RenderTargetUpdateFlags.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/RenderTargetUpdateFlags.cs
index e575923d9..58c7bdb44 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/RenderTargetUpdateFlags.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/RenderTargetUpdateFlags.cs
@@ -33,6 +33,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
         /// </summary>
         UpdateDepthStencil = 1 << 3,
 
+        /// <summary>
+        /// Indicates that the data in the clip region can be discarded for the next use.
+        /// </summary>
+        DiscardClip = 1 << 4,
+
         /// <summary>
         /// Default update flags for draw.
         /// </summary>
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/SpecializationStateUpdater.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/SpecializationStateUpdater.cs
index e0607fbfe..4fbbee3bc 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/SpecializationStateUpdater.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/SpecializationStateUpdater.cs
@@ -218,11 +218,13 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
         {
             bool changed = false;
             ref Array32<AttributeType> attributeTypes = ref _graphics.AttributeTypes;
-            bool supportsScaledFormats = _context.Capabilities.SupportsScaledVertexFormats;
+            bool mayConvertVtgToCompute = ShaderCache.MayConvertVtgToCompute(ref _context.Capabilities);
+            bool supportsScaledFormats = _context.Capabilities.SupportsScaledVertexFormats && !mayConvertVtgToCompute;
 
             for (int location = 0; location < state.Length; location++)
             {
                 VertexAttribType type = state[location].UnpackType();
+                VertexAttribSize size = state[location].UnpackSize();
 
                 AttributeType value;
 
@@ -247,6 +249,18 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
                     };
                 }
 
+                if (mayConvertVtgToCompute && (size == VertexAttribSize.Rgb10A2 || size == VertexAttribSize.Rg11B10))
+                {
+                    value |= AttributeType.Packed;
+
+                    if (type == VertexAttribType.Snorm ||
+                        type == VertexAttribType.Sint ||
+                        type == VertexAttribType.Sscaled)
+                    {
+                        value |= AttributeType.PackedRgb10A2Signed;
+                    }
+                }
+
                 if (attributeTypes[location] != value)
                 {
                     attributeTypes[location] = value;
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs
index 1f919d9b2..1ff821569 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs
@@ -17,9 +17,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
     class StateUpdater
     {
         public const int ShaderStateIndex = 26;
+        public const int RtColorMaskIndex = 14;
         public const int RasterizerStateIndex = 15;
         public const int ScissorStateIndex = 16;
         public const int VertexBufferStateIndex = 0;
+        public const int BlendStateIndex = 2;
+        public const int IndexBufferStateIndex = 23;
         public const int PrimitiveRestartStateIndex = 12;
         public const int RenderTargetStateIndex = 27;
 
@@ -290,7 +293,13 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
             // of the shader for the new state.
             if (_shaderSpecState != null && _currentSpecState.HasChanged())
             {
-                if (!_shaderSpecState.MatchesGraphics(_channel, ref _currentSpecState.GetPoolState(), ref _currentSpecState.GetGraphicsState(), _vsUsesDrawParameters, false))
+                if (!_shaderSpecState.MatchesGraphics(
+                    _channel,
+                    ref _currentSpecState.GetPoolState(),
+                    ref _currentSpecState.GetGraphicsState(),
+                    _drawState.VertexAsCompute != null,
+                    _vsUsesDrawParameters,
+                    checkTextures: false))
                 {
                     // Shader must be reloaded. _vtgWritesRtLayer should not change.
                     UpdateShaderState();
@@ -440,6 +449,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
             bool useControl = updateFlags.HasFlag(RenderTargetUpdateFlags.UseControl);
             bool layered = updateFlags.HasFlag(RenderTargetUpdateFlags.Layered);
             bool singleColor = updateFlags.HasFlag(RenderTargetUpdateFlags.SingleColor);
+            bool discard = updateFlags.HasFlag(RenderTargetUpdateFlags.DiscardClip);
 
             int count = useControl ? rtControl.UnpackCount() : Constants.TotalRenderTargets;
 
@@ -479,6 +489,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
                     memoryManager,
                     colorState,
                     _vtgWritesRtLayer || layered,
+                    discard,
                     samplesInX,
                     samplesInY,
                     sizeHint);
@@ -518,6 +529,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
                     dsState,
                     dsSize,
                     _vtgWritesRtLayer || layered,
+                    discard,
                     samplesInX,
                     samplesInY,
                     sizeHint);
@@ -1453,6 +1465,19 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
                 _fsReadsFragCoord = false;
             }
 
+            if (gs.VertexAsCompute != null)
+            {
+                _drawState.VertexAsCompute = gs.VertexAsCompute;
+                _drawState.GeometryAsCompute = gs.GeometryAsCompute;
+                _drawState.VertexPassthrough = gs.HostProgram;
+            }
+            else
+            {
+                _drawState.VertexAsCompute = null;
+                _drawState.GeometryAsCompute = null;
+                _drawState.VertexPassthrough = null;
+            }
+
             _context.Renderer.Pipeline.SetProgram(gs.HostProgram);
         }
 
@@ -1540,5 +1565,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
         {
             _updateTracker.ForceDirty(ShaderStateIndex);
         }
+
+        /// <summary>
+        /// Forces a register group as dirty, by index.
+        /// </summary>
+        /// <param name="groupIndex">Index of the group to be dirtied</param>
+        public void ForceDirty(int groupIndex)
+        {
+            _updateTracker.ForceDirty(groupIndex);
+        }
     }
 }
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClass.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClass.cs
index 1f6628909..df9d1f5c9 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClass.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClass.cs
@@ -1,19 +1,22 @@
-using Ryujinx.Graphics.Device;
+using Ryujinx.Common.Memory;
+using Ryujinx.Graphics.Device;
 using Ryujinx.Graphics.GAL;
 using Ryujinx.Graphics.Gpu.Engine.GPFifo;
 using Ryujinx.Graphics.Gpu.Engine.InlineToMemory;
 using Ryujinx.Graphics.Gpu.Engine.Threed.Blender;
+using Ryujinx.Graphics.Gpu.Engine.Types;
 using Ryujinx.Graphics.Gpu.Synchronization;
 using System;
 using System.Collections.Generic;
 using System.Runtime.CompilerServices;
+using System.Runtime.Intrinsics;
 
 namespace Ryujinx.Graphics.Gpu.Engine.Threed
 {
     /// <summary>
     /// Represents a 3D engine class.
     /// </summary>
-    class ThreedClass : IDeviceState
+    class ThreedClass : IDeviceState, IDisposable
     {
         private readonly GpuContext _context;
         private readonly GPFifoClass _fifoClass;
@@ -26,6 +29,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
         private readonly ConstantBufferUpdater _cbUpdater;
         private readonly StateUpdater _stateUpdater;
 
+        private SetMmeShadowRamControlMode ShadowMode => _state.State.SetMmeShadowRamControlMode;
+
         /// <summary>
         /// Creates a new instance of the 3D engine class.
         /// </summary>
@@ -178,6 +183,15 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
             _stateUpdater.SetDirty(offset);
         }
 
+        /// <summary>
+        /// Marks the specified register range for a group index as dirty, forcing the associated state to update on the next draw.
+        /// </summary>
+        /// <param name="groupIndex">Index of the group to dirty</param>
+        public void ForceStateDirtyByIndex(int groupIndex)
+        {
+            _stateUpdater.ForceDirty(groupIndex);
+        }
+
         /// <summary>
         /// Forces the shaders to be rebound on the next draw.
         /// </summary>
@@ -207,7 +221,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
         /// </summary>
         public void PerformDeferredDraws()
         {
-            _drawManager.PerformDeferredDraws();
+            _drawManager.PerformDeferredDraws(this);
         }
 
         /// <summary>
@@ -219,6 +233,206 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
             _cbUpdater.Update(data);
         }
 
+        /// <summary>
+        /// Test if two 32 byte structs are equal. 
+        /// </summary>
+        /// <typeparam name="T">Type of the 32-byte struct</typeparam>
+        /// <param name="lhs">First struct</param>
+        /// <param name="rhs">Second struct</param>
+        /// <returns>True if equal, false otherwise</returns>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static bool UnsafeEquals32Byte<T>(ref T lhs, ref T rhs) where T : unmanaged
+        {
+            if (Vector256.IsHardwareAccelerated)
+            {
+                return Vector256.EqualsAll(
+                    Unsafe.As<T, Vector256<uint>>(ref lhs),
+                    Unsafe.As<T, Vector256<uint>>(ref rhs)
+                );
+            }
+            else
+            {
+                ref var lhsVec = ref Unsafe.As<T, Vector128<uint>>(ref lhs);
+                ref var rhsVec = ref Unsafe.As<T, Vector128<uint>>(ref rhs);
+
+                return Vector128.EqualsAll(lhsVec, rhsVec) &&
+                    Vector128.EqualsAll(Unsafe.Add(ref lhsVec, 1), Unsafe.Add(ref rhsVec, 1));
+            }
+        }
+
+        /// <summary>
+        /// Updates blend enable. Respects current shadow mode.
+        /// </summary>
+        /// <param name="masks">Blend enable</param>
+        public void UpdateBlendEnable(ref Array8<Boolean32> enable)
+        {
+            var shadow = ShadowMode;
+            ref var state = ref _state.State.BlendEnable;
+
+            if (shadow.IsReplay())
+            {
+                enable = _state.ShadowState.BlendEnable;
+            }
+
+            if (!UnsafeEquals32Byte(ref enable, ref state))
+            {
+                state = enable;
+
+                _stateUpdater.ForceDirty(StateUpdater.BlendStateIndex);
+            }
+
+            if (shadow.IsTrack())
+            {
+                _state.ShadowState.BlendEnable = enable;
+            }
+        }
+
+        /// <summary>
+        /// Updates color masks. Respects current shadow mode.
+        /// </summary>
+        /// <param name="masks">Color masks</param>
+        public void UpdateColorMasks(ref Array8<RtColorMask> masks)
+        {
+            var shadow = ShadowMode;
+            ref var state = ref _state.State.RtColorMask;
+
+            if (shadow.IsReplay())
+            {
+                masks = _state.ShadowState.RtColorMask;
+            }
+
+            if (!UnsafeEquals32Byte(ref masks, ref state))
+            {
+                state = masks;
+
+                _stateUpdater.ForceDirty(StateUpdater.RtColorMaskIndex);
+            }
+
+            if (shadow.IsTrack())
+            {
+                _state.ShadowState.RtColorMask = masks;
+            }
+        }
+
+        /// <summary>
+        /// Updates index buffer state for an indexed draw. Respects current shadow mode.
+        /// </summary>
+        /// <param name="addrHigh">High part of the address</param>
+        /// <param name="addrLow">Low part of the address</param>
+        /// <param name="type">Type of the binding</param>
+        public void UpdateIndexBuffer(uint addrHigh, uint addrLow, IndexType type)
+        {
+            var shadow = ShadowMode;
+            ref var state = ref _state.State.IndexBufferState;
+
+            if (shadow.IsReplay())
+            {
+                ref var shadowState = ref _state.ShadowState.IndexBufferState;
+                addrHigh = shadowState.Address.High;
+                addrLow = shadowState.Address.Low;
+                type = shadowState.Type;
+            }
+
+            if (state.Address.High != addrHigh || state.Address.Low != addrLow || state.Type != type)
+            {
+                state.Address.High = addrHigh;
+                state.Address.Low = addrLow;
+                state.Type = type;
+
+                _stateUpdater.ForceDirty(StateUpdater.IndexBufferStateIndex);
+            }
+
+            if (shadow.IsTrack())
+            {
+                ref var shadowState = ref _state.ShadowState.IndexBufferState;
+                shadowState.Address.High = addrHigh;
+                shadowState.Address.Low = addrLow;
+                shadowState.Type = type;
+            }
+        }
+
+        /// <summary>
+        /// Updates uniform buffer state for update or bind. Respects current shadow mode.
+        /// </summary>
+        /// <param name="size">Size of the binding</param>
+        /// <param name="addrHigh">High part of the addrsss</param>
+        /// <param name="addrLow">Low part of the address</param>
+        public void UpdateUniformBufferState(int size, uint addrHigh, uint addrLow)
+        {
+            var shadow = ShadowMode;
+            ref var state = ref _state.State.UniformBufferState;
+
+            if (shadow.IsReplay())
+            {
+                ref var shadowState = ref _state.ShadowState.UniformBufferState;
+                size = shadowState.Size;
+                addrHigh = shadowState.Address.High;
+                addrLow = shadowState.Address.Low;
+            }
+
+            state.Size = size;
+            state.Address.High = addrHigh;
+            state.Address.Low = addrLow;
+
+            if (shadow.IsTrack())
+            {
+                ref var shadowState = ref _state.ShadowState.UniformBufferState;
+                shadowState.Size = size;
+                shadowState.Address.High = addrHigh;
+                shadowState.Address.Low = addrLow;
+            }
+        }
+
+        /// <summary>
+        /// Updates a shader offset. Respects current shadow mode.
+        /// </summary>
+        /// <param name="index">Index of the shader to update</param>
+        /// <param name="offset">Offset to update with</param>
+        public void SetShaderOffset(int index, uint offset)
+        {
+            var shadow = ShadowMode;
+            ref var shaderState = ref _state.State.ShaderState[index];
+
+            if (shadow.IsReplay())
+            {
+                offset = _state.ShadowState.ShaderState[index].Offset;
+            }
+
+            if (shaderState.Offset != offset)
+            {
+                shaderState.Offset = offset;
+
+                _stateUpdater.ForceDirty(StateUpdater.ShaderStateIndex);
+            }
+
+            if (shadow.IsTrack())
+            {
+                _state.ShadowState.ShaderState[index].Offset = offset;
+            }
+        }
+
+        /// <summary>
+        /// Updates uniform buffer state for update. Respects current shadow mode.
+        /// </summary>
+        /// <param name="ubState">Uniform buffer state</param>
+        public void UpdateUniformBufferState(UniformBufferState ubState)
+        {
+            var shadow = ShadowMode;
+            ref var state = ref _state.State.UniformBufferState;
+
+            if (shadow.IsReplay())
+            {
+                ubState = _state.ShadowState.UniformBufferState;
+            }
+
+            state = ubState;
+
+            if (shadow.IsTrack())
+            {
+                _state.ShadowState.UniformBufferState = ubState;
+            }
+        }
+
         /// <summary>
         /// Launches the Inline-to-Memory DMA copy operation.
         /// </summary>
@@ -402,7 +616,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
         /// <param name="argument">Method call argument</param>
         private void DrawBegin(int argument)
         {
-            _drawManager.DrawBegin(argument);
+            _drawManager.DrawBegin(this, argument);
         }
 
         /// <summary>
@@ -617,5 +831,19 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
         {
             _drawManager.Clear(this, argument, layerCount);
         }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                _drawManager.Dispose();
+            }
+        }
+
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
     }
 }
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClassState.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClassState.cs
index f2997678c..45284525c 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClassState.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClassState.cs
@@ -590,9 +590,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
     /// </summary>
     struct RtColorMask
     {
-#pragma warning disable CS0649 // Field is never assigned to
         public uint Packed;
-#pragma warning restore CS0649
+
+        public RtColorMask(uint packed)
+        {
+            Packed = packed;
+        }
 
         /// <summary>
         /// Unpacks red channel enable.
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Types/Boolean32.cs b/src/Ryujinx.Graphics.Gpu/Engine/Types/Boolean32.cs
index 911ad53b4..7293fab9c 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/Types/Boolean32.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Types/Boolean32.cs
@@ -5,9 +5,12 @@
     /// </summary>
     readonly struct Boolean32
     {
-#pragma warning disable CS0649 // Field is never assigned to
         private readonly uint _value;
-#pragma warning restore CS0649
+
+        public Boolean32(uint value)
+        {
+            _value = value;
+        }
 
         public static implicit operator bool(Boolean32 value)
         {
diff --git a/src/Ryujinx.Graphics.Gpu/GpuChannel.cs b/src/Ryujinx.Graphics.Gpu/GpuChannel.cs
index 8fe643815..d70c9645e 100644
--- a/src/Ryujinx.Graphics.Gpu/GpuChannel.cs
+++ b/src/Ryujinx.Graphics.Gpu/GpuChannel.cs
@@ -135,6 +135,7 @@ namespace Ryujinx.Graphics.Gpu
         /// </summary>
         private void Destroy()
         {
+            _processor.Dispose();
             TextureManager.Dispose();
 
             var oldMemoryManager = Interlocked.Exchange(ref _memoryManager, null);
diff --git a/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs b/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs
index fcc6b8cfc..1b517e63f 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs
@@ -557,6 +557,91 @@ namespace Ryujinx.Graphics.Gpu.Image
         };
 #pragma warning restore IDE0055
 
+        // Note: Some of those formats have been changed and requires conversion on the shader,
+        // as GPUs don't support them when used as buffer texture format.
+        private static readonly Dictionary<VertexAttributeFormat, (Format, int)> _singleComponentAttribFormats = new()
+        {
+            { VertexAttributeFormat.R8Unorm,             (Format.R8Unorm, 1)          },
+            { VertexAttributeFormat.R8Snorm,             (Format.R8Snorm, 1)          },
+            { VertexAttributeFormat.R8Uint,              (Format.R8Uint, 1)           },
+            { VertexAttributeFormat.R8Sint,              (Format.R8Sint, 1)           },
+            { VertexAttributeFormat.R16Float,            (Format.R16Float, 1)         },
+            { VertexAttributeFormat.R16Unorm,            (Format.R16Unorm, 1)         },
+            { VertexAttributeFormat.R16Snorm,            (Format.R16Snorm, 1)         },
+            { VertexAttributeFormat.R16Uint,             (Format.R16Uint, 1)          },
+            { VertexAttributeFormat.R16Sint,             (Format.R16Sint, 1)          },
+            { VertexAttributeFormat.R32Float,            (Format.R32Float, 1)         },
+            { VertexAttributeFormat.R32Uint,             (Format.R32Uint, 1)          },
+            { VertexAttributeFormat.R32Sint,             (Format.R32Sint, 1)          },
+            { VertexAttributeFormat.R8G8Unorm,           (Format.R8Unorm, 2)          },
+            { VertexAttributeFormat.R8G8Snorm,           (Format.R8Snorm, 2)          },
+            { VertexAttributeFormat.R8G8Uint,            (Format.R8Uint, 2)           },
+            { VertexAttributeFormat.R8G8Sint,            (Format.R8Sint, 2)           },
+            { VertexAttributeFormat.R16G16Float,         (Format.R16Float, 2)         },
+            { VertexAttributeFormat.R16G16Unorm,         (Format.R16Unorm, 2)         },
+            { VertexAttributeFormat.R16G16Snorm,         (Format.R16Snorm, 2)         },
+            { VertexAttributeFormat.R16G16Uint,          (Format.R16Uint, 2)          },
+            { VertexAttributeFormat.R16G16Sint,          (Format.R16Sint, 2)          },
+            { VertexAttributeFormat.R32G32Float,         (Format.R32Float, 2)         },
+            { VertexAttributeFormat.R32G32Uint,          (Format.R32Uint, 2)          },
+            { VertexAttributeFormat.R32G32Sint,          (Format.R32Sint, 2)          },
+            { VertexAttributeFormat.R8G8B8Unorm,         (Format.R8Unorm, 3)          },
+            { VertexAttributeFormat.R8G8B8Snorm,         (Format.R8Snorm, 3)          },
+            { VertexAttributeFormat.R8G8B8Uint,          (Format.R8Uint, 3)           },
+            { VertexAttributeFormat.R8G8B8Sint,          (Format.R8Sint, 3)           },
+            { VertexAttributeFormat.R16G16B16Float,      (Format.R16Float, 3)         },
+            { VertexAttributeFormat.R16G16B16Unorm,      (Format.R16Unorm, 3)         },
+            { VertexAttributeFormat.R16G16B16Snorm,      (Format.R16Snorm, 3)         },
+            { VertexAttributeFormat.R16G16B16Uint,       (Format.R16Uint, 3)          },
+            { VertexAttributeFormat.R16G16B16Sint,       (Format.R16Sint, 3)          },
+            { VertexAttributeFormat.R32G32B32Float,      (Format.R32Float, 3)         },
+            { VertexAttributeFormat.R32G32B32Uint,       (Format.R32Uint, 3)          },
+            { VertexAttributeFormat.R32G32B32Sint,       (Format.R32Sint, 3)          },
+            { VertexAttributeFormat.R8G8B8A8Unorm,       (Format.R8Unorm, 4)          },
+            { VertexAttributeFormat.R8G8B8A8Snorm,       (Format.R8Snorm, 4)          },
+            { VertexAttributeFormat.R8G8B8A8Uint,        (Format.R8Uint, 4)           },
+            { VertexAttributeFormat.R8G8B8A8Sint,        (Format.R8Sint, 4)           },
+            { VertexAttributeFormat.R16G16B16A16Float,   (Format.R16Float, 4)         },
+            { VertexAttributeFormat.R16G16B16A16Unorm,   (Format.R16Unorm, 4)         },
+            { VertexAttributeFormat.R16G16B16A16Snorm,   (Format.R16Snorm, 4)         },
+            { VertexAttributeFormat.R16G16B16A16Uint,    (Format.R16Uint, 4)          },
+            { VertexAttributeFormat.R16G16B16A16Sint,    (Format.R16Sint, 4)          },
+            { VertexAttributeFormat.R32G32B32A32Float,   (Format.R32Float, 4)         },
+            { VertexAttributeFormat.R32G32B32A32Uint,    (Format.R32Uint, 4)          },
+            { VertexAttributeFormat.R32G32B32A32Sint,    (Format.R32Sint, 4)          },
+            { VertexAttributeFormat.A2B10G10R10Unorm,    (Format.R10G10B10A2Unorm, 4) },
+            { VertexAttributeFormat.A2B10G10R10Uint,     (Format.R10G10B10A2Uint, 4)  },
+            { VertexAttributeFormat.B10G11R11Float,      (Format.R11G11B10Float, 3)   },
+            { VertexAttributeFormat.R8Uscaled,           (Format.R8Uint, 1)           }, // Uscaled -> Uint
+            { VertexAttributeFormat.R8Sscaled,           (Format.R8Sint, 1)           }, // Sscaled -> Sint
+            { VertexAttributeFormat.R16Uscaled,          (Format.R16Uint, 1)          }, // Uscaled -> Uint
+            { VertexAttributeFormat.R16Sscaled,          (Format.R16Sint, 1)          }, // Sscaled -> Sint
+            { VertexAttributeFormat.R32Uscaled,          (Format.R32Uint, 1)          }, // Uscaled -> Uint
+            { VertexAttributeFormat.R32Sscaled,          (Format.R32Sint, 1)          }, // Sscaled -> Sint
+            { VertexAttributeFormat.R8G8Uscaled,         (Format.R8Uint, 2)           }, // Uscaled -> Uint
+            { VertexAttributeFormat.R8G8Sscaled,         (Format.R8Sint, 2)           }, // Sscaled -> Sint
+            { VertexAttributeFormat.R16G16Uscaled,       (Format.R16Uint, 2)          }, // Uscaled -> Uint
+            { VertexAttributeFormat.R16G16Sscaled,       (Format.R16Sint, 2)          }, // Sscaled -> Sint
+            { VertexAttributeFormat.R32G32Uscaled,       (Format.R32Uint, 2)          }, // Uscaled -> Uint
+            { VertexAttributeFormat.R32G32Sscaled,       (Format.R32Sint, 2)          }, // Sscaled -> Sint
+            { VertexAttributeFormat.R8G8B8Uscaled,       (Format.R8Uint, 3)           }, // Uscaled -> Uint
+            { VertexAttributeFormat.R8G8B8Sscaled,       (Format.R8Sint, 3)           }, // Sscaled -> Sint
+            { VertexAttributeFormat.R16G16B16Uscaled,    (Format.R16Uint, 3)          }, // Uscaled -> Uint
+            { VertexAttributeFormat.R16G16B16Sscaled,    (Format.R16Sint, 3)          }, // Sscaled -> Sint
+            { VertexAttributeFormat.R32G32B32Uscaled,    (Format.R32Uint, 3)          }, // Uscaled -> Uint
+            { VertexAttributeFormat.R32G32B32Sscaled,    (Format.R32Sint , 3)         }, // Sscaled -> Sint
+            { VertexAttributeFormat.R8G8B8A8Uscaled,     (Format.R8Uint, 4)           }, // Uscaled -> Uint
+            { VertexAttributeFormat.R8G8B8A8Sscaled,     (Format.R8Sint, 4)           }, // Sscaled -> Sint
+            { VertexAttributeFormat.R16G16B16A16Uscaled, (Format.R16Uint, 4)          }, // Uscaled -> Uint
+            { VertexAttributeFormat.R16G16B16A16Sscaled, (Format.R16Sint, 4)          }, // Sscaled -> Sint
+            { VertexAttributeFormat.R32G32B32A32Uscaled, (Format.R32Uint, 4)          }, // Uscaled -> Uint
+            { VertexAttributeFormat.R32G32B32A32Sscaled, (Format.R32Sint, 4)          }, // Sscaled -> Sint
+            { VertexAttributeFormat.A2B10G10R10Snorm,    (Format.R10G10B10A2Uint, 4)  }, // Snorm -> Uint
+            { VertexAttributeFormat.A2B10G10R10Sint,     (Format.R10G10B10A2Uint, 4)  }, // Sint -> Uint
+            { VertexAttributeFormat.A2B10G10R10Uscaled,  (Format.R10G10B10A2Uint, 4)  }, // Uscaled -> Uint
+            { VertexAttributeFormat.A2B10G10R10Sscaled,  (Format.R10G10B10A2Sint, 4)  }  // Sscaled -> Sint
+        };
+
         /// <summary>
         /// Try getting the texture format from an encoded format integer from the Maxwell texture descriptor.
         /// </summary>
@@ -581,5 +666,22 @@ namespace Ryujinx.Graphics.Gpu.Image
         {
             return _attribFormats.TryGetValue((VertexAttributeFormat)encoded, out format);
         }
+
+        /// <summary>
+        /// Try getting a single component vertex attribute format from an encoded format integer from Maxwell attribute registers.
+        /// </summary>
+        /// <param name="encoded">The encoded format integer from the attribute registers</param>
+        /// <param name="format">The output single component vertex attribute format</param>
+        /// <param name="componentsCount">Number of components that the format has</param>
+        /// <returns>True if the format is valid, false otherwise</returns>
+        public static bool TryGetSingleComponentAttribFormat(uint encoded, out Format format, out int componentsCount)
+        {
+            bool result = _singleComponentAttribFormats.TryGetValue((VertexAttributeFormat)encoded, out var tuple);
+
+            format = tuple.Item1;
+            componentsCount = tuple.Item2;
+
+            return result;
+        }
     }
 }
diff --git a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs
index 0fce4debb..022a3839f 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs
@@ -570,6 +570,18 @@ namespace Ryujinx.Graphics.Gpu.Image
             return Group.CheckDirty(this, consume);
         }
 
+        /// <summary>
+        /// Discards all data for this texture.
+        /// This clears all dirty flags, modified flags, and pending copies from other textures.
+        /// It should be used if the texture data will be fully overwritten by the next use.
+        /// </summary>
+        public void DiscardData()
+        {
+            Group.DiscardData(this);
+
+            _dirty = false;
+        }
+
         /// <summary>
         /// Synchronizes guest and host memory.
         /// This will overwrite the texture data with the texture data on the guest memory, if a CPU
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs
index 3f215a4ac..5048ccca4 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs
@@ -311,7 +311,7 @@ namespace Ryujinx.Graphics.Gpu.Image
                 flags |= TextureSearchFlags.NoCreate;
             }
 
-            Texture texture = FindOrCreateTexture(memoryManager, flags, info, 0);
+            Texture texture = FindOrCreateTexture(memoryManager, flags, info, 0, sizeHint: sizeHint);
 
             texture?.SynchronizeMemory();
 
@@ -324,6 +324,7 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <param name="memoryManager">GPU memory manager where the texture is mapped</param>
         /// <param name="colorState">Color buffer texture to find or create</param>
         /// <param name="layered">Indicates if the texture might be accessed with a non-zero layer index</param>
+        /// <param name="discard">Indicates that the sizeHint region's data will be overwritten</param>
         /// <param name="samplesInX">Number of samples in the X direction, for MSAA</param>
         /// <param name="samplesInY">Number of samples in the Y direction, for MSAA</param>
         /// <param name="sizeHint">A hint indicating the minimum used size for the texture</param>
@@ -332,6 +333,7 @@ namespace Ryujinx.Graphics.Gpu.Image
             MemoryManager memoryManager,
             RtColorState colorState,
             bool layered,
+            bool discard,
             int samplesInX,
             int samplesInY,
             Size sizeHint)
@@ -398,7 +400,14 @@ namespace Ryujinx.Graphics.Gpu.Image
 
             int layerSize = !isLinear ? colorState.LayerSize * 4 : 0;
 
-            Texture texture = FindOrCreateTexture(memoryManager, TextureSearchFlags.WithUpscale, info, layerSize);
+            var flags = TextureSearchFlags.WithUpscale;
+
+            if (discard)
+            {
+                flags |= TextureSearchFlags.DiscardData;
+            }
+
+            Texture texture = FindOrCreateTexture(memoryManager, flags, info, layerSize, sizeHint: sizeHint);
 
             texture?.SynchronizeMemory();
 
@@ -412,6 +421,7 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <param name="dsState">Depth-stencil buffer texture to find or create</param>
         /// <param name="size">Size of the depth-stencil texture</param>
         /// <param name="layered">Indicates if the texture might be accessed with a non-zero layer index</param>
+        /// <param name="discard">Indicates that the sizeHint region's data will be overwritten</param>
         /// <param name="samplesInX">Number of samples in the X direction, for MSAA</param>
         /// <param name="samplesInY">Number of samples in the Y direction, for MSAA</param>
         /// <param name="sizeHint">A hint indicating the minimum used size for the texture</param>
@@ -421,6 +431,7 @@ namespace Ryujinx.Graphics.Gpu.Image
             RtDepthStencilState dsState,
             Size3D size,
             bool layered,
+            bool discard,
             int samplesInX,
             int samplesInY,
             Size sizeHint)
@@ -465,7 +476,14 @@ namespace Ryujinx.Graphics.Gpu.Image
                 target,
                 formatInfo);
 
-            Texture texture = FindOrCreateTexture(memoryManager, TextureSearchFlags.WithUpscale, info, dsState.LayerSize * 4);
+            var flags = TextureSearchFlags.WithUpscale;
+
+            if (discard)
+            {
+                flags |= TextureSearchFlags.DiscardData;
+            }
+
+            Texture texture = FindOrCreateTexture(memoryManager, flags, info, dsState.LayerSize * 4, sizeHint: sizeHint);
 
             texture?.SynchronizeMemory();
 
@@ -500,6 +518,37 @@ namespace Ryujinx.Graphics.Gpu.Image
             return Math.Clamp(widthAligned - alignment + 1, minimumWidth, widthAligned);
         }
 
+        /// <summary>
+        /// Determines if texture data should be fully discarded
+        /// based on the size hint region and whether it is set to be discarded.
+        /// </summary>
+        /// <param name="discard">Whether the size hint region should be discarded</param>
+        /// <param name="texture">The texture being discarded</param>
+        /// <param name="sizeHint">A hint indicating the minimum used size for the texture</param>
+        /// <returns>True if the data should be discarded, false otherwise</returns>
+        private static bool ShouldDiscard(bool discard, Texture texture, Size? sizeHint)
+        {
+            return discard &&
+                texture.Info.DepthOrLayers == 1 &&
+                sizeHint != null &&
+                texture.Width <= sizeHint.Value.Width &&
+                texture.Height <= sizeHint.Value.Height;
+        }
+
+        /// <summary>
+        /// Discards texture data if requested and possible.
+        /// </summary>
+        /// <param name="discard">Whether the size hint region should be discarded</param>
+        /// <param name="texture">The texture being discarded</param>
+        /// <param name="sizeHint">A hint indicating the minimum used size for the texture</param>
+        private static void DiscardIfNeeded(bool discard, Texture texture, Size? sizeHint)
+        {
+            if (ShouldDiscard(discard, texture, sizeHint))
+            {
+                texture.DiscardData();
+            }
+        }
+
         /// <summary>
         /// Tries to find an existing texture, or create a new one if not found.
         /// </summary>
@@ -507,6 +556,7 @@ namespace Ryujinx.Graphics.Gpu.Image
         /// <param name="flags">The texture search flags, defines texture comparison rules</param>
         /// <param name="info">Texture information of the texture to be found or created</param>
         /// <param name="layerSize">Size in bytes of a single texture layer</param>
+        /// <param name="sizeHint">A hint indicating the minimum used size for the texture</param>
         /// <param name="range">Optional ranges of physical memory where the texture data is located</param>
         /// <returns>The texture</returns>
         public Texture FindOrCreateTexture(
@@ -514,9 +564,11 @@ namespace Ryujinx.Graphics.Gpu.Image
             TextureSearchFlags flags,
             TextureInfo info,
             int layerSize = 0,
+            Size? sizeHint = null,
             MultiRange? range = null)
         {
             bool isSamplerTexture = (flags & TextureSearchFlags.ForSampler) != 0;
+            bool discard = (flags & TextureSearchFlags.DiscardData) != 0;
 
             TextureScaleMode scaleMode = IsUpscaleCompatible(info, (flags & TextureSearchFlags.WithUpscale) != 0);
 
@@ -612,6 +664,8 @@ namespace Ryujinx.Graphics.Gpu.Image
 
             if (texture != null)
             {
+                DiscardIfNeeded(discard, texture, sizeHint);
+
                 texture.SynchronizeMemory();
 
                 return texture;
@@ -642,11 +696,14 @@ namespace Ryujinx.Graphics.Gpu.Image
             }
 
             // Find view compatible matches.
-            int overlapsCount;
+            int overlapsCount = 0;
 
-            lock (_textures)
+            if (info.Target != Target.TextureBuffer)
             {
-                overlapsCount = _textures.FindOverlaps(range.Value, ref _textureOverlaps);
+                lock (_textures)
+                {
+                    overlapsCount = _textures.FindOverlaps(range.Value, ref _textureOverlaps);
+                }
             }
 
             if (_overlapInfo.Length != _textureOverlaps.Length)
@@ -907,7 +964,7 @@ namespace Ryujinx.Graphics.Gpu.Image
 
                 // We need to synchronize before copying the old view data to the texture,
                 // otherwise the copied data would be overwritten by a future synchronization.
-                texture.InitializeData(false, setData);
+                texture.InitializeData(false, setData && !ShouldDiscard(discard, texture, sizeHint));
 
                 texture.Group.InitializeOverlaps();
 
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
index 1b947cd3b..746a95ffc 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
@@ -79,6 +79,7 @@ namespace Ryujinx.Graphics.Gpu.Image
         private int[] _allOffsets;
         private int[] _sliceSizes;
         private readonly bool _is3D;
+        private readonly bool _isBuffer;
         private bool _hasMipViews;
         private bool _hasLayerViews;
         private readonly int _layers;
@@ -118,6 +119,7 @@ namespace Ryujinx.Graphics.Gpu.Image
             _physicalMemory = physicalMemory;
 
             _is3D = storage.Info.Target == Target.Texture3D;
+            _isBuffer = storage.Info.Target == Target.TextureBuffer;
             _layers = storage.Info.GetSlices();
             _levels = storage.Info.Levels;
 
@@ -278,6 +280,24 @@ namespace Ryujinx.Graphics.Gpu.Image
             return dirty;
         }
 
+        /// <summary>
+        /// Discards all data for a given texture.
+        /// This clears all dirty flags, modified flags, and pending copies from other textures.
+        /// </summary>
+        /// <param name="texture">The texture being discarded</param>
+        public void DiscardData(Texture texture)
+        {
+            EvaluateRelevantHandles(texture, (baseHandle, regionCount, split) =>
+            {
+                for (int i = 0; i < regionCount; i++)
+                {
+                    TextureGroupHandle group = _handles[baseHandle + i];
+
+                    group.DiscardData();
+                }
+            });
+        }
+
         /// <summary>
         /// Synchronize memory for a given texture.
         /// If overlapping tracking handles are dirty, fully or partially synchronize the texture data.
@@ -776,7 +796,11 @@ namespace Ryujinx.Graphics.Gpu.Image
             int targetLayerHandles = _hasLayerViews ? slices : 1;
             int targetLevelHandles = _hasMipViews ? levels : 1;
 
-            if (_is3D)
+            if (_isBuffer)
+            {
+                return;
+            }
+            else if (_is3D)
             {
                 // Future mip levels come after all layers of the last mip level. Each mipmap has less layers (depth) than the last.
 
@@ -1309,7 +1333,11 @@ namespace Ryujinx.Graphics.Gpu.Image
         {
             TextureGroupHandle[] handles;
 
-            if (!(_hasMipViews || _hasLayerViews))
+            if (_isBuffer)
+            {
+                handles = Array.Empty<TextureGroupHandle>();
+            }
+            else if (!(_hasMipViews || _hasLayerViews))
             {
                 // Single dirty region.
                 var cpuRegionHandles = new RegionHandle[TextureRange.Count];
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs
index ef7198e88..84171c7a9 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs
@@ -2,7 +2,6 @@
 using Ryujinx.Memory.Tracking;
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using System.Threading;
 
 namespace Ryujinx.Graphics.Gpu.Image
@@ -155,6 +154,24 @@ namespace Ryujinx.Graphics.Gpu.Image
             }
         }
 
+        /// <summary>
+        /// Discards all data for this handle.
+        /// This clears all dirty flags, modified flags, and pending copies from other handles.
+        /// </summary>
+        public void DiscardData()
+        {
+            Modified = false;
+            DeferredCopy = null;
+
+            foreach (RegionHandle handle in Handles)
+            {
+                if (handle.Dirty)
+                {
+                    handle.Reprotect();
+                }
+            }
+        }
+
         /// <summary>
         /// Calculate a list of which views overlap this handle.
         /// </summary>
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureSearchFlags.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureSearchFlags.cs
index fb2a97b0b..f651420a7 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TextureSearchFlags.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureSearchFlags.cs
@@ -14,5 +14,6 @@ namespace Ryujinx.Graphics.Gpu.Image
         DepthAlias = 1 << 3,
         WithUpscale = 1 << 4,
         NoCreate = 1 << 5,
+        DiscardData = 1 << 6,
     }
 }
diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs
index bf4cb5d05..8e9b4b858 100644
--- a/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs
+++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs
@@ -6,7 +6,6 @@ using Ryujinx.Graphics.Shader;
 using System;
 using System.Collections.Generic;
 using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
 
 namespace Ryujinx.Graphics.Gpu.Memory
 {
@@ -15,9 +14,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
     /// </summary>
     class BufferManager
     {
-        private const int TfInfoVertexCountOffset = Constants.TotalTransformFeedbackBuffers * sizeof(int);
-        private const int TfInfoBufferSize = TfInfoVertexCountOffset + sizeof(int);
-
         private readonly GpuContext _context;
         private readonly GpuChannel _channel;
 
@@ -104,9 +100,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
         private readonly BuffersPerStage[] _gpStorageBuffers;
         private readonly BuffersPerStage[] _gpUniformBuffers;
 
-        private BufferHandle _tfInfoBuffer;
-        private readonly int[] _tfInfoData;
-
         private bool _gpStorageBuffersDirty;
         private bool _gpUniformBuffersDirty;
 
@@ -146,11 +139,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
             _bufferTextures = new List<BufferTextureBinding>();
 
             _ranges = new BufferAssignment[Constants.TotalGpUniformBuffers * Constants.ShaderStages];
-
-            if (!context.Capabilities.SupportsTransformFeedback)
-            {
-                _tfInfoData = new int[Constants.TotalTransformFeedbackBuffers];
-            }
         }
 
 
@@ -339,13 +327,10 @@ namespace Ryujinx.Graphics.Gpu.Memory
         /// <param name="vertexCount">Vertex count per instance</param>
         public void SetInstancedDrawVertexCount(int vertexCount)
         {
-            if (!_context.Capabilities.SupportsTransformFeedback &&
-                HasTransformFeedbackOutputs &&
-                _tfInfoBuffer != BufferHandle.Null)
+            if (!_context.Capabilities.SupportsTransformFeedback && HasTransformFeedbackOutputs)
             {
-                Span<byte> data = stackalloc byte[sizeof(int)];
-                MemoryMarshal.Cast<byte, int>(data)[0] = vertexCount;
-                _context.Renderer.SetBufferData(_tfInfoBuffer, TfInfoVertexCountOffset, data);
+                _context.SupportBufferUpdater.SetTfeVertexCount(vertexCount);
+                _context.SupportBufferUpdater.Commit();
             }
         }
 
@@ -607,17 +592,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
                 }
                 else if (HasTransformFeedbackOutputs)
                 {
-                    Span<int> info = _tfInfoData.AsSpan();
-                    Span<BufferAssignment> buffers = stackalloc BufferAssignment[Constants.TotalTransformFeedbackBuffers + 1];
-
-                    bool needsDataUpdate = false;
-
-                    if (_tfInfoBuffer == BufferHandle.Null)
-                    {
-                        _tfInfoBuffer = _context.Renderer.CreateBuffer(TfInfoBufferSize, BufferAccess.Stream);
-                    }
-
-                    buffers[0] = new BufferAssignment(0, new BufferRange(_tfInfoBuffer, 0, TfInfoBufferSize));
+                    Span<BufferAssignment> buffers = stackalloc BufferAssignment[Constants.TotalTransformFeedbackBuffers];
 
                     int alignment = _context.Capabilities.StorageBufferOffsetAlignment;
 
@@ -627,7 +602,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
 
                         if (tfb.Address == 0)
                         {
-                            buffers[1 + index] = new BufferAssignment(1 + index, BufferRange.Empty);
+                            buffers[index] = new BufferAssignment(index, BufferRange.Empty);
                         }
                         else
                         {
@@ -637,22 +612,12 @@ namespace Ryujinx.Graphics.Gpu.Memory
 
                             int tfeOffset = ((int)tfb.Address & (alignment - 1)) / 4;
 
-                            if (info[index] != tfeOffset)
-                            {
-                                info[index] = tfeOffset;
-                                needsDataUpdate = true;
-                            }
+                            _context.SupportBufferUpdater.SetTfeOffset(index, tfeOffset);
 
-                            buffers[1 + index] = new BufferAssignment(1 + index, bufferCache.GetBufferRange(address, size, write: true));
+                            buffers[index] = new BufferAssignment(index, bufferCache.GetBufferRange(address, size, write: true));
                         }
                     }
 
-                    if (needsDataUpdate)
-                    {
-                        Span<byte> infoData = MemoryMarshal.Cast<int, byte>(info);
-                        _context.Renderer.SetBufferData(_tfInfoBuffer, 0, infoData);
-                    }
-
                     _context.Renderer.Pipeline.SetStorageBuffers(buffers);
                 }
             }
diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferUpdater.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferUpdater.cs
new file mode 100644
index 000000000..02090c04f
--- /dev/null
+++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferUpdater.cs
@@ -0,0 +1,123 @@
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.Shader;
+using System;
+
+namespace Ryujinx.Graphics.Gpu.Memory
+{
+    /// <summary>
+    /// Buffer data updater.
+    /// </summary>
+    class BufferUpdater : IDisposable
+    {
+        private BufferHandle _handle;
+
+        /// <summary>
+        /// Handle of the buffer.
+        /// </summary>
+        public BufferHandle Handle => _handle;
+
+        private readonly IRenderer _renderer;
+        private int _startOffset = -1;
+        private int _endOffset = -1;
+
+        /// <summary>
+        /// Creates a new instance of the buffer updater.
+        /// </summary>
+        /// <param name="renderer">Renderer that the buffer will be used with</param>
+        public BufferUpdater(IRenderer renderer)
+        {
+            _renderer = renderer;
+        }
+
+        /// <summary>
+        /// Mark a region of the buffer as modified and needing to be sent to the GPU.
+        /// </summary>
+        /// <param name="startOffset">Start offset of the region in bytes</param>
+        /// <param name="byteSize">Size of the region in bytes</param>
+        protected void MarkDirty(int startOffset, int byteSize)
+        {
+            int endOffset = startOffset + byteSize;
+
+            if (_startOffset == -1)
+            {
+                _startOffset = startOffset;
+                _endOffset = endOffset;
+            }
+            else
+            {
+                if (startOffset < _startOffset)
+                {
+                    _startOffset = startOffset;
+                }
+
+                if (endOffset > _endOffset)
+                {
+                    _endOffset = endOffset;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Submits all pending buffer updates to the GPU.
+        /// </summary>
+        /// <param name="data">All data that should be sent to the GPU. Only the modified regions will be updated</param>
+        /// <param name="binding">Optional binding to bind the buffer if a new buffer was created</param>
+        protected void Commit(ReadOnlySpan<byte> data, int binding = -1)
+        {
+            if (_startOffset != -1)
+            {
+                if (_handle == BufferHandle.Null)
+                {
+                    _handle = _renderer.CreateBuffer(data.Length, BufferAccess.Stream);
+                    _renderer.Pipeline.ClearBuffer(_handle, 0, data.Length, 0);
+
+                    if (binding >= 0)
+                    {
+                        var range = new BufferRange(_handle, 0, data.Length);
+                        _renderer.Pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, range) });
+                    }
+                };
+
+                _renderer.SetBufferData(_handle, _startOffset, data[_startOffset.._endOffset]);
+
+                _startOffset = -1;
+                _endOffset = -1;
+            }
+        }
+
+        /// <summary>
+        /// Gets a reference to a given element of a vector.
+        /// </summary>
+        /// <param name="vector">Vector to get the element reference from</param>
+        /// <param name="elementIndex">Element index</param>
+        /// <returns>Reference to the specified element</returns>
+        protected static ref T GetElementRef<T>(ref Vector4<T> vector, int elementIndex)
+        {
+            switch (elementIndex)
+            {
+                case 0:
+                    return ref vector.X;
+                case 1:
+                    return ref vector.Y;
+                case 2:
+                    return ref vector.Z;
+                case 3:
+                    return ref vector.W;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(elementIndex));
+            }
+        }
+
+        /// <summary>
+        /// Destroys the buffer.
+        /// </summary>
+        public void Dispose()
+        {
+            if (_handle != BufferHandle.Null)
+            {
+                _renderer.DeleteBuffer(_handle);
+                _handle = BufferHandle.Null;
+            }
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.Gpu/Memory/SupportBufferUpdater.cs b/src/Ryujinx.Graphics.Gpu/Memory/SupportBufferUpdater.cs
index c1e91c54b..fb141db42 100644
--- a/src/Ryujinx.Graphics.Gpu/Memory/SupportBufferUpdater.cs
+++ b/src/Ryujinx.Graphics.Gpu/Memory/SupportBufferUpdater.cs
@@ -9,56 +9,21 @@ namespace Ryujinx.Graphics.Gpu.Memory
     /// <summary>
     /// Support buffer data updater.
     /// </summary>
-    class SupportBufferUpdater : IDisposable
+    class SupportBufferUpdater : BufferUpdater
     {
         private SupportBuffer _data;
-        private BufferHandle _handle;
-
-        private readonly IRenderer _renderer;
-        private int _startOffset = -1;
-        private int _endOffset = -1;
 
         /// <summary>
         /// Creates a new instance of the support buffer updater.
         /// </summary>
         /// <param name="renderer">Renderer that the support buffer will be used with</param>
-        public SupportBufferUpdater(IRenderer renderer)
+        public SupportBufferUpdater(IRenderer renderer) : base(renderer)
         {
-            _renderer = renderer;
-
             var defaultScale = new Vector4<float> { X = 1f, Y = 0f, Z = 0f, W = 0f };
             _data.RenderScale.AsSpan().Fill(defaultScale);
             DirtyRenderScale(0, SupportBuffer.RenderScaleMaxCount);
         }
 
-        /// <summary>
-        /// Mark a region of the support buffer as modified and needing to be sent to the GPU.
-        /// </summary>
-        /// <param name="startOffset">Start offset of the region in bytes</param>
-        /// <param name="byteSize">Size of the region in bytes</param>
-        private void MarkDirty(int startOffset, int byteSize)
-        {
-            int endOffset = startOffset + byteSize;
-
-            if (_startOffset == -1)
-            {
-                _startOffset = startOffset;
-                _endOffset = endOffset;
-            }
-            else
-            {
-                if (startOffset < _startOffset)
-                {
-                    _startOffset = startOffset;
-                }
-
-                if (endOffset > _endOffset)
-                {
-                    _endOffset = endOffset;
-                }
-            }
-        }
-
         /// <summary>
         /// Marks the fragment render scale count as being modified.
         /// </summary>
@@ -220,40 +185,40 @@ namespace Ryujinx.Graphics.Gpu.Memory
         }
 
         /// <summary>
-        /// Submits all pending buffer updates to the GPU.
+        /// Sets offset for the misaligned portion of a transform feedback buffer, and the buffer size, for transform feedback emulation.
         /// </summary>
-        public void Commit()
+        /// <param name="bufferIndex">Index of the transform feedback buffer</param>
+        /// <param name="offset">Misaligned offset of the buffer</param>
+        public void SetTfeOffset(int bufferIndex, int offset)
         {
-            if (_startOffset != -1)
+            ref int currentOffset = ref GetElementRef(ref _data.TfeOffset, bufferIndex);
+
+            if (currentOffset != offset)
             {
-                if (_handle == BufferHandle.Null)
-                {
-                    _handle = _renderer.CreateBuffer(SupportBuffer.RequiredSize, BufferAccess.Stream);
-                    _renderer.Pipeline.ClearBuffer(_handle, 0, SupportBuffer.RequiredSize, 0);
-
-                    var range = new BufferRange(_handle, 0, SupportBuffer.RequiredSize);
-                    _renderer.Pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, range) });
-                }
-
-                ReadOnlySpan<byte> data = MemoryMarshal.Cast<SupportBuffer, byte>(MemoryMarshal.CreateSpan(ref _data, 1));
-
-                _renderer.SetBufferData(_handle, _startOffset, data[_startOffset.._endOffset]);
-
-                _startOffset = -1;
-                _endOffset = -1;
+                currentOffset = offset;
+                MarkDirty(SupportBuffer.TfeOffsetOffset + bufferIndex * sizeof(int), sizeof(int));
             }
         }
 
         /// <summary>
-        /// Destroys the support buffer.
+        /// Sets the vertex count used for transform feedback emulation with instanced draws.
         /// </summary>
-        public void Dispose()
+        /// <param name="vertexCount">Vertex count of the instanced draw</param>
+        public void SetTfeVertexCount(int vertexCount)
         {
-            if (_handle != BufferHandle.Null)
+            if (_data.TfeVertexCount.X != vertexCount)
             {
-                _renderer.DeleteBuffer(_handle);
-                _handle = BufferHandle.Null;
+                _data.TfeVertexCount.X = vertexCount;
+                MarkDirty(SupportBuffer.TfeVertexCountOffset, sizeof(int));
             }
         }
+
+        /// <summary>
+        /// Submits all pending buffer updates to the GPU.
+        /// </summary>
+        public void Commit()
+        {
+            Commit(MemoryMarshal.Cast<SupportBuffer, byte>(MemoryMarshal.CreateSpan(ref _data, 1)), SupportBuffer.Binding);
+        }
     }
 }
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/CachedShaderProgram.cs b/src/Ryujinx.Graphics.Gpu/Shader/CachedShaderProgram.cs
index ff9c39a19..600c8a985 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/CachedShaderProgram.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/CachedShaderProgram.cs
@@ -14,6 +14,16 @@ namespace Ryujinx.Graphics.Gpu.Shader
         /// </summary>
         public IProgram HostProgram { get; }
 
+        /// <summary>
+        /// Optional vertex shader converted to compute.
+        /// </summary>
+        public ShaderAsCompute VertexAsCompute { get; }
+
+        /// <summary>
+        /// Optional geometry shader converted to compute.
+        /// </summary>
+        public ShaderAsCompute GeometryAsCompute { get; }
+
         /// <summary>
         /// GPU state used to create this version of the shader.
         /// </summary>
@@ -45,12 +55,25 @@ namespace Ryujinx.Graphics.Gpu.Shader
             Bindings = new CachedShaderBindings(shaders.Length == 1, shaders);
         }
 
+        public CachedShaderProgram(
+            IProgram hostProgram,
+            ShaderAsCompute vertexAsCompute,
+            ShaderAsCompute geometryAsCompute,
+            ShaderSpecializationState specializationState,
+            CachedShaderStage[] shaders) : this(hostProgram, specializationState, shaders)
+        {
+            VertexAsCompute = vertexAsCompute;
+            GeometryAsCompute = geometryAsCompute;
+        }
+
         /// <summary>
         /// Dispose of the host shader resources.
         /// </summary>
         public void Dispose()
         {
             HostProgram.Dispose();
+            VertexAsCompute?.HostProgram.Dispose();
+            GeometryAsCompute?.HostProgram.Dispose();
         }
     }
 }
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGpuAccessor.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGpuAccessor.cs
index 93d293f62..de6432bc1 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGpuAccessor.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGpuAccessor.cs
@@ -35,7 +35,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
             ShaderSpecializationState oldSpecState,
             ShaderSpecializationState newSpecState,
             ResourceCounts counts,
-            int stageIndex) : base(context, counts, stageIndex, oldSpecState.TransformFeedbackDescriptors != null)
+            int stageIndex) : base(context, counts, stageIndex)
         {
             _data = data;
             _cb1Data = cb1Data;
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
index 7d64bdf67..43fad9f8b 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
@@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
         private const ushort FileFormatVersionMajor = 1;
         private const ushort FileFormatVersionMinor = 2;
         private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor;
-        private const uint CodeGenVersion = 5576;
+        private const uint CodeGenVersion = 5767;
 
         private const string SharedTocFileName = "shared.toc";
         private const string SharedDataFileName = "shared.data";
@@ -140,6 +140,21 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
             /// </summary>
             public ShaderStage Stage;
 
+            /// <summary>
+            /// Number of vertices that each output primitive has on a geometry shader.
+            /// </summary>
+            public byte GeometryVerticesPerPrimitive;
+
+            /// <summary>
+            /// Maximum number of vertices that a geometry shader may generate.
+            /// </summary>
+            public ushort GeometryMaxOutputVertices;
+
+            /// <summary>
+            /// Number of invocations per primitive on tessellation or geometry shaders.
+            /// </summary>
+            public ushort ThreadsPerInputPrimitive;
+
             /// <summary>
             /// Indicates if the fragment shader accesses the fragment coordinate built-in variable.
             /// </summary>
@@ -783,9 +798,10 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
                 sBuffers,
                 textures,
                 images,
-                ShaderIdentification.None,
-                0,
                 dataInfo.Stage,
+                dataInfo.GeometryVerticesPerPrimitive,
+                dataInfo.GeometryMaxOutputVertices,
+                dataInfo.ThreadsPerInputPrimitive,
                 dataInfo.UsesFragCoord,
                 dataInfo.UsesInstanceId,
                 dataInfo.UsesDrawParameters,
@@ -813,6 +829,9 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
                 TexturesCount = (ushort)info.Textures.Count,
                 ImagesCount = (ushort)info.Images.Count,
                 Stage = info.Stage,
+                GeometryVerticesPerPrimitive = (byte)info.GeometryVerticesPerPrimitive,
+                GeometryMaxOutputVertices = (ushort)info.GeometryMaxOutputVertices,
+                ThreadsPerInputPrimitive = (ushort)info.ThreadsPerInputPrimitive,
                 UsesFragCoord = info.UsesFragCoord,
                 UsesInstanceId = info.UsesInstanceId,
                 UsesDrawParameters = info.UsesDrawParameters,
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs
index 5fcb0f913..4f3f71fea 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs
@@ -595,6 +595,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
 
             ResourceCounts counts = new();
 
+            DiskCacheGpuAccessor[] gpuAccessors = new DiskCacheGpuAccessor[Constants.ShaderStages];
             TranslatorContext[] translatorContexts = new TranslatorContext[Constants.ShaderStages + 1];
             TranslatorContext nextStage = null;
 
@@ -626,14 +627,22 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
                         translatorContexts[0] = DecodeGraphicsShader(gpuAccessorA, api, DefaultFlags | TranslationFlags.VertexA, 0);
                     }
 
+                    gpuAccessors[stageIndex] = gpuAccessor;
                     translatorContexts[stageIndex + 1] = currentStage;
                     nextStage = currentStage;
                 }
             }
 
-            if (!_context.Capabilities.SupportsGeometryShader)
+            bool hasGeometryShader = translatorContexts[4] != null;
+            bool vertexHasStore = translatorContexts[1] != null && translatorContexts[1].HasStore;
+            bool geometryHasStore = hasGeometryShader && translatorContexts[4].HasStore;
+            bool vertexToCompute = ShouldConvertVertexToCompute(_context, vertexHasStore, geometryHasStore, hasGeometryShader);
+
+            // We don't support caching shader stages that have been converted to compute currently,
+            // so just eliminate them if they exist in the cache.
+            if (vertexToCompute)
             {
-                ShaderCache.TryRemoveGeometryStage(translatorContexts);
+                return;
             }
 
             CachedShaderStage[] shaders = new CachedShaderStage[guestShaders.Length];
@@ -647,6 +656,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
 
                 if (currentStage != null)
                 {
+                    gpuAccessors[stageIndex].InitializeReservedCounts(specState.TransformFeedbackDescriptors != null, vertexToCompute);
+
                     ShaderProgram program;
 
                     byte[] guestCode = guestShaders[stageIndex + 1].Value.Code;
@@ -701,6 +712,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
             ResourceCounts counts = new();
             ShaderSpecializationState newSpecState = new(ref specState.ComputeState);
             DiskCacheGpuAccessor gpuAccessor = new(_context, shader.Code, shader.Cb1Data, specState, newSpecState, counts, 0);
+            gpuAccessor.InitializeReservedCounts(tfEnabled: false, vertexAsCompute: false);
 
             TranslatorContext translatorContext = DecodeComputeShader(gpuAccessor, _context.Capabilities.Api, 0);
 
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs
index b4f4a439c..1d84d0e46 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs
@@ -25,11 +25,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
         /// <param name="channel">GPU channel</param>
         /// <param name="state">Current GPU state</param>
         /// <param name="stageIndex">Graphics shader stage index (0 = Vertex, 4 = Fragment)</param>
-        public GpuAccessor(
-            GpuContext context,
-            GpuChannel channel,
-            GpuAccessorState state,
-            int stageIndex) : base(context, state.ResourceCounts, stageIndex, state.TransformFeedbackDescriptors != null)
+        public GpuAccessor(GpuContext context, GpuChannel channel, GpuAccessorState state, int stageIndex) : base(context, state.ResourceCounts, stageIndex)
         {
             _isVulkan = context.Capabilities.Api == TargetApi.Vulkan;
             _channel = channel;
@@ -49,7 +45,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
         /// <param name="context">GPU context</param>
         /// <param name="channel">GPU channel</param>
         /// <param name="state">Current GPU state</param>
-        public GpuAccessor(GpuContext context, GpuChannel channel, GpuAccessorState state) : base(context, state.ResourceCounts, 0, false)
+        public GpuAccessor(GpuContext context, GpuChannel channel, GpuAccessorState state) : base(context, state.ResourceCounts, 0)
         {
             _channel = channel;
             _state = state;
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs
index e7a2d345f..9d030cd60 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs
@@ -15,8 +15,10 @@ namespace Ryujinx.Graphics.Gpu.Shader
         private readonly ResourceCounts _resourceCounts;
         private readonly int _stageIndex;
 
-        private readonly int _reservedConstantBuffers;
-        private readonly int _reservedStorageBuffers;
+        private int _reservedConstantBuffers;
+        private int _reservedStorageBuffers;
+        private int _reservedTextures;
+        private int _reservedImages;
 
         /// <summary>
         /// Creates a new GPU accessor.
@@ -24,15 +26,26 @@ namespace Ryujinx.Graphics.Gpu.Shader
         /// <param name="context">GPU context</param>
         /// <param name="resourceCounts">Counter of GPU resources used by the shader</param>
         /// <param name="stageIndex">Index of the shader stage, 0 for compute</param>
-        /// <param name="tfEnabled">Indicates if the current graphics shader is used with transform feedback enabled</param>
-        public GpuAccessorBase(GpuContext context, ResourceCounts resourceCounts, int stageIndex, bool tfEnabled)
+        public GpuAccessorBase(GpuContext context, ResourceCounts resourceCounts, int stageIndex)
         {
             _context = context;
             _resourceCounts = resourceCounts;
             _stageIndex = stageIndex;
+        }
 
-            _reservedConstantBuffers = 1; // For the support buffer.
-            _reservedStorageBuffers = !context.Capabilities.SupportsTransformFeedback && tfEnabled ? 5 : 0;
+        /// <summary>
+        /// Initializes counts for bindings that will be reserved for emulator use.
+        /// </summary>
+        /// <param name="tfEnabled">Indicates if the current graphics shader is used with transform feedback enabled</param>
+        /// <param name="vertexAsCompute">Indicates that the vertex shader will be emulated on a compute shader</param>
+        public void InitializeReservedCounts(bool tfEnabled, bool vertexAsCompute)
+        {
+            ResourceReservationCounts rrc = new(!_context.Capabilities.SupportsTransformFeedback && tfEnabled, vertexAsCompute);
+
+            _reservedConstantBuffers = rrc.ReservedConstantBuffers;
+            _reservedStorageBuffers = rrc.ReservedStorageBuffers;
+            _reservedTextures = rrc.ReservedTextures;
+            _reservedImages = rrc.ReservedImages;
         }
 
         public int QueryBindingConstantBuffer(int index)
@@ -69,6 +82,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
 
         public int QueryBindingTexture(int index, bool isBuffer)
         {
+            int binding;
+
             if (_context.Capabilities.Api == TargetApi.Vulkan)
             {
                 if (isBuffer)
@@ -76,16 +91,20 @@ namespace Ryujinx.Graphics.Gpu.Shader
                     index += (int)_context.Capabilities.MaximumTexturesPerStage;
                 }
 
-                return GetBindingFromIndex(index, _context.Capabilities.MaximumTexturesPerStage * 2, "Texture");
+                binding = GetBindingFromIndex(index, _context.Capabilities.MaximumTexturesPerStage * 2, "Texture");
             }
             else
             {
-                return _resourceCounts.TexturesCount++;
+                binding = _resourceCounts.TexturesCount++;
             }
+
+            return binding + _reservedTextures;
         }
 
         public int QueryBindingImage(int index, bool isBuffer)
         {
+            int binding;
+
             if (_context.Capabilities.Api == TargetApi.Vulkan)
             {
                 if (isBuffer)
@@ -93,12 +112,14 @@ namespace Ryujinx.Graphics.Gpu.Shader
                     index += (int)_context.Capabilities.MaximumImagesPerStage;
                 }
 
-                return GetBindingFromIndex(index, _context.Capabilities.MaximumImagesPerStage * 2, "Image");
+                binding = GetBindingFromIndex(index, _context.Capabilities.MaximumImagesPerStage * 2, "Image");
             }
             else
             {
-                return _resourceCounts.ImagesCount++;
+                binding = _resourceCounts.ImagesCount++;
             }
+
+            return binding + _reservedImages;
         }
 
         private int GetBindingFromIndex(int index, uint maxPerStage, string resourceName)
@@ -137,6 +158,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
 
         public int QueryHostStorageBufferOffsetAlignment() => _context.Capabilities.StorageBufferOffsetAlignment;
 
+        public int QueryHostSubgroupSize() => _context.Capabilities.ShaderSubgroupSize;
+
         public bool QueryHostSupportsBgraFormat() => _context.Capabilities.SupportsBgraFormat;
 
         public bool QueryHostSupportsFragmentShaderInterlock() => _context.Capabilities.SupportsFragmentShaderInterlock;
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderAsCompute.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderAsCompute.cs
new file mode 100644
index 000000000..71540a13f
--- /dev/null
+++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderAsCompute.cs
@@ -0,0 +1,20 @@
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.Shader;
+using Ryujinx.Graphics.Shader.Translation;
+
+namespace Ryujinx.Graphics.Gpu.Shader
+{
+    class ShaderAsCompute
+    {
+        public IProgram HostProgram { get; }
+        public ShaderProgramInfo Info { get; }
+        public ResourceReservations Reservations { get; }
+
+        public ShaderAsCompute(IProgram hostProgram, ShaderProgramInfo info, ResourceReservations reservations)
+        {
+            HostProgram = hostProgram;
+            Info = info;
+            Reservations = reservations;
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs
index 28d478639..5c59aedb8 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs
@@ -222,9 +222,10 @@ namespace Ryujinx.Graphics.Gpu.Shader
             ShaderSpecializationState specState = new(ref computeState);
             GpuAccessorState gpuAccessorState = new(poolState, computeState, default, specState);
             GpuAccessor gpuAccessor = new(_context, channel, gpuAccessorState);
+            gpuAccessor.InitializeReservedCounts(tfEnabled: false, vertexAsCompute: false);
 
             TranslatorContext translatorContext = DecodeComputeShader(gpuAccessor, _context.Capabilities.Api, gpuVa);
-            TranslatedShader translatedShader = TranslateShader(_dumper, channel, translatorContext, cachedGuestCode);
+            TranslatedShader translatedShader = TranslateShader(_dumper, channel, translatorContext, cachedGuestCode, asCompute: false);
 
             ShaderSource[] shaderSourcesArray = new ShaderSource[] { CreateShaderSource(translatedShader.Program) };
             ShaderInfo info = ShaderInfoBuilder.BuildForCompute(_context, translatedShader.Program.Info);
@@ -328,6 +329,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
 
             ReadOnlySpan<ulong> addressesSpan = addresses.AsSpan();
 
+            GpuAccessor[] gpuAccessors = new GpuAccessor[Constants.ShaderStages];
             TranslatorContext[] translatorContexts = new TranslatorContext[Constants.ShaderStages + 1];
             TranslatorContext nextStage = null;
 
@@ -352,22 +354,31 @@ namespace Ryujinx.Graphics.Gpu.Shader
                         translatorContexts[0] = DecodeGraphicsShader(gpuAccessor, api, DefaultFlags | TranslationFlags.VertexA, addresses.VertexA);
                     }
 
+                    gpuAccessors[stageIndex] = gpuAccessor;
                     translatorContexts[stageIndex + 1] = currentStage;
                     nextStage = currentStage;
                 }
             }
 
-            if (!_context.Capabilities.SupportsGeometryShader)
-            {
-                TryRemoveGeometryStage(translatorContexts);
-            }
+            bool hasGeometryShader = translatorContexts[4] != null;
+            bool vertexHasStore = translatorContexts[1] != null && translatorContexts[1].HasStore;
+            bool geometryHasStore = hasGeometryShader && translatorContexts[4].HasStore;
+            bool vertexToCompute = ShouldConvertVertexToCompute(_context, vertexHasStore, geometryHasStore, hasGeometryShader);
+            bool geometryToCompute = ShouldConvertGeometryToCompute(_context, geometryHasStore);
 
             CachedShaderStage[] shaders = new CachedShaderStage[Constants.ShaderStages + 1];
             List<ShaderSource> shaderSources = new();
 
             TranslatorContext previousStage = null;
+            ShaderInfoBuilder infoBuilder = new(_context, transformFeedbackDescriptors != null, vertexToCompute);
 
-            ShaderInfoBuilder infoBuilder = new(_context, transformFeedbackDescriptors != null);
+            if (geometryToCompute && translatorContexts[4] != null)
+            {
+                translatorContexts[4].SetVertexOutputMapForGeometryAsCompute(translatorContexts[1]);
+            }
+
+            ShaderAsCompute vertexAsCompute = null;
+            ShaderAsCompute geometryAsCompute = null;
 
             for (int stageIndex = 0; stageIndex < Constants.ShaderStages; stageIndex++)
             {
@@ -375,8 +386,12 @@ namespace Ryujinx.Graphics.Gpu.Shader
 
                 if (currentStage != null)
                 {
+                    gpuAccessors[stageIndex].InitializeReservedCounts(transformFeedbackDescriptors != null, vertexToCompute);
+
                     ShaderProgram program;
 
+                    bool asCompute = (stageIndex == 0 && vertexToCompute) || (stageIndex == 3 && geometryToCompute);
+
                     if (stageIndex == 0 && translatorContexts[0] != null)
                     {
                         TranslatedShaderVertexPair translatedShader = TranslateShader(
@@ -385,7 +400,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
                             currentStage,
                             translatorContexts[0],
                             cachedGuestCode.VertexACode,
-                            cachedGuestCode.VertexBCode);
+                            cachedGuestCode.VertexBCode,
+                            asCompute);
 
                         shaders[0] = translatedShader.VertexA;
                         shaders[1] = translatedShader.VertexB;
@@ -395,12 +411,31 @@ namespace Ryujinx.Graphics.Gpu.Shader
                     {
                         byte[] code = cachedGuestCode.GetByIndex(stageIndex);
 
-                        TranslatedShader translatedShader = TranslateShader(_dumper, channel, currentStage, code);
+                        TranslatedShader translatedShader = TranslateShader(_dumper, channel, currentStage, code, asCompute);
 
                         shaders[stageIndex + 1] = translatedShader.Shader;
                         program = translatedShader.Program;
                     }
 
+                    if (asCompute)
+                    {
+                        bool tfEnabled = transformFeedbackDescriptors != null;
+
+                        if (stageIndex == 0)
+                        {
+                            vertexAsCompute = CreateHostVertexAsComputeProgram(program, currentStage, tfEnabled);
+
+                            TranslatorContext lastInVertexPipeline = geometryToCompute ? translatorContexts[4] ?? currentStage : currentStage;
+
+                            program = lastInVertexPipeline.GenerateVertexPassthroughForCompute();
+                        }
+                        else
+                        {
+                            geometryAsCompute = CreateHostVertexAsComputeProgram(program, currentStage, tfEnabled);
+                            program = null;
+                        }
+                    }
+
                     if (program != null)
                     {
                         shaderSources.Add(CreateShaderSource(program));
@@ -425,46 +460,81 @@ namespace Ryujinx.Graphics.Gpu.Shader
 
             IProgram hostProgram = _context.Renderer.CreateProgram(shaderSourcesArray, info);
 
-            gpShaders = new CachedShaderProgram(hostProgram, specState, shaders);
+            gpShaders = new(hostProgram, vertexAsCompute, geometryAsCompute, specState, shaders);
 
             _graphicsShaderCache.Add(gpShaders);
-            EnqueueProgramToSave(gpShaders, hostProgram, shaderSourcesArray);
+
+            // We don't currently support caching shaders that have been converted to compute.
+            if (vertexAsCompute == null)
+            {
+                EnqueueProgramToSave(gpShaders, hostProgram, shaderSourcesArray);
+            }
+
             _gpPrograms[addresses] = gpShaders;
 
             return gpShaders;
         }
 
         /// <summary>
-        /// Tries to eliminate the geometry stage from the array of translator contexts.
+        /// Checks if a vertex shader should be converted to a compute shader due to it making use of
+        /// features that are not supported on the host.
         /// </summary>
-        /// <param name="translatorContexts">Array of translator contexts</param>
-        public static void TryRemoveGeometryStage(TranslatorContext[] translatorContexts)
+        /// <param name="context">GPU context of the shader</param>
+        /// <param name="vertexHasStore">Whether the vertex shader has image or storage buffer store operations</param>
+        /// <param name="geometryHasStore">Whether the geometry shader has image or storage buffer store operations, if one exists</param>
+        /// <param name="hasGeometryShader">Whether a geometry shader exists</param>
+        /// <returns>True if the vertex shader should be converted to compute, false otherwise</returns>
+        public static bool ShouldConvertVertexToCompute(GpuContext context, bool vertexHasStore, bool geometryHasStore, bool hasGeometryShader)
         {
-            if (translatorContexts[4] != null)
+            // If the host does not support store operations on vertex,
+            // we need to emulate it on a compute shader.
+            if (!context.Capabilities.SupportsVertexStoreAndAtomics && vertexHasStore)
             {
-                // We have a geometry shader, but geometry shaders are not supported.
-                // Try to eliminate the geometry shader.
-
-                ShaderProgramInfo info = translatorContexts[4].Translate().Info;
-
-                if (info.Identification == ShaderIdentification.GeometryLayerPassthrough)
-                {
-                    // We managed to identify that this geometry shader is only used to set the output Layer value,
-                    // we can set the Layer on the previous stage instead (usually the vertex stage) and eliminate it.
-
-                    for (int i = 3; i >= 1; i--)
-                    {
-                        if (translatorContexts[i] != null)
-                        {
-                            translatorContexts[i].SetGeometryShaderLayerInputAttribute(info.GpLayerInputAttribute);
-                            translatorContexts[i].SetLastInVertexPipeline();
-                            break;
-                        }
-                    }
-
-                    translatorContexts[4] = null;
-                }
+                return true;
             }
+
+            // If any stage after the vertex stage is converted to compute,
+            // we need to convert vertex to compute too.
+            return hasGeometryShader && ShouldConvertGeometryToCompute(context, geometryHasStore);
+        }
+
+        /// <summary>
+        /// Checks if a geometry shader should be converted to a compute shader due to it making use of
+        /// features that are not supported on the host.
+        /// </summary>
+        /// <param name="context">GPU context of the shader</param>
+        /// <param name="geometryHasStore">Whether the geometry shader has image or storage buffer store operations, if one exists</param>
+        /// <returns>True if the geometry shader should be converted to compute, false otherwise</returns>
+        public static bool ShouldConvertGeometryToCompute(GpuContext context, bool geometryHasStore)
+        {
+            return (!context.Capabilities.SupportsVertexStoreAndAtomics && geometryHasStore) ||
+                   !context.Capabilities.SupportsGeometryShader;
+        }
+
+        /// <summary>
+        /// Checks if it might be necessary for any vertex, tessellation or geometry shader to be converted to compute,
+        /// based on the supported host features.
+        /// </summary>
+        /// <param name="capabilities">Host capabilities</param>
+        /// <returns>True if the possibility of a shader being converted to compute exists, false otherwise</returns>
+        public static bool MayConvertVtgToCompute(ref Capabilities capabilities)
+        {
+            return !capabilities.SupportsVertexStoreAndAtomics || !capabilities.SupportsGeometryShader;
+        }
+
+        /// <summary>
+        /// Creates a compute shader from a vertex, tessellation or geometry shader that has been converted to compute.
+        /// </summary>
+        /// <param name="program">Shader program</param>
+        /// <param name="context">Translation context of the shader</param>
+        /// <param name="tfEnabled">Whether transform feedback is enabled</param>
+        /// <returns>Compute shader</returns>
+        private ShaderAsCompute CreateHostVertexAsComputeProgram(ShaderProgram program, TranslatorContext context, bool tfEnabled)
+        {
+            ShaderSource source = new(program.Code, program.BinaryCode, ShaderStage.Compute, program.Language);
+            ShaderInfo info = ShaderInfoBuilder.BuildForVertexAsCompute(_context, program.Info, tfEnabled);
+
+            return new(_context.Renderer.CreateProgram(new[] { source }, info), program.Info, context.GetResourceReservations());
         }
 
         /// <summary>
@@ -580,9 +650,16 @@ namespace Ryujinx.Graphics.Gpu.Shader
                 }
             }
 
+            bool vertexAsCompute = gpShaders.VertexAsCompute != null;
             bool usesDrawParameters = gpShaders.Shaders[1]?.Info.UsesDrawParameters ?? false;
 
-            return gpShaders.SpecializationState.MatchesGraphics(channel, ref poolState, ref graphicsState, usesDrawParameters, true);
+            return gpShaders.SpecializationState.MatchesGraphics(
+                channel,
+                ref poolState,
+                ref graphicsState,
+                vertexAsCompute,
+                usesDrawParameters,
+                checkTextures: true);
         }
 
         /// <summary>
@@ -643,6 +720,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
         /// <param name="vertexA">Optional translator context of the shader that should be combined</param>
         /// <param name="codeA">Optional Maxwell binary code of the Vertex A shader, if present</param>
         /// <param name="codeB">Optional Maxwell binary code of the Vertex B or current stage shader, if present on cache</param>
+        /// <param name="asCompute">Indicates that the vertex shader should be converted to a compute shader</param>
         /// <returns>Compiled graphics shader code</returns>
         private static TranslatedShaderVertexPair TranslateShader(
             ShaderDumper dumper,
@@ -650,7 +728,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
             TranslatorContext currentStage,
             TranslatorContext vertexA,
             byte[] codeA,
-            byte[] codeB)
+            byte[] codeB,
+            bool asCompute)
         {
             ulong cb1DataAddress = channel.BufferManager.GetGraphicsUniformBufferAddress(0, 1);
 
@@ -670,7 +749,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
                 pathsB = dumper.Dump(codeB, compute: false);
             }
 
-            ShaderProgram program = currentStage.Translate(vertexA);
+            ShaderProgram program = currentStage.Translate(vertexA, asCompute);
 
             pathsB.Prepend(program);
             pathsA.Prepend(program);
@@ -688,8 +767,9 @@ namespace Ryujinx.Graphics.Gpu.Shader
         /// <param name="channel">GPU channel using the shader</param>
         /// <param name="context">Translator context of the stage to be translated</param>
         /// <param name="code">Optional Maxwell binary code of the current stage shader, if present on cache</param>
+        /// <param name="asCompute">Indicates that the vertex shader should be converted to a compute shader</param>
         /// <returns>Compiled graphics shader code</returns>
-        private static TranslatedShader TranslateShader(ShaderDumper dumper, GpuChannel channel, TranslatorContext context, byte[] code)
+        private static TranslatedShader TranslateShader(ShaderDumper dumper, GpuChannel channel, TranslatorContext context, byte[] code, bool asCompute)
         {
             var memoryManager = channel.MemoryManager;
 
@@ -701,7 +781,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
             code ??= memoryManager.GetSpan(context.Address, context.Size).ToArray();
 
             ShaderDumpPaths paths = dumper?.Dump(code, context.Stage == ShaderStage.Compute) ?? default;
-            ShaderProgram program = context.Translate();
+            ShaderProgram program = context.Translate(asCompute);
 
             paths.Prepend(program);
 
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs
index af1e1ee3f..c2258026c 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs
@@ -33,6 +33,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
 
         private readonly int _reservedConstantBuffers;
         private readonly int _reservedStorageBuffers;
+        private readonly int _reservedTextures;
+        private readonly int _reservedImages;
 
         private readonly List<ResourceDescriptor>[] _resourceDescriptors;
         private readonly List<ResourceUsage>[] _resourceUsages;
@@ -42,7 +44,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
         /// </summary>
         /// <param name="context">GPU context that owns the shaders that will be added to the builder</param>
         /// <param name="tfEnabled">Indicates if the graphics shader is used with transform feedback enabled</param>
-        public ShaderInfoBuilder(GpuContext context, bool tfEnabled)
+        /// <param name="vertexAsCompute">Indicates that the vertex shader will be emulated on a compute shader</param>
+        public ShaderInfoBuilder(GpuContext context, bool tfEnabled, bool vertexAsCompute = false)
         {
             _context = context;
 
@@ -58,29 +61,36 @@ namespace Ryujinx.Graphics.Gpu.Shader
             }
 
             AddDescriptor(SupportBufferStages, ResourceType.UniformBuffer, UniformSetIndex, 0, 1);
-            AddUsage(SupportBufferStages, ResourceType.UniformBuffer, ResourceAccess.Read, UniformSetIndex, 0, 1);
+            AddUsage(SupportBufferStages, ResourceType.UniformBuffer, UniformSetIndex, 0, 1);
 
-            _reservedConstantBuffers = 1; // For the support buffer.
+            ResourceReservationCounts rrc = new(!context.Capabilities.SupportsTransformFeedback && tfEnabled, vertexAsCompute);
 
-            if (!context.Capabilities.SupportsTransformFeedback && tfEnabled)
-            {
-                _reservedStorageBuffers = 5;
+            _reservedConstantBuffers = rrc.ReservedConstantBuffers;
+            _reservedStorageBuffers = rrc.ReservedStorageBuffers;
+            _reservedTextures = rrc.ReservedTextures;
+            _reservedImages = rrc.ReservedImages;
 
-                AddDescriptor(VtgStages, ResourceType.StorageBuffer, StorageSetIndex, 0, 5);
-                AddUsage(VtgStages, ResourceType.StorageBuffer, ResourceAccess.Read, StorageSetIndex, 0, 1);
-                AddUsage(VtgStages, ResourceType.StorageBuffer, ResourceAccess.Write, StorageSetIndex, 1, 4);
-            }
-            else
-            {
-                _reservedStorageBuffers = 0;
-            }
+            // TODO: Handle that better? Maybe we should only set the binding that are really needed on each shader.
+            ResourceStages stages = vertexAsCompute ? ResourceStages.Compute | ResourceStages.Vertex : VtgStages;
+
+            PopulateDescriptorAndUsages(stages, ResourceType.UniformBuffer, UniformSetIndex, 1, rrc.ReservedConstantBuffers - 1);
+            PopulateDescriptorAndUsages(stages, ResourceType.StorageBuffer, StorageSetIndex, 0, rrc.ReservedStorageBuffers);
+            PopulateDescriptorAndUsages(stages, ResourceType.BufferTexture, TextureSetIndex, 0, rrc.ReservedTextures);
+            PopulateDescriptorAndUsages(stages, ResourceType.BufferImage, ImageSetIndex, 0, rrc.ReservedImages);
+        }
+
+        private void PopulateDescriptorAndUsages(ResourceStages stages, ResourceType type, int setIndex, int start, int count)
+        {
+            AddDescriptor(stages, type, setIndex, start, count);
+            AddUsage(stages, type, setIndex, start, count);
         }
 
         /// <summary>
         /// Adds information from a given shader stage.
         /// </summary>
         /// <param name="info">Shader stage information</param>
-        public void AddStageInfo(ShaderProgramInfo info)
+        /// <param name="vertexAsCompute">True if the shader stage has been converted into a compute shader</param>
+        public void AddStageInfo(ShaderProgramInfo info, bool vertexAsCompute = false)
         {
             if (info.Stage == ShaderStage.Fragment)
             {
@@ -96,7 +106,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
                 _ => 0,
             });
 
-            ResourceStages stages = info.Stage switch
+            ResourceStages stages = vertexAsCompute ? ResourceStages.Compute : info.Stage switch
             {
                 ShaderStage.Compute => ResourceStages.Compute,
                 ShaderStage.Vertex => ResourceStages.Vertex,
@@ -114,8 +124,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
 
             int uniformBinding = _reservedConstantBuffers + stageIndex * uniformsPerStage;
             int storageBinding = _reservedStorageBuffers + stageIndex * storagesPerStage;
-            int textureBinding = stageIndex * texturesPerStage * 2;
-            int imageBinding = stageIndex * imagesPerStage * 2;
+            int textureBinding = _reservedTextures + stageIndex * texturesPerStage * 2;
+            int imageBinding = _reservedImages + stageIndex * imagesPerStage * 2;
 
             AddDescriptor(stages, ResourceType.UniformBuffer, UniformSetIndex, uniformBinding, uniformsPerStage);
             AddDescriptor(stages, ResourceType.StorageBuffer, StorageSetIndex, storageBinding, storagesPerStage);
@@ -164,15 +174,14 @@ namespace Ryujinx.Graphics.Gpu.Shader
         /// </summary>
         /// <param name="stages">Shader stages where the resource is used</param>
         /// <param name="type">Type of the resource</param>
-        /// <param name="access">How the resource is accessed by the shader stages where it is used</param>
         /// <param name="setIndex">Descriptor set number where the resource will be bound</param>
         /// <param name="binding">Binding number where the resource will be bound</param>
         /// <param name="count">Number of resources bound at the binding location</param>
-        private void AddUsage(ResourceStages stages, ResourceType type, ResourceAccess access, int setIndex, int binding, int count)
+        private void AddUsage(ResourceStages stages, ResourceType type, int setIndex, int binding, int count)
         {
             for (int index = 0; index < count; index++)
             {
-                _resourceUsages[setIndex].Add(new ResourceUsage(binding + index, type, stages, access));
+                _resourceUsages[setIndex].Add(new ResourceUsage(binding + index, type, stages));
             }
         }
 
@@ -190,8 +199,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
                 _resourceUsages[setIndex].Add(new ResourceUsage(
                     buffer.Binding,
                     isStorage ? ResourceType.StorageBuffer : ResourceType.UniformBuffer,
-                    stages,
-                    buffer.Flags.HasFlag(BufferUsageFlags.Write) ? ResourceAccess.ReadWrite : ResourceAccess.Read));
+                    stages));
             }
         }
 
@@ -215,8 +223,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
                 _resourceUsages[setIndex].Add(new ResourceUsage(
                     texture.Binding,
                     type,
-                    stages,
-                    texture.Flags.HasFlag(TextureUsageFlags.ImageStore) ? ResourceAccess.ReadWrite : ResourceAccess.Read));
+                    stages));
             }
         }
 
@@ -285,11 +292,28 @@ namespace Ryujinx.Graphics.Gpu.Shader
         /// <returns>Shader information</returns>
         public static ShaderInfo BuildForCompute(GpuContext context, ShaderProgramInfo info, bool fromCache = false)
         {
-            ShaderInfoBuilder builder = new(context, tfEnabled: false);
+            ShaderInfoBuilder builder = new(context, tfEnabled: false, vertexAsCompute: false);
 
             builder.AddStageInfo(info);
 
             return builder.Build(null, fromCache);
         }
+
+        /// <summary>
+        /// Builds shader information for a vertex or geometry shader thas was converted to compute shader.
+        /// </summary>
+        /// <param name="context">GPU context that owns the shader</param>
+        /// <param name="info">Compute shader information</param>
+        /// <param name="tfEnabled">Indicates if the graphics shader is used with transform feedback enabled</param>
+        /// <param name="fromCache">True if the compute shader comes from a disk cache, false otherwise</param>
+        /// <returns>Shader information</returns>
+        public static ShaderInfo BuildForVertexAsCompute(GpuContext context, ShaderProgramInfo info, bool tfEnabled, bool fromCache = false)
+        {
+            ShaderInfoBuilder builder = new(context, tfEnabled, vertexAsCompute: true);
+
+            builder.AddStageInfo(info, vertexAsCompute: true);
+
+            return builder.Build(null, fromCache);
+        }
     }
 }
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderSpecializationList.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderSpecializationList.cs
index e57e1df1a..3c2f0b9be 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderSpecializationList.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderSpecializationList.cs
@@ -35,9 +35,16 @@ namespace Ryujinx.Graphics.Gpu.Shader
         {
             foreach (var entry in _entries)
             {
+                bool vertexAsCompute = entry.VertexAsCompute != null;
                 bool usesDrawParameters = entry.Shaders[1]?.Info.UsesDrawParameters ?? false;
 
-                if (entry.SpecializationState.MatchesGraphics(channel, ref poolState, ref graphicsState, usesDrawParameters, true))
+                if (entry.SpecializationState.MatchesGraphics(
+                    channel,
+                    ref poolState,
+                    ref graphicsState,
+                    vertexAsCompute,
+                    usesDrawParameters,
+                    checkTextures: true))
                 {
                     program = entry;
                     return true;
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderSpecializationState.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderSpecializationState.cs
index fcd953754..a41f761bd 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderSpecializationState.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderSpecializationState.cs
@@ -457,6 +457,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
         /// <param name="channel">GPU channel</param>
         /// <param name="poolState">Texture pool state</param>
         /// <param name="graphicsState">Graphics state</param>
+        /// <param name="vertexAsCompute">Indicates that the vertex shader has been converted into a compute shader</param>
         /// <param name="usesDrawParameters">Indicates whether the vertex shader accesses draw parameters</param>
         /// <param name="checkTextures">Indicates whether texture descriptors should be checked</param>
         /// <returns>True if the state matches, false otherwise</returns>
@@ -464,6 +465,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
             GpuChannel channel,
             ref GpuChannelPoolState poolState,
             ref GpuChannelGraphicsState graphicsState,
+            bool vertexAsCompute,
             bool usesDrawParameters,
             bool checkTextures)
         {
@@ -497,9 +499,25 @@ namespace Ryujinx.Graphics.Gpu.Shader
                 return false;
             }
 
-            if (!graphicsState.AttributeTypes.AsSpan().SequenceEqual(GraphicsState.AttributeTypes.AsSpan()))
+            if (ShaderCache.MayConvertVtgToCompute(ref channel.Capabilities) && !vertexAsCompute)
             {
-                return false;
+                for (int index = 0; index < graphicsState.AttributeTypes.Length; index++)
+                {
+                    AttributeType lType = FilterAttributeType(channel, graphicsState.AttributeTypes[index]);
+                    AttributeType rType = FilterAttributeType(channel, GraphicsState.AttributeTypes[index]);
+
+                    if (lType != rType)
+                    {
+                        return false;
+                    }
+                }
+            }
+            else
+            {
+                if (!graphicsState.AttributeTypes.AsSpan().SequenceEqual(GraphicsState.AttributeTypes.AsSpan()))
+                {
+                    return false;
+                }
             }
 
             if (usesDrawParameters && graphicsState.HasConstantBufferDrawParameters != GraphicsState.HasConstantBufferDrawParameters)
@@ -530,6 +548,19 @@ namespace Ryujinx.Graphics.Gpu.Shader
             return Matches(channel, ref poolState, checkTextures, isCompute: false);
         }
 
+        private static AttributeType FilterAttributeType(GpuChannel channel, AttributeType type)
+        {
+            type &= ~(AttributeType.Packed | AttributeType.PackedRgb10A2Signed);
+
+            if (channel.Capabilities.SupportsScaledVertexFormats &&
+                (type == AttributeType.Sscaled || type == AttributeType.Uscaled))
+            {
+                type = AttributeType.Float;
+            }
+
+            return type;
+        }
+
         /// <summary>
         /// Checks if the recorded state matches the current GPU compute engine state.
         /// </summary>
diff --git a/src/Ryujinx.Graphics.Gpu/Window.cs b/src/Ryujinx.Graphics.Gpu/Window.cs
index 1f94122d8..3b2368537 100644
--- a/src/Ryujinx.Graphics.Gpu/Window.cs
+++ b/src/Ryujinx.Graphics.Gpu/Window.cs
@@ -200,7 +200,7 @@ namespace Ryujinx.Graphics.Gpu
             {
                 pt.AcquireCallback(_context, pt.UserObj);
 
-                Image.Texture texture = pt.Cache.FindOrCreateTexture(null, TextureSearchFlags.WithUpscale, pt.Info, 0, pt.Range);
+                Image.Texture texture = pt.Cache.FindOrCreateTexture(null, TextureSearchFlags.WithUpscale, pt.Info, 0, range: pt.Range);
 
                 pt.Cache.Tick();
 
diff --git a/src/Ryujinx.Graphics.OpenGL/Constants.cs b/src/Ryujinx.Graphics.OpenGL/Constants.cs
index 8817011a9..38fedea0d 100644
--- a/src/Ryujinx.Graphics.OpenGL/Constants.cs
+++ b/src/Ryujinx.Graphics.OpenGL/Constants.cs
@@ -7,5 +7,6 @@
         public const int MaxVertexAttribs = 16;
         public const int MaxVertexBuffers = 16;
         public const int MaxTransformFeedbackBuffers = 4;
+        public const int MaxSubgroupSize = 64;
     }
 }
diff --git a/src/Ryujinx.Graphics.OpenGL/HwCapabilities.cs b/src/Ryujinx.Graphics.OpenGL/HwCapabilities.cs
index df12de600..e9a1aa78e 100644
--- a/src/Ryujinx.Graphics.OpenGL/HwCapabilities.cs
+++ b/src/Ryujinx.Graphics.OpenGL/HwCapabilities.cs
@@ -30,6 +30,7 @@ namespace Ryujinx.Graphics.OpenGL
 
         private static readonly Lazy<int> _maximumComputeSharedMemorySize = new(() => GetLimit(All.MaxComputeSharedMemorySize));
         private static readonly Lazy<int> _storageBufferOffsetAlignment = new(() => GetLimit(All.ShaderStorageBufferOffsetAlignment));
+        private static readonly Lazy<int> _textureBufferOffsetAlignment = new(() => GetLimit(All.TextureBufferOffsetAlignment));
 
         public enum GpuVendor
         {
@@ -80,6 +81,7 @@ namespace Ryujinx.Graphics.OpenGL
 
         public static int MaximumComputeSharedMemorySize => _maximumComputeSharedMemorySize.Value;
         public static int StorageBufferOffsetAlignment => _storageBufferOffsetAlignment.Value;
+        public static int TextureBufferOffsetAlignment => _textureBufferOffsetAlignment.Value;
 
         public static float MaximumSupportedAnisotropy => _maxSupportedAnisotropy.Value;
 
diff --git a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
index d99250470..060ab815b 100644
--- a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
+++ b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
@@ -164,6 +164,7 @@ namespace Ryujinx.Graphics.OpenGL
                 supportsShaderBarrierDivergence: !(intelWindows || intelUnix),
                 supportsShaderFloat64: true,
                 supportsTextureShadowLod: HwCapabilities.SupportsTextureShadowLod,
+                supportsVertexStoreAndAtomics: true,
                 supportsViewportIndexVertexTessellation: HwCapabilities.SupportsShaderViewportLayerArray,
                 supportsViewportMask: HwCapabilities.SupportsViewportArray2,
                 supportsViewportSwizzle: HwCapabilities.SupportsViewportSwizzle,
@@ -176,7 +177,9 @@ namespace Ryujinx.Graphics.OpenGL
                 maximumImagesPerStage: 8,
                 maximumComputeSharedMemorySize: HwCapabilities.MaximumComputeSharedMemorySize,
                 maximumSupportedAnisotropy: HwCapabilities.MaximumSupportedAnisotropy,
+                shaderSubgroupSize: Constants.MaxSubgroupSize,
                 storageBufferOffsetAlignment: HwCapabilities.StorageBufferOffsetAlignment,
+                textureBufferOffsetAlignment: HwCapabilities.TextureBufferOffsetAlignment,
                 gatherBiasPrecision: intelWindows || amdWindows ? 8 : 0); // Precision is 8 for these vendors on Vulkan.
         }
 
diff --git a/src/Ryujinx.Graphics.Shader/AttributeType.cs b/src/Ryujinx.Graphics.Shader/AttributeType.cs
index 1d9507731..d2d146ecb 100644
--- a/src/Ryujinx.Graphics.Shader/AttributeType.cs
+++ b/src/Ryujinx.Graphics.Shader/AttributeType.cs
@@ -11,13 +11,17 @@ namespace Ryujinx.Graphics.Shader
         Uint,
         Sscaled,
         Uscaled,
+
+        Packed = 1 << 6,
+        PackedRgb10A2Signed = 1 << 7,
+        AnyPacked = Packed | PackedRgb10A2Signed,
     }
 
     static class AttributeTypeExtensions
     {
         public static AggregateType ToAggregateType(this AttributeType type)
         {
-            return type switch
+            return (type & ~AttributeType.AnyPacked) switch
             {
                 AttributeType.Float => AggregateType.FP32,
                 AttributeType.Sint => AggregateType.S32,
@@ -28,7 +32,7 @@ namespace Ryujinx.Graphics.Shader
 
         public static AggregateType ToAggregateType(this AttributeType type, bool supportsScaledFormats)
         {
-            return type switch
+            return (type & ~AttributeType.AnyPacked) switch
             {
                 AttributeType.Float => AggregateType.FP32,
                 AttributeType.Sint => AggregateType.S32,
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/CodeGenContext.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/CodeGenContext.cs
index 7506f72af..cd9c71280 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/CodeGenContext.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/CodeGenContext.cs
@@ -92,14 +92,14 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
 
         private static string GetIndentation(int level)
         {
-            string indentation = string.Empty;
+            StringBuilder indentationBuilder = new();
 
             for (int index = 0; index < level; index++)
             {
-                indentation += Tab;
+                indentationBuilder.Append(Tab);
             }
 
-            return indentation;
+            return indentationBuilder.ToString();
         }
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
index e181ae98d..500de71f6 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
@@ -25,6 +25,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
             {
                 context.AppendLine("#extension GL_KHR_shader_subgroup_basic : enable");
                 context.AppendLine("#extension GL_KHR_shader_subgroup_ballot : enable");
+                context.AppendLine("#extension GL_KHR_shader_subgroup_shuffle : enable");
             }
 
             context.AppendLine("#extension GL_ARB_shader_group_vote : enable");
@@ -99,10 +100,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
                     else
                     {
                         string outPrimitive = context.Definitions.OutputTopology.ToGlslString();
-
-                        int maxOutputVertices = context.Definitions.GpPassthrough
-                            ? context.Definitions.InputTopology.ToInputVertices()
-                            : context.Definitions.MaxOutputVertices;
+                        int maxOutputVertices = context.Definitions.MaxOutputVertices;
 
                         context.AppendLine($"layout ({outPrimitive}, max_vertices = {maxOutputVertices}) out;");
                     }
@@ -201,26 +199,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
                 AppendHelperFunction(context, "Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/MultiplyHighU32.glsl");
             }
 
-            if ((info.HelperFunctionsMask & HelperFunctionsMask.Shuffle) != 0)
-            {
-                AppendHelperFunction(context, "Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/Shuffle.glsl");
-            }
-
-            if ((info.HelperFunctionsMask & HelperFunctionsMask.ShuffleDown) != 0)
-            {
-                AppendHelperFunction(context, "Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/ShuffleDown.glsl");
-            }
-
-            if ((info.HelperFunctionsMask & HelperFunctionsMask.ShuffleUp) != 0)
-            {
-                AppendHelperFunction(context, "Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/ShuffleUp.glsl");
-            }
-
-            if ((info.HelperFunctionsMask & HelperFunctionsMask.ShuffleXor) != 0)
-            {
-                AppendHelperFunction(context, "Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/ShuffleXor.glsl");
-            }
-
             if ((info.HelperFunctionsMask & HelperFunctionsMask.SwizzleAdd) != 0)
             {
                 AppendHelperFunction(context, "Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/SwizzleAdd.glsl");
@@ -339,15 +317,22 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
             {
                 string typeName = GetVarTypeName(context, memory.Type & ~AggregateType.Array);
 
-                if (memory.ArrayLength > 0)
+                if (memory.Type.HasFlag(AggregateType.Array))
                 {
-                    string arraySize = memory.ArrayLength.ToString(CultureInfo.InvariantCulture);
+                    if (memory.ArrayLength > 0)
+                    {
+                        string arraySize = memory.ArrayLength.ToString(CultureInfo.InvariantCulture);
 
-                    context.AppendLine($"{prefix}{typeName} {memory.Name}[{arraySize}];");
+                        context.AppendLine($"{prefix}{typeName} {memory.Name}[{arraySize}];");
+                    }
+                    else
+                    {
+                        context.AppendLine($"{prefix}{typeName} {memory.Name}[];");
+                    }
                 }
                 else
                 {
-                    context.AppendLine($"{prefix}{typeName} {memory.Name}[];");
+                    context.AppendLine($"{prefix}{typeName} {memory.Name};");
                 }
             }
         }
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/HelperFunctionNames.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/HelperFunctionNames.cs
index 221802727..0b80ac2b6 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/HelperFunctionNames.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/HelperFunctionNames.cs
@@ -5,10 +5,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
         public static string MultiplyHighS32 = "Helper_MultiplyHighS32";
         public static string MultiplyHighU32 = "Helper_MultiplyHighU32";
 
-        public static string Shuffle = "Helper_Shuffle";
-        public static string ShuffleDown = "Helper_ShuffleDown";
-        public static string ShuffleUp = "Helper_ShuffleUp";
-        public static string ShuffleXor = "Helper_ShuffleXor";
         public static string SwizzleAdd = "Helper_SwizzleAdd";
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/Shuffle.glsl b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/Shuffle.glsl
deleted file mode 100644
index 7cb4764dd..000000000
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/Shuffle.glsl
+++ /dev/null
@@ -1,11 +0,0 @@
-float Helper_Shuffle(float x, uint index, uint mask, out bool valid)
-{
-    uint clamp = mask & 0x1fu;
-    uint segMask = (mask >> 8) & 0x1fu;
-    uint minThreadId = $SUBGROUP_INVOCATION$ & segMask;
-    uint maxThreadId = minThreadId | (clamp & ~segMask);
-    uint srcThreadId = (index & ~segMask) | minThreadId;
-    valid = srcThreadId <= maxThreadId;
-    float v = $SUBGROUP_BROADCAST$(x, srcThreadId);
-    return valid ? v : x;
-}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/ShuffleDown.glsl b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/ShuffleDown.glsl
deleted file mode 100644
index 71d901d5d..000000000
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/ShuffleDown.glsl
+++ /dev/null
@@ -1,11 +0,0 @@
-float Helper_ShuffleDown(float x, uint index, uint mask, out bool valid)
-{
-    uint clamp = mask & 0x1fu;
-    uint segMask = (mask >> 8) & 0x1fu;
-    uint minThreadId = $SUBGROUP_INVOCATION$ & segMask;
-    uint maxThreadId = minThreadId | (clamp & ~segMask);
-    uint srcThreadId = $SUBGROUP_INVOCATION$ + index;
-    valid = srcThreadId <= maxThreadId;
-    float v = $SUBGROUP_BROADCAST$(x, srcThreadId);
-    return valid ? v : x;
-}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/ShuffleUp.glsl b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/ShuffleUp.glsl
deleted file mode 100644
index ae264d870..000000000
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/ShuffleUp.glsl
+++ /dev/null
@@ -1,9 +0,0 @@
-float Helper_ShuffleUp(float x, uint index, uint mask, out bool valid)
-{
-    uint segMask = (mask >> 8) & 0x1fu;
-    uint minThreadId = $SUBGROUP_INVOCATION$ & segMask;
-    uint srcThreadId = $SUBGROUP_INVOCATION$ - index;
-    valid = int(srcThreadId) >= int(minThreadId);
-    float v = $SUBGROUP_BROADCAST$(x, srcThreadId);
-    return valid ? v : x;
-}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/ShuffleXor.glsl b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/ShuffleXor.glsl
deleted file mode 100644
index 789089d69..000000000
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/ShuffleXor.glsl
+++ /dev/null
@@ -1,11 +0,0 @@
-float Helper_ShuffleXor(float x, uint index, uint mask, out bool valid)
-{
-    uint clamp = mask & 0x1fu;
-    uint segMask = (mask >> 8) & 0x1fu;
-    uint minThreadId = $SUBGROUP_INVOCATION$ & segMask;
-    uint maxThreadId = minThreadId | (clamp & ~segMask);
-    uint srcThreadId = $SUBGROUP_INVOCATION$ ^ index;
-    valid = srcThreadId <= maxThreadId;
-    float v = $SUBGROUP_BROADCAST$(x, srcThreadId);
-    return valid ? v : x;
-}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGen.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGen.cs
index 9208ceead..eb0cb92db 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGen.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGen.cs
@@ -2,6 +2,7 @@ using Ryujinx.Graphics.Shader.IntermediateRepresentation;
 using Ryujinx.Graphics.Shader.StructuredIr;
 using Ryujinx.Graphics.Shader.Translation;
 using System;
+using System.Text;
 
 using static Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions.InstGenBallot;
 using static Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions.InstGenCall;
@@ -9,6 +10,7 @@ using static Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions.InstGenFSI;
 using static Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions.InstGenHelper;
 using static Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions.InstGenMemory;
 using static Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions.InstGenPacking;
+using static Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions.InstGenShuffle;
 using static Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions.InstGenVector;
 using static Ryujinx.Graphics.Shader.StructuredIr.InstructionInfo;
 
@@ -66,11 +68,11 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
 
                 int arity = (int)(info.Type & InstType.ArityMask);
 
-                string args = string.Empty;
+                StringBuilder builder = new();
 
                 if (atomic && (operation.StorageKind == StorageKind.StorageBuffer || operation.StorageKind == StorageKind.SharedMemory))
                 {
-                    args = GenerateLoadOrStore(context, operation, isStore: false);
+                    builder.Append(GenerateLoadOrStore(context, operation, isStore: false));
 
                     AggregateType dstType = operation.Inst == Instruction.AtomicMaxS32 || operation.Inst == Instruction.AtomicMinS32
                         ? AggregateType.S32
@@ -78,7 +80,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
 
                     for (int argIndex = operation.SourcesCount - arity + 2; argIndex < operation.SourcesCount; argIndex++)
                     {
-                        args += ", " + GetSoureExpr(context, operation.GetSource(argIndex), dstType);
+                        builder.Append($", {GetSoureExpr(context, operation.GetSource(argIndex), dstType)}");
                     }
                 }
                 else
@@ -87,16 +89,16 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
                     {
                         if (argIndex != 0)
                         {
-                            args += ", ";
+                            builder.Append(", ");
                         }
 
                         AggregateType dstType = GetSrcVarType(inst, argIndex);
 
-                        args += GetSoureExpr(context, operation.GetSource(argIndex), dstType);
+                        builder.Append(GetSoureExpr(context, operation.GetSource(argIndex), dstType));
                     }
                 }
 
-                return info.OpName + '(' + args + ')';
+                return $"{info.OpName}({builder})";
             }
             else if ((info.Type & InstType.Op) != 0)
             {
@@ -174,14 +176,20 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
                     case Instruction.PackHalf2x16:
                         return PackHalf2x16(context, operation);
 
+                    case Instruction.Shuffle:
+                        return Shuffle(context, operation);
+
                     case Instruction.Store:
                         return Store(context, operation);
 
                     case Instruction.TextureSample:
                         return TextureSample(context, operation);
 
-                    case Instruction.TextureSize:
-                        return TextureSize(context, operation);
+                    case Instruction.TextureQuerySamples:
+                        return TextureQuerySamples(context, operation);
+
+                    case Instruction.TextureQuerySize:
+                        return TextureQuerySize(context, operation);
 
                     case Instruction.UnpackDouble2x32:
                         return UnpackDouble2x32(context, operation);
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenBallot.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenBallot.cs
index b44759c0d..6cc7048bd 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenBallot.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenBallot.cs
@@ -13,14 +13,15 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
             AggregateType dstType = GetSrcVarType(operation.Inst, 0);
 
             string arg = GetSoureExpr(context, operation.GetSource(0), dstType);
+            char component = "xyzw"[operation.Index];
 
             if (context.HostCapabilities.SupportsShaderBallot)
             {
-                return $"unpackUint2x32(ballotARB({arg})).x";
+                return $"unpackUint2x32(ballotARB({arg})).{component}";
             }
             else
             {
-                return $"subgroupBallot({arg}).x";
+                return $"subgroupBallot({arg}).{component}";
             }
         }
     }
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenHelper.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenHelper.cs
index c3d52b2c5..5c2d16f4c 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenHelper.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenHelper.cs
@@ -108,17 +108,18 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
             Add(Instruction.ShiftLeft,                InstType.OpBinary,       "<<",              3);
             Add(Instruction.ShiftRightS32,            InstType.OpBinary,       ">>",              3);
             Add(Instruction.ShiftRightU32,            InstType.OpBinary,       ">>",              3);
-            Add(Instruction.Shuffle,                  InstType.CallQuaternary, HelperFunctionNames.Shuffle);
-            Add(Instruction.ShuffleDown,              InstType.CallQuaternary, HelperFunctionNames.ShuffleDown);
-            Add(Instruction.ShuffleUp,                InstType.CallQuaternary, HelperFunctionNames.ShuffleUp);
-            Add(Instruction.ShuffleXor,               InstType.CallQuaternary, HelperFunctionNames.ShuffleXor);
+            Add(Instruction.Shuffle,                  InstType.Special);
+            Add(Instruction.ShuffleDown,              InstType.CallBinary,     "subgroupShuffleDown");
+            Add(Instruction.ShuffleUp,                InstType.CallBinary,     "subgroupShuffleUp");
+            Add(Instruction.ShuffleXor,               InstType.CallBinary,     "subgroupShuffleXor");
             Add(Instruction.Sine,                     InstType.CallUnary,      "sin");
             Add(Instruction.SquareRoot,               InstType.CallUnary,      "sqrt");
             Add(Instruction.Store,                    InstType.Special);
             Add(Instruction.Subtract,                 InstType.OpBinary,       "-",               2);
             Add(Instruction.SwizzleAdd,               InstType.CallTernary,    HelperFunctionNames.SwizzleAdd);
             Add(Instruction.TextureSample,            InstType.Special);
-            Add(Instruction.TextureSize,              InstType.Special);
+            Add(Instruction.TextureQuerySamples,      InstType.Special);
+            Add(Instruction.TextureQuerySize,         InstType.Special);
             Add(Instruction.Truncate,                 InstType.CallUnary,      "trunc");
             Add(Instruction.UnpackDouble2x32,         InstType.Special);
             Add(Instruction.UnpackHalf2x16,           InstType.Special);
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
index a1f92d11a..2e90bd16d 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
@@ -517,7 +517,33 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
             return texCall;
         }
 
-        public static string TextureSize(CodeGenContext context, AstOperation operation)
+        public static string TextureQuerySamples(CodeGenContext context, AstOperation operation)
+        {
+            AstTextureOperation texOp = (AstTextureOperation)operation;
+
+            bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
+
+            // TODO: Bindless texture support. For now we just return 0.
+            if (isBindless)
+            {
+                return NumberFormatter.FormatInt(0);
+            }
+
+            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
+
+            string indexExpr = null;
+
+            if (isIndexed)
+            {
+                indexExpr = GetSoureExpr(context, texOp.GetSource(0), AggregateType.S32);
+            }
+
+            string samplerName = GetSamplerName(context.Properties, texOp, indexExpr);
+
+            return $"textureSamples({samplerName})";
+        }
+
+        public static string TextureQuerySize(CodeGenContext context, AstOperation operation)
         {
             AstTextureOperation texOp = (AstTextureOperation)operation;
 
@@ -753,17 +779,18 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
 
         private static string GetMaskMultiDest(int mask)
         {
-            string swizzle = ".";
+            StringBuilder swizzleBuilder = new();
+            swizzleBuilder.Append('.');
 
             for (int i = 0; i < 4; i++)
             {
                 if ((mask & (1 << i)) != 0)
                 {
-                    swizzle += "xyzw"[i];
+                    swizzleBuilder.Append("xyzw"[i]);
                 }
             }
 
-            return swizzle;
+            return swizzleBuilder.ToString();
         }
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenShuffle.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenShuffle.cs
new file mode 100644
index 000000000..6d3859efd
--- /dev/null
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenShuffle.cs
@@ -0,0 +1,25 @@
+using Ryujinx.Graphics.Shader.StructuredIr;
+using Ryujinx.Graphics.Shader.Translation;
+
+using static Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions.InstGenHelper;
+
+namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
+{
+    static class InstGenShuffle
+    {
+        public static string Shuffle(CodeGenContext context, AstOperation operation)
+        {
+            string value = GetSoureExpr(context, operation.GetSource(0), AggregateType.FP32);
+            string index = GetSoureExpr(context, operation.GetSource(1), AggregateType.U32);
+
+            if (context.HostCapabilities.SupportsShaderBallot)
+            {
+                return $"readInvocationARB({value}, {index})";
+            }
+            else
+            {
+                return $"subgroupShuffle({value}, {index})";
+            }
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/IoMap.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/IoMap.cs
index b5f453aef..caa6ef642 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/IoMap.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/IoMap.cs
@@ -31,6 +31,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
                 IoVariable.FrontColorDiffuse => ("gl_FrontColor", AggregateType.Vector4 | AggregateType.FP32), // Deprecated.
                 IoVariable.FrontColorSpecular => ("gl_FrontSecondaryColor", AggregateType.Vector4 | AggregateType.FP32), // Deprecated.
                 IoVariable.FrontFacing => ("gl_FrontFacing", AggregateType.Bool),
+                IoVariable.GlobalId => ("gl_GlobalInvocationID", AggregateType.Vector3 | AggregateType.U32),
                 IoVariable.InstanceId => ("gl_InstanceID", AggregateType.S32),
                 IoVariable.InstanceIndex => ("gl_InstanceIndex", AggregateType.S32),
                 IoVariable.InvocationId => ("gl_InvocationID", AggregateType.S32),
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs
index d385782af..53267c60b 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs
@@ -27,8 +27,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
         public ILogger Logger { get; }
         public TargetApi TargetApi { get; }
 
-        public int InputVertices { get; }
-
         public Dictionary<int, Instruction> ConstantBuffers { get; } = new();
         public Dictionary<int, Instruction> StorageBuffers { get; } = new();
 
@@ -46,7 +44,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
         public StructuredFunction CurrentFunction { get; set; }
         private readonly Dictionary<AstOperand, Instruction> _locals = new();
-        private readonly Dictionary<int, Instruction[]> _localForArgs = new();
         private readonly Dictionary<int, Instruction> _funcArgs = new();
         private readonly Dictionary<int, (StructuredFunction, Instruction)> _functions = new();
 
@@ -101,19 +98,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             Logger = parameters.Logger;
             TargetApi = parameters.TargetApi;
 
-            if (parameters.Definitions.Stage == ShaderStage.Geometry)
-            {
-                InputVertices = parameters.Definitions.InputTopology switch
-                {
-                    InputTopology.Points => 1,
-                    InputTopology.Lines => 2,
-                    InputTopology.LinesAdjacency => 2,
-                    InputTopology.Triangles => 3,
-                    InputTopology.TrianglesAdjacency => 3,
-                    _ => throw new InvalidOperationException($"Invalid input topology \"{parameters.Definitions.InputTopology}\"."),
-                };
-            }
-
             AddCapability(Capability.Shader);
             AddCapability(Capability.Float64);
 
@@ -127,7 +111,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             IsMainFunction = isMainFunction;
             MayHaveReturned = false;
             _locals.Clear();
-            _localForArgs.Clear();
             _funcArgs.Clear();
         }
 
@@ -184,11 +167,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             _locals.Add(local, spvLocal);
         }
 
-        public void DeclareLocalForArgs(int funcIndex, Instruction[] spvLocals)
-        {
-            _localForArgs.Add(funcIndex, spvLocals);
-        }
-
         public void DeclareArgument(int argIndex, Instruction spvLocal)
         {
             _funcArgs.Add(argIndex, spvLocal);
@@ -293,11 +271,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             return _locals[local];
         }
 
-        public Instruction[] GetLocalForArgsPointers(int funcIndex)
-        {
-            return _localForArgs[funcIndex];
-        }
-
         public Instruction GetArgumentPointer(AstOperand funcArg)
         {
             return _funcArgs[funcArg.Value];
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs
index 153a6940b..45933a21b 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs
@@ -41,28 +41,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             }
         }
 
-        public static void DeclareLocalForArgs(CodeGenContext context, List<StructuredFunction> functions)
-        {
-            for (int funcIndex = 0; funcIndex < functions.Count; funcIndex++)
-            {
-                StructuredFunction function = functions[funcIndex];
-                SpvInstruction[] locals = new SpvInstruction[function.InArguments.Length];
-
-                for (int i = 0; i < function.InArguments.Length; i++)
-                {
-                    var type = function.GetArgumentType(i);
-                    var localPointerType = context.TypePointer(StorageClass.Function, context.GetType(type));
-                    var spvLocal = context.Variable(localPointerType, StorageClass.Function);
-
-                    context.AddLocalVariable(spvLocal);
-
-                    locals[i] = spvLocal;
-                }
-
-                context.DeclareLocalForArgs(funcIndex, locals);
-            }
-        }
-
         public static void DeclareAll(CodeGenContext context, StructuredProgramInfo info)
         {
             DeclareConstantBuffers(context, context.Properties.ConstantBuffers.Values);
@@ -369,7 +347,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                 if (context.Definitions.Stage != ShaderStage.Vertex)
                 {
                     var perVertexInputStructType = CreatePerVertexStructType(context);
-                    int arraySize = context.Definitions.Stage == ShaderStage.Geometry ? context.InputVertices : 32;
+                    int arraySize = context.Definitions.Stage == ShaderStage.Geometry ? context.Definitions.InputTopology.ToInputVertices() : 32;
                     var perVertexInputArrayType = context.TypeArray(perVertexInputStructType, context.Constant(context.TypeU32(), arraySize));
                     var perVertexInputPointerType = context.TypePointer(StorageClass.Input, perVertexInputArrayType);
                     var perVertexInputVariable = context.Variable(perVertexInputPointerType, StorageClass.Input);
@@ -434,6 +412,11 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
             context.Decorate(perVertexStructType, Decoration.Block);
 
+            if (context.HostCapabilities.ReducedPrecision)
+            {
+                context.MemberDecorate(perVertexStructType, 0, Decoration.Invariant);
+            }
+
             context.MemberDecorate(perVertexStructType, 0, Decoration.BuiltIn, (LiteralInteger)BuiltIn.Position);
             context.MemberDecorate(perVertexStructType, 1, Decoration.BuiltIn, (LiteralInteger)BuiltIn.PointSize);
             context.MemberDecorate(perVertexStructType, 2, Decoration.BuiltIn, (LiteralInteger)BuiltIn.ClipDistance);
@@ -501,7 +484,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
             if (!isPerPatch && IoMap.IsPerVertex(ioVariable, context.Definitions.Stage, isOutput))
             {
-                int arraySize = context.Definitions.Stage == ShaderStage.Geometry ? context.InputVertices : 32;
+                int arraySize = context.Definitions.Stage == ShaderStage.Geometry ? context.Definitions.InputTopology.ToInputVertices() : 32;
                 spvType = context.TypeArray(spvType, context.Constant(context.TypeU32(), arraySize));
 
                 if (context.Definitions.GpPassthrough && context.HostCapabilities.SupportsGeometryShaderPassthrough)
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs
index 98c1b9d28..50a73ab83 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs
@@ -134,7 +134,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             Add(Instruction.Subtract,                 GenerateSubtract);
             Add(Instruction.SwizzleAdd,               GenerateSwizzleAdd);
             Add(Instruction.TextureSample,            GenerateTextureSample);
-            Add(Instruction.TextureSize,              GenerateTextureSize);
+            Add(Instruction.TextureQuerySamples,      GenerateTextureQuerySamples);
+            Add(Instruction.TextureQuerySize,         GenerateTextureQuerySize);
             Add(Instruction.Truncate,                 GenerateTruncate);
             Add(Instruction.UnpackDouble2x32,         GenerateUnpackDouble2x32);
             Add(Instruction.UnpackHalf2x16,           GenerateUnpackHalf2x16);
@@ -231,7 +232,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             var execution = context.Constant(context.TypeU32(), Scope.Subgroup);
 
             var maskVector = context.GroupNonUniformBallot(uvec4Type, execution, context.Get(AggregateType.Bool, source));
-            var mask = context.CompositeExtract(context.TypeU32(), maskVector, (SpvLiteralInteger)0);
+            var mask = context.CompositeExtract(context.TypeU32(), maskVector, (SpvLiteralInteger)operation.Index);
 
             return new OperationResult(AggregateType.U32, mask);
         }
@@ -310,26 +311,14 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             var (function, spvFunc) = context.GetFunction(funcId.Value);
 
             var args = new SpvInstruction[operation.SourcesCount - 1];
-            var spvLocals = context.GetLocalForArgsPointers(funcId.Value);
 
             for (int i = 0; i < args.Length; i++)
             {
                 var operand = operation.GetSource(i + 1);
 
-                if (i >= function.InArguments.Length)
-                {
-                    args[i] = context.GetLocalPointer((AstOperand)operand);
-                }
-                else
-                {
-                    var type = function.GetArgumentType(i);
-                    var value = context.Get(type, operand);
-                    var spvLocal = spvLocals[i];
-
-                    context.Store(spvLocal, value);
-
-                    args[i] = spvLocal;
-                }
+                AstOperand local = (AstOperand)operand;
+                Debug.Assert(local.Type == OperandType.LocalVariable);
+                args[i] = context.GetLocalPointer(local);
             }
 
             var retType = function.ReturnType;
@@ -1100,117 +1089,40 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
         private static OperationResult GenerateShuffle(CodeGenContext context, AstOperation operation)
         {
-            var x = context.GetFP32(operation.GetSource(0));
+            var value = context.GetFP32(operation.GetSource(0));
             var index = context.GetU32(operation.GetSource(1));
-            var mask = context.GetU32(operation.GetSource(2));
 
-            var const31 = context.Constant(context.TypeU32(), 31);
-            var const8 = context.Constant(context.TypeU32(), 8);
-
-            var clamp = context.BitwiseAnd(context.TypeU32(), mask, const31);
-            var segMask = context.BitwiseAnd(context.TypeU32(), context.ShiftRightLogical(context.TypeU32(), mask, const8), const31);
-            var notSegMask = context.Not(context.TypeU32(), segMask);
-            var clampNotSegMask = context.BitwiseAnd(context.TypeU32(), clamp, notSegMask);
-            var indexNotSegMask = context.BitwiseAnd(context.TypeU32(), index, notSegMask);
-
-            var threadId = GetScalarInput(context, IoVariable.SubgroupLaneId);
-
-            var minThreadId = context.BitwiseAnd(context.TypeU32(), threadId, segMask);
-            var maxThreadId = context.BitwiseOr(context.TypeU32(), minThreadId, clampNotSegMask);
-            var srcThreadId = context.BitwiseOr(context.TypeU32(), indexNotSegMask, minThreadId);
-            var valid = context.ULessThanEqual(context.TypeBool(), srcThreadId, maxThreadId);
-            var value = context.GroupNonUniformShuffle(context.TypeFP32(), context.Constant(context.TypeU32(), (int)Scope.Subgroup), x, srcThreadId);
-            var result = context.Select(context.TypeFP32(), valid, value, x);
-
-            var validLocal = (AstOperand)operation.GetSource(3);
-
-            context.Store(context.GetLocalPointer(validLocal), context.BitcastIfNeeded(validLocal.VarType, AggregateType.Bool, valid));
+            var result = context.GroupNonUniformShuffle(context.TypeFP32(), context.Constant(context.TypeU32(), (int)Scope.Subgroup), value, index);
 
             return new OperationResult(AggregateType.FP32, result);
         }
 
         private static OperationResult GenerateShuffleDown(CodeGenContext context, AstOperation operation)
         {
-            var x = context.GetFP32(operation.GetSource(0));
+            var value = context.GetFP32(operation.GetSource(0));
             var index = context.GetU32(operation.GetSource(1));
-            var mask = context.GetU32(operation.GetSource(2));
 
-            var const31 = context.Constant(context.TypeU32(), 31);
-            var const8 = context.Constant(context.TypeU32(), 8);
-
-            var clamp = context.BitwiseAnd(context.TypeU32(), mask, const31);
-            var segMask = context.BitwiseAnd(context.TypeU32(), context.ShiftRightLogical(context.TypeU32(), mask, const8), const31);
-            var notSegMask = context.Not(context.TypeU32(), segMask);
-            var clampNotSegMask = context.BitwiseAnd(context.TypeU32(), clamp, notSegMask);
-
-            var threadId = GetScalarInput(context, IoVariable.SubgroupLaneId);
-
-            var minThreadId = context.BitwiseAnd(context.TypeU32(), threadId, segMask);
-            var maxThreadId = context.BitwiseOr(context.TypeU32(), minThreadId, clampNotSegMask);
-            var srcThreadId = context.IAdd(context.TypeU32(), threadId, index);
-            var valid = context.ULessThanEqual(context.TypeBool(), srcThreadId, maxThreadId);
-            var value = context.GroupNonUniformShuffle(context.TypeFP32(), context.Constant(context.TypeU32(), (int)Scope.Subgroup), x, srcThreadId);
-            var result = context.Select(context.TypeFP32(), valid, value, x);
-
-            var validLocal = (AstOperand)operation.GetSource(3);
-
-            context.Store(context.GetLocalPointer(validLocal), context.BitcastIfNeeded(validLocal.VarType, AggregateType.Bool, valid));
+            var result = context.GroupNonUniformShuffleDown(context.TypeFP32(), context.Constant(context.TypeU32(), (int)Scope.Subgroup), value, index);
 
             return new OperationResult(AggregateType.FP32, result);
         }
 
         private static OperationResult GenerateShuffleUp(CodeGenContext context, AstOperation operation)
         {
-            var x = context.GetFP32(operation.GetSource(0));
+            var value = context.GetFP32(operation.GetSource(0));
             var index = context.GetU32(operation.GetSource(1));
-            var mask = context.GetU32(operation.GetSource(2));
 
-            var const31 = context.Constant(context.TypeU32(), 31);
-            var const8 = context.Constant(context.TypeU32(), 8);
-
-            var segMask = context.BitwiseAnd(context.TypeU32(), context.ShiftRightLogical(context.TypeU32(), mask, const8), const31);
-
-            var threadId = GetScalarInput(context, IoVariable.SubgroupLaneId);
-
-            var minThreadId = context.BitwiseAnd(context.TypeU32(), threadId, segMask);
-            var srcThreadId = context.ISub(context.TypeU32(), threadId, index);
-            var valid = context.SGreaterThanEqual(context.TypeBool(), srcThreadId, minThreadId);
-            var value = context.GroupNonUniformShuffle(context.TypeFP32(), context.Constant(context.TypeU32(), (int)Scope.Subgroup), x, srcThreadId);
-            var result = context.Select(context.TypeFP32(), valid, value, x);
-
-            var validLocal = (AstOperand)operation.GetSource(3);
-
-            context.Store(context.GetLocalPointer(validLocal), context.BitcastIfNeeded(validLocal.VarType, AggregateType.Bool, valid));
+            var result = context.GroupNonUniformShuffleUp(context.TypeFP32(), context.Constant(context.TypeU32(), (int)Scope.Subgroup), value, index);
 
             return new OperationResult(AggregateType.FP32, result);
         }
 
         private static OperationResult GenerateShuffleXor(CodeGenContext context, AstOperation operation)
         {
-            var x = context.GetFP32(operation.GetSource(0));
+            var value = context.GetFP32(operation.GetSource(0));
             var index = context.GetU32(operation.GetSource(1));
-            var mask = context.GetU32(operation.GetSource(2));
 
-            var const31 = context.Constant(context.TypeU32(), 31);
-            var const8 = context.Constant(context.TypeU32(), 8);
-
-            var clamp = context.BitwiseAnd(context.TypeU32(), mask, const31);
-            var segMask = context.BitwiseAnd(context.TypeU32(), context.ShiftRightLogical(context.TypeU32(), mask, const8), const31);
-            var notSegMask = context.Not(context.TypeU32(), segMask);
-            var clampNotSegMask = context.BitwiseAnd(context.TypeU32(), clamp, notSegMask);
-
-            var threadId = GetScalarInput(context, IoVariable.SubgroupLaneId);
-
-            var minThreadId = context.BitwiseAnd(context.TypeU32(), threadId, segMask);
-            var maxThreadId = context.BitwiseOr(context.TypeU32(), minThreadId, clampNotSegMask);
-            var srcThreadId = context.BitwiseXor(context.TypeU32(), threadId, index);
-            var valid = context.ULessThanEqual(context.TypeBool(), srcThreadId, maxThreadId);
-            var value = context.GroupNonUniformShuffle(context.TypeFP32(), context.Constant(context.TypeU32(), (int)Scope.Subgroup), x, srcThreadId);
-            var result = context.Select(context.TypeFP32(), valid, value, x);
-
-            var validLocal = (AstOperand)operation.GetSource(3);
-
-            context.Store(context.GetLocalPointer(validLocal), context.BitcastIfNeeded(validLocal.VarType, AggregateType.Bool, valid));
+            var result = context.GroupNonUniformShuffleXor(context.TypeFP32(), context.Constant(context.TypeU32(), (int)Scope.Subgroup), value, index);
 
             return new OperationResult(AggregateType.FP32, result);
         }
@@ -1569,7 +1481,36 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             return new OperationResult(swizzledResultType, result);
         }
 
-        private static OperationResult GenerateTextureSize(CodeGenContext context, AstOperation operation)
+        private static OperationResult GenerateTextureQuerySamples(CodeGenContext context, AstOperation operation)
+        {
+            AstTextureOperation texOp = (AstTextureOperation)operation;
+
+            bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
+
+            // TODO: Bindless texture support. For now we just return 0.
+            if (isBindless)
+            {
+                return new OperationResult(AggregateType.S32, context.Constant(context.TypeS32(), 0));
+            }
+
+            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
+
+            if (isIndexed)
+            {
+                context.GetS32(texOp.GetSource(0));
+            }
+
+            (var imageType, var sampledImageType, var sampledImageVariable) = context.Samplers[texOp.Binding];
+
+            var image = context.Load(sampledImageType, sampledImageVariable);
+            image = context.Image(imageType, image);
+
+            SpvInstruction result = context.ImageQuerySamples(context.TypeS32(), image);
+
+            return new OperationResult(AggregateType.S32, result);
+        }
+
+        private static OperationResult GenerateTextureQuerySize(CodeGenContext context, AstOperation operation)
         {
             AstTextureOperation texOp = (AstTextureOperation)operation;
 
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/IoMap.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/IoMap.cs
index 08d403e2f..7b4e14ff0 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/IoMap.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/IoMap.cs
@@ -22,6 +22,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                 IoVariable.FragmentCoord => (BuiltIn.FragCoord, AggregateType.Vector4 | AggregateType.FP32),
                 IoVariable.FragmentOutputDepth => (BuiltIn.FragDepth, AggregateType.FP32),
                 IoVariable.FrontFacing => (BuiltIn.FrontFacing, AggregateType.Bool),
+                IoVariable.GlobalId => (BuiltIn.GlobalInvocationId, AggregateType.Vector3 | AggregateType.U32),
                 IoVariable.InstanceId => (BuiltIn.InstanceId, AggregateType.S32),
                 IoVariable.InstanceIndex => (BuiltIn.InstanceIndex, AggregateType.S32),
                 IoVariable.InvocationId => (BuiltIn.InvocationId, AggregateType.S32),
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs
index 5eee888e4..a1e9054f6 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs
@@ -28,12 +28,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             _poolLock = new object();
         }
 
-        private const HelperFunctionsMask NeedsInvocationIdMask =
-            HelperFunctionsMask.Shuffle |
-            HelperFunctionsMask.ShuffleDown |
-            HelperFunctionsMask.ShuffleUp |
-            HelperFunctionsMask.ShuffleXor |
-            HelperFunctionsMask.SwizzleAdd;
+        private const HelperFunctionsMask NeedsInvocationIdMask = HelperFunctionsMask.SwizzleAdd;
 
         public static byte[] Generate(StructuredProgramInfo info, CodeGenParameters parameters)
         {
@@ -94,6 +89,16 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                 context.AddCapability(Capability.DrawParameters);
             }
 
+            if (context.Definitions.Stage != ShaderStage.Fragment &&
+                context.Definitions.Stage != ShaderStage.Geometry &&
+                context.Definitions.Stage != ShaderStage.Compute &&
+                (context.Info.IoDefinitions.Contains(new IoDefinition(StorageKind.Output, IoVariable.Layer)) ||
+                context.Info.IoDefinitions.Contains(new IoDefinition(StorageKind.Output, IoVariable.ViewportIndex))))
+            {
+                context.AddExtension("SPV_EXT_shader_viewport_index_layer");
+                context.AddCapability(Capability.ShaderViewportIndexLayerEXT);
+            }
+
             if (context.Info.IoDefinitions.Contains(new IoDefinition(StorageKind.Output, IoVariable.ViewportMask)))
             {
                 context.AddExtension("SPV_NV_viewport_array2");
@@ -156,7 +161,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             context.EnterBlock(function.MainBlock);
 
             Declarations.DeclareLocals(context, function);
-            Declarations.DeclareLocalForArgs(context, info.Functions);
 
             Generate(context, function.MainBlock);
 
@@ -244,9 +248,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                         _ => throw new InvalidOperationException($"Invalid output topology \"{context.Definitions.OutputTopology}\"."),
                     });
 
-                    int maxOutputVertices = context.Definitions.GpPassthrough ? context.InputVertices : context.Definitions.MaxOutputVertices;
-
-                    context.AddExecutionMode(spvFunc, ExecutionMode.OutputVertices, (SpvLiteralInteger)maxOutputVertices);
+                    context.AddExecutionMode(spvFunc, ExecutionMode.OutputVertices, (SpvLiteralInteger)context.Definitions.MaxOutputVertices);
                 }
                 else if (context.Definitions.Stage == ShaderStage.Fragment)
                 {
diff --git a/src/Ryujinx.Graphics.Shader/Constants.cs b/src/Ryujinx.Graphics.Shader/Constants.cs
index cff2c37a0..6317369f0 100644
--- a/src/Ryujinx.Graphics.Shader/Constants.cs
+++ b/src/Ryujinx.Graphics.Shader/Constants.cs
@@ -10,11 +10,5 @@ namespace Ryujinx.Graphics.Shader
         public const int NvnBaseVertexByteOffset = 0x640;
         public const int NvnBaseInstanceByteOffset = 0x644;
         public const int NvnDrawIndexByteOffset = 0x648;
-
-        // Transform Feedback emulation.
-
-        public const int TfeInfoBinding = 0;
-        public const int TfeBufferBaseBinding = 1;
-        public const int TfeBuffersCount = 4;
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/Decoders/DecodedProgram.cs b/src/Ryujinx.Graphics.Shader/Decoders/DecodedProgram.cs
index 67304d027..fdf3eacc3 100644
--- a/src/Ryujinx.Graphics.Shader/Decoders/DecodedProgram.cs
+++ b/src/Ryujinx.Graphics.Shader/Decoders/DecodedProgram.cs
@@ -60,6 +60,11 @@ namespace Ryujinx.Graphics.Shader.Decoders
             _functionsWithId.Add(function);
         }
 
+        public IoUsage GetIoUsage()
+        {
+            return new IoUsage(UsedFeatures, ClipDistancesWritten, AttributeUsage.UsedOutputAttributes);
+        }
+
         public IEnumerator<DecodedFunction> GetEnumerator()
         {
             return _functions.Values.GetEnumerator();
diff --git a/src/Ryujinx.Graphics.Shader/Decoders/Decoder.cs b/src/Ryujinx.Graphics.Shader/Decoders/Decoder.cs
index d18a9baf8..1211e561f 100644
--- a/src/Ryujinx.Graphics.Shader/Decoders/Decoder.cs
+++ b/src/Ryujinx.Graphics.Shader/Decoders/Decoder.cs
@@ -297,6 +297,9 @@ namespace Ryujinx.Graphics.Shader.Decoders
                     case InstName.Ssy:
                         block.AddPushOp(op);
                         break;
+                    case InstName.Shfl:
+                        context.SetUsedFeature(FeatureFlags.Shuffle);
+                        break;
                     case InstName.Ldl:
                     case InstName.Stl:
                         context.SetUsedFeature(FeatureFlags.LocalMemory);
@@ -307,6 +310,23 @@ namespace Ryujinx.Graphics.Shader.Decoders
                     case InstName.Sts:
                         context.SetUsedFeature(FeatureFlags.SharedMemory);
                         break;
+                    case InstName.Atom:
+                    case InstName.AtomCas:
+                    case InstName.Red:
+                    case InstName.Stg:
+                    case InstName.Suatom:
+                    case InstName.SuatomB:
+                    case InstName.SuatomB2:
+                    case InstName.SuatomCas:
+                    case InstName.SuatomCasB:
+                    case InstName.Sured:
+                    case InstName.SuredB:
+                    case InstName.Sust:
+                    case InstName.SustB:
+                    case InstName.SustD:
+                    case InstName.SustDB:
+                        context.SetUsedFeature(FeatureFlags.Store);
+                        break;
                 }
 
                 block.OpCodes.Add(op);
@@ -421,6 +441,12 @@ namespace Ryujinx.Graphics.Shader.Decoders
                                         context.SetUsedFeature(FeatureFlags.RtLayer);
                                     }
                                     break;
+                                case AttributeConsts.ViewportIndex:
+                                    if (definitions.Stage != ShaderStage.Fragment)
+                                    {
+                                        context.SetUsedFeature(FeatureFlags.ViewportIndex);
+                                    }
+                                    break;
                                 case AttributeConsts.ClipDistance0:
                                 case AttributeConsts.ClipDistance1:
                                 case AttributeConsts.ClipDistance2:
@@ -429,11 +455,17 @@ namespace Ryujinx.Graphics.Shader.Decoders
                                 case AttributeConsts.ClipDistance5:
                                 case AttributeConsts.ClipDistance6:
                                 case AttributeConsts.ClipDistance7:
-                                    if (definitions.Stage == ShaderStage.Vertex)
+                                    if (definitions.Stage.IsVtg())
                                     {
                                         context.SetClipDistanceWritten((attr - AttributeConsts.ClipDistance0) / 4);
                                     }
                                     break;
+                                case AttributeConsts.ViewportMask:
+                                    if (definitions.Stage != ShaderStage.Fragment)
+                                    {
+                                        context.SetUsedFeature(FeatureFlags.ViewportMask);
+                                    }
+                                    break;
                             }
                         }
                         else
diff --git a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs
index ee31f02d1..4dc75a3e1 100644
--- a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs
+++ b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs
@@ -128,7 +128,26 @@ namespace Ryujinx.Graphics.Shader
         /// <returns>GPU graphics state</returns>
         GpuGraphicsState QueryGraphicsState()
         {
-            return default;
+            return new GpuGraphicsState(
+                false,
+                InputTopology.Points,
+                false,
+                TessPatchType.Triangles,
+                TessSpacing.EqualSpacing,
+                false,
+                false,
+                false,
+                false,
+                false,
+                1f,
+                AlphaTestOp.Always,
+                0f,
+                default,
+                true,
+                default,
+                false,
+                false,
+                false);
         }
 
         /// <summary>
@@ -194,6 +213,15 @@ namespace Ryujinx.Graphics.Shader
             return 16;
         }
 
+        /// <summary>
+        /// Queries host shader subgroup size.
+        /// </summary>
+        /// <returns>Host shader subgroup size in invocations</returns>
+        int QueryHostSubgroupSize()
+        {
+            return 32;
+        }
+
         /// <summary>
         /// Queries host support for texture formats with BGRA component order (such as BGRA8).
         /// </summary>
diff --git a/src/Ryujinx.Graphics.Shader/InputTopology.cs b/src/Ryujinx.Graphics.Shader/InputTopology.cs
index ebd2930e4..9438263de 100644
--- a/src/Ryujinx.Graphics.Shader/InputTopology.cs
+++ b/src/Ryujinx.Graphics.Shader/InputTopology.cs
@@ -25,6 +25,19 @@ namespace Ryujinx.Graphics.Shader
         }
 
         public static int ToInputVertices(this InputTopology topology)
+        {
+            return topology switch
+            {
+                InputTopology.Points => 1,
+                InputTopology.Lines => 2,
+                InputTopology.LinesAdjacency => 4,
+                InputTopology.Triangles => 3,
+                InputTopology.TrianglesAdjacency => 6,
+                _ => 1,
+            };
+        }
+
+        public static int ToInputVerticesNoAdjacency(this InputTopology topology)
         {
             return topology switch
             {
diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs
index 53d774d69..63ce38e25 100644
--- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs
+++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs
@@ -63,7 +63,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
                     {
                         value = AttributeMap.GenerateAttributeLoad(context, primVertex, offset, isOutput, op.P);
 
-                        if (!context.TranslatorContext.Definitions.SupportsScaledVertexFormats &&
+                        if ((!context.TranslatorContext.Definitions.SupportsScaledVertexFormats || context.VertexAsCompute) &&
                             context.TranslatorContext.Stage == ShaderStage.Vertex &&
                             !op.O &&
                             offset >= 0x80 &&
diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitMove.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitMove.cs
index 9d1c7d087..944039d65 100644
--- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitMove.cs
+++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitMove.cs
@@ -76,7 +76,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
             switch (op.SReg)
             {
                 case SReg.LaneId:
-                    src = context.Load(StorageKind.Input, IoVariable.SubgroupLaneId);
+                    src = EmitLoadSubgroupLaneId(context);
                     break;
 
                 case SReg.InvocationId:
@@ -146,19 +146,19 @@ namespace Ryujinx.Graphics.Shader.Instructions
                     break;
 
                 case SReg.EqMask:
-                    src = context.Load(StorageKind.Input, IoVariable.SubgroupEqMask, null, Const(0));
+                    src = EmitLoadSubgroupMask(context, IoVariable.SubgroupEqMask);
                     break;
                 case SReg.LtMask:
-                    src = context.Load(StorageKind.Input, IoVariable.SubgroupLtMask, null, Const(0));
+                    src = EmitLoadSubgroupMask(context, IoVariable.SubgroupLtMask);
                     break;
                 case SReg.LeMask:
-                    src = context.Load(StorageKind.Input, IoVariable.SubgroupLeMask, null, Const(0));
+                    src = EmitLoadSubgroupMask(context, IoVariable.SubgroupLeMask);
                     break;
                 case SReg.GtMask:
-                    src = context.Load(StorageKind.Input, IoVariable.SubgroupGtMask, null, Const(0));
+                    src = EmitLoadSubgroupMask(context, IoVariable.SubgroupGtMask);
                     break;
                 case SReg.GeMask:
-                    src = context.Load(StorageKind.Input, IoVariable.SubgroupGeMask, null, Const(0));
+                    src = EmitLoadSubgroupMask(context, IoVariable.SubgroupGeMask);
                     break;
 
                 default:
@@ -169,6 +169,52 @@ namespace Ryujinx.Graphics.Shader.Instructions
             context.Copy(GetDest(op.Dest), src);
         }
 
+        private static Operand EmitLoadSubgroupLaneId(EmitterContext context)
+        {
+            if (context.TranslatorContext.GpuAccessor.QueryHostSubgroupSize() <= 32)
+            {
+                return context.Load(StorageKind.Input, IoVariable.SubgroupLaneId);
+            }
+
+            return context.BitwiseAnd(context.Load(StorageKind.Input, IoVariable.SubgroupLaneId), Const(0x1f));
+        }
+
+        private static Operand EmitLoadSubgroupMask(EmitterContext context, IoVariable ioVariable)
+        {
+            int subgroupSize = context.TranslatorContext.GpuAccessor.QueryHostSubgroupSize();
+
+            if (subgroupSize <= 32)
+            {
+                return context.Load(StorageKind.Input, ioVariable, null, Const(0));
+            }
+            else if (subgroupSize == 64)
+            {
+                Operand laneId = context.Load(StorageKind.Input, IoVariable.SubgroupLaneId);
+                Operand low = context.Load(StorageKind.Input, ioVariable, null, Const(0));
+                Operand high = context.Load(StorageKind.Input, ioVariable, null, Const(1));
+
+                return context.ConditionalSelect(context.BitwiseAnd(laneId, Const(32)), high, low);
+            }
+            else
+            {
+                Operand laneId = context.Load(StorageKind.Input, IoVariable.SubgroupLaneId);
+                Operand element = context.ShiftRightU32(laneId, Const(5));
+
+                Operand res = context.Load(StorageKind.Input, ioVariable, null, Const(0));
+                res = context.ConditionalSelect(
+                    context.ICompareEqual(element, Const(1)),
+                    context.Load(StorageKind.Input, ioVariable, null, Const(1)), res);
+                res = context.ConditionalSelect(
+                    context.ICompareEqual(element, Const(2)),
+                    context.Load(StorageKind.Input, ioVariable, null, Const(2)), res);
+                res = context.ConditionalSelect(
+                    context.ICompareEqual(element, Const(3)),
+                    context.Load(StorageKind.Input, ioVariable, null, Const(3)), res);
+
+                return res;
+            }
+        }
+
         public static void SelR(EmitterContext context)
         {
             InstSelR op = context.GetOp<InstSelR>();
diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs
index bbac8038a..5a231079a 100644
--- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs
+++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs
@@ -1094,7 +1094,14 @@ namespace Ryujinx.Graphics.Shader.Instructions
 
             if (isBindless)
             {
-                type = (componentMask & 4) != 0 ? SamplerType.Texture3D : SamplerType.Texture2D;
+                if (query == TexQuery.TexHeaderTextureType)
+                {
+                    type = SamplerType.Texture2D | SamplerType.Multisample;
+                }
+                else
+                {
+                    type = (componentMask & 4) != 0 ? SamplerType.Texture3D : SamplerType.Texture2D;
+                }
             }
             else
             {
@@ -1102,31 +1109,69 @@ namespace Ryujinx.Graphics.Shader.Instructions
             }
 
             TextureFlags flags = isBindless ? TextureFlags.Bindless : TextureFlags.None;
+            int binding;
 
-            int binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding(
-                Instruction.TextureSize,
-                type,
-                TextureFormat.Unknown,
-                flags,
-                TextureOperation.DefaultCbufSlot,
-                imm);
-
-            for (int compMask = componentMask, compIndex = 0; compMask != 0; compMask >>= 1, compIndex++)
+            switch (query)
             {
-                if ((compMask & 1) != 0)
-                {
-                    Operand d = GetDest();
+                case TexQuery.TexHeaderDimension:
+                    binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding(
+                        Instruction.TextureQuerySize,
+                        type,
+                        TextureFormat.Unknown,
+                        flags,
+                        TextureOperation.DefaultCbufSlot,
+                        imm);
 
-                    if (d == null)
+                    for (int compMask = componentMask, compIndex = 0; compMask != 0; compMask >>= 1, compIndex++)
                     {
-                        break;
+                        if ((compMask & 1) != 0)
+                        {
+                            Operand d = GetDest();
+
+                            if (d == null)
+                            {
+                                break;
+                            }
+
+                            context.Copy(d, context.TextureQuerySize(type, flags, binding, compIndex, sources));
+                        }
                     }
+                    break;
 
-                    // TODO: Validate and use query parameter.
-                    Operand res = context.TextureSize(type, flags, binding, compIndex, sources);
+                case TexQuery.TexHeaderTextureType:
+                    binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding(
+                        Instruction.TextureQuerySamples,
+                        type,
+                        TextureFormat.Unknown,
+                        flags,
+                        TextureOperation.DefaultCbufSlot,
+                        imm);
 
-                    context.Copy(d, res);
-                }
+                    if ((componentMask & 4) != 0)
+                    {
+                        // Skip first 2 components if necessary.
+                        if ((componentMask & 1) != 0)
+                        {
+                            GetDest();
+                        }
+
+                        if ((componentMask & 2) != 0)
+                        {
+                            GetDest();
+                        }
+
+                        Operand d = GetDest();
+
+                        if (d != null)
+                        {
+                            context.Copy(d, context.TextureQuerySamples(type, flags, binding, sources));
+                        }
+                    }
+                    break;
+
+                default:
+                    context.TranslatorContext.GpuAccessor.Log($"Invalid or unsupported query type \"{query}\".");
+                    break;
             }
         }
 
diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitWarp.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitWarp.cs
index a84944e43..73eea5c34 100644
--- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitWarp.cs
+++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitWarp.cs
@@ -50,20 +50,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
             InstVote op = context.GetOp<InstVote>();
 
             Operand pred = GetPredicate(context, op.SrcPred, op.SrcPredInv);
-            Operand res = null;
-
-            switch (op.VoteMode)
-            {
-                case VoteMode.All:
-                    res = context.VoteAll(pred);
-                    break;
-                case VoteMode.Any:
-                    res = context.VoteAny(pred);
-                    break;
-                case VoteMode.Eq:
-                    res = context.VoteAllEqual(pred);
-                    break;
-            }
+            Operand res = EmitVote(context, op.VoteMode, pred);
 
             if (res != null)
             {
@@ -76,7 +63,81 @@ namespace Ryujinx.Graphics.Shader.Instructions
 
             if (op.Dest != RegisterConsts.RegisterZeroIndex)
             {
-                context.Copy(GetDest(op.Dest), context.Ballot(pred));
+                context.Copy(GetDest(op.Dest), EmitBallot(context, pred));
+            }
+        }
+
+        private static Operand EmitVote(EmitterContext context, VoteMode voteMode, Operand pred)
+        {
+            int subgroupSize = context.TranslatorContext.GpuAccessor.QueryHostSubgroupSize();
+
+            if (subgroupSize <= 32)
+            {
+                return voteMode switch
+                {
+                    VoteMode.All => context.VoteAll(pred),
+                    VoteMode.Any => context.VoteAny(pred),
+                    VoteMode.Eq => context.VoteAllEqual(pred),
+                    _ => null,
+                };
+            }
+
+            // Emulate vote with ballot masks.
+            // We do that when the GPU thread count is not 32,
+            // since the shader code assumes it is 32.
+            // allInvocations => ballot(pred) == ballot(true),
+            // anyInvocation => ballot(pred) != 0,
+            // allInvocationsEqual => ballot(pred) == balot(true) || ballot(pred) == 0
+            Operand ballotMask = EmitBallot(context, pred);
+
+            Operand AllTrue() => context.ICompareEqual(ballotMask, EmitBallot(context, Const(IrConsts.True)));
+
+            return voteMode switch
+            {
+                VoteMode.All => AllTrue(),
+                VoteMode.Any => context.ICompareNotEqual(ballotMask, Const(0)),
+                VoteMode.Eq => context.BitwiseOr(AllTrue(), context.ICompareEqual(ballotMask, Const(0))),
+                _ => null,
+            };
+        }
+
+        private static Operand EmitBallot(EmitterContext context, Operand pred)
+        {
+            int subgroupSize = context.TranslatorContext.GpuAccessor.QueryHostSubgroupSize();
+
+            if (subgroupSize <= 32)
+            {
+                return context.Ballot(pred, 0);
+            }
+            else if (subgroupSize == 64)
+            {
+                // TODO: Add support for vector destination and do that with a single operation.
+
+                Operand laneId = context.Load(StorageKind.Input, IoVariable.SubgroupLaneId);
+                Operand low = context.Ballot(pred, 0);
+                Operand high = context.Ballot(pred, 1);
+
+                return context.ConditionalSelect(context.BitwiseAnd(laneId, Const(32)), high, low);
+            }
+            else
+            {
+                // TODO: Add support for vector destination and do that with a single operation.
+
+                Operand laneId = context.Load(StorageKind.Input, IoVariable.SubgroupLaneId);
+                Operand element = context.ShiftRightU32(laneId, Const(5));
+
+                Operand res = context.Ballot(pred, 0);
+                res = context.ConditionalSelect(
+                    context.ICompareEqual(element, Const(1)),
+                    context.Ballot(pred, 1), res);
+                res = context.ConditionalSelect(
+                    context.ICompareEqual(element, Const(2)),
+                    context.Ballot(pred, 2), res);
+                res = context.ConditionalSelect(
+                    context.ICompareEqual(element, Const(3)),
+                    context.Ballot(pred, 3), res);
+
+                return res;
             }
         }
     }
diff --git a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs
index 808cc7ed6..e5695ebc2 100644
--- a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs
+++ b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs
@@ -1,10 +1,8 @@
 using System;
-using System.Diagnostics.CodeAnalysis;
 
 namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
 {
     [Flags]
-    [SuppressMessage("Design", "CA1069: Enums values should not be duplicated")]
     enum Instruction
     {
         Absolute = 1,
@@ -118,7 +116,8 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
         Subtract,
         SwizzleAdd,
         TextureSample,
-        TextureSize,
+        TextureQuerySamples,
+        TextureQuerySize,
         Truncate,
         UnpackDouble2x32,
         UnpackHalf2x16,
@@ -160,7 +159,7 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
         public static bool IsTextureQuery(this Instruction inst)
         {
             inst &= Instruction.Mask;
-            return inst == Instruction.Lod || inst == Instruction.TextureSize;
+            return inst == Instruction.Lod || inst == Instruction.TextureQuerySamples || inst == Instruction.TextureQuerySize;
         }
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/IoVariable.cs b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/IoVariable.cs
index fdee83451..21e208636 100644
--- a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/IoVariable.cs
+++ b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/IoVariable.cs
@@ -18,6 +18,7 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
         FrontColorDiffuse,
         FrontColorSpecular,
         FrontFacing,
+        GlobalId,
         InstanceId,
         InstanceIndex,
         InvocationId,
diff --git a/src/Ryujinx.Graphics.Shader/ResourceReservationCounts.cs b/src/Ryujinx.Graphics.Shader/ResourceReservationCounts.cs
new file mode 100644
index 000000000..c0bae8eab
--- /dev/null
+++ b/src/Ryujinx.Graphics.Shader/ResourceReservationCounts.cs
@@ -0,0 +1,22 @@
+using Ryujinx.Graphics.Shader.Translation;
+
+namespace Ryujinx.Graphics.Shader
+{
+    public readonly struct ResourceReservationCounts
+    {
+        public readonly int ReservedConstantBuffers { get; }
+        public readonly int ReservedStorageBuffers { get; }
+        public readonly int ReservedTextures { get; }
+        public readonly int ReservedImages { get; }
+
+        public ResourceReservationCounts(bool isTransformFeedbackEmulated, bool vertexAsCompute)
+        {
+            ResourceReservations reservations = new(isTransformFeedbackEmulated, vertexAsCompute);
+
+            ReservedConstantBuffers = reservations.ReservedConstantBuffers;
+            ReservedStorageBuffers = reservations.ReservedStorageBuffers;
+            ReservedTextures = reservations.ReservedTextures;
+            ReservedImages = reservations.ReservedImages;
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj b/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj
index b1f1fb963..ea9a7821b 100644
--- a/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj
+++ b/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj
@@ -12,10 +12,6 @@
   <ItemGroup>
     <EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\MultiplyHighS32.glsl" />
     <EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\MultiplyHighU32.glsl" />
-    <EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\Shuffle.glsl" />
-    <EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\ShuffleDown.glsl" />
-    <EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\ShuffleUp.glsl" />
-    <EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\ShuffleXor.glsl" />
     <EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\SwizzleAdd.glsl" />
   </ItemGroup>
 
diff --git a/src/Ryujinx.Graphics.Shader/ShaderIdentification.cs b/src/Ryujinx.Graphics.Shader/ShaderIdentification.cs
deleted file mode 100644
index 551e318c0..000000000
--- a/src/Ryujinx.Graphics.Shader/ShaderIdentification.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.Graphics.Shader
-{
-    public enum ShaderIdentification
-    {
-        None,
-        GeometryLayerPassthrough,
-    }
-}
diff --git a/src/Ryujinx.Graphics.Shader/ShaderProgramInfo.cs b/src/Ryujinx.Graphics.Shader/ShaderProgramInfo.cs
index f9776afc0..22823ac38 100644
--- a/src/Ryujinx.Graphics.Shader/ShaderProgramInfo.cs
+++ b/src/Ryujinx.Graphics.Shader/ShaderProgramInfo.cs
@@ -10,9 +10,10 @@ namespace Ryujinx.Graphics.Shader
         public ReadOnlyCollection<TextureDescriptor> Textures { get; }
         public ReadOnlyCollection<TextureDescriptor> Images { get; }
 
-        public ShaderIdentification Identification { get; }
-        public int GpLayerInputAttribute { get; }
         public ShaderStage Stage { get; }
+        public int GeometryVerticesPerPrimitive { get; }
+        public int GeometryMaxOutputVertices { get; }
+        public int ThreadsPerInputPrimitive { get; }
         public bool UsesFragCoord { get; }
         public bool UsesInstanceId { get; }
         public bool UsesDrawParameters { get; }
@@ -25,9 +26,10 @@ namespace Ryujinx.Graphics.Shader
             BufferDescriptor[] sBuffers,
             TextureDescriptor[] textures,
             TextureDescriptor[] images,
-            ShaderIdentification identification,
-            int gpLayerInputAttribute,
             ShaderStage stage,
+            int geometryVerticesPerPrimitive,
+            int geometryMaxOutputVertices,
+            int threadsPerInputPrimitive,
             bool usesFragCoord,
             bool usesInstanceId,
             bool usesDrawParameters,
@@ -40,9 +42,10 @@ namespace Ryujinx.Graphics.Shader
             Textures = Array.AsReadOnly(textures);
             Images = Array.AsReadOnly(images);
 
-            Identification = identification;
-            GpLayerInputAttribute = gpLayerInputAttribute;
             Stage = stage;
+            GeometryVerticesPerPrimitive = geometryVerticesPerPrimitive;
+            GeometryMaxOutputVertices = geometryMaxOutputVertices;
+            ThreadsPerInputPrimitive = threadsPerInputPrimitive;
             UsesFragCoord = usesFragCoord;
             UsesInstanceId = usesInstanceId;
             UsesDrawParameters = usesDrawParameters;
diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/HelperFunctionsMask.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/HelperFunctionsMask.cs
index 73ce90827..2a3d65e75 100644
--- a/src/Ryujinx.Graphics.Shader/StructuredIr/HelperFunctionsMask.cs
+++ b/src/Ryujinx.Graphics.Shader/StructuredIr/HelperFunctionsMask.cs
@@ -7,10 +7,6 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
     {
         MultiplyHighS32 = 1 << 2,
         MultiplyHighU32 = 1 << 3,
-        Shuffle = 1 << 4,
-        ShuffleDown = 1 << 5,
-        ShuffleUp = 1 << 6,
-        ShuffleXor = 1 << 7,
         SwizzleAdd = 1 << 10,
         FSI = 1 << 11,
     }
diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/InstructionInfo.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/InstructionInfo.cs
index 6cd0fd086..72d0e9896 100644
--- a/src/Ryujinx.Graphics.Shader/StructuredIr/InstructionInfo.cs
+++ b/src/Ryujinx.Graphics.Shader/StructuredIr/InstructionInfo.cs
@@ -109,21 +109,23 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
             Add(Instruction.PackDouble2x32,           AggregateType.FP64,   AggregateType.U32,     AggregateType.U32);
             Add(Instruction.PackHalf2x16,             AggregateType.U32,    AggregateType.FP32,    AggregateType.FP32);
             Add(Instruction.ReciprocalSquareRoot,     AggregateType.Scalar, AggregateType.Scalar);
+            Add(Instruction.Return,                   AggregateType.Void,   AggregateType.U32);
             Add(Instruction.Round,                    AggregateType.Scalar, AggregateType.Scalar);
             Add(Instruction.ShiftLeft,                AggregateType.S32,    AggregateType.S32,     AggregateType.S32);
             Add(Instruction.ShiftRightS32,            AggregateType.S32,    AggregateType.S32,     AggregateType.S32);
             Add(Instruction.ShiftRightU32,            AggregateType.U32,    AggregateType.U32,     AggregateType.S32);
-            Add(Instruction.Shuffle,                  AggregateType.FP32,   AggregateType.FP32,    AggregateType.U32,     AggregateType.U32,     AggregateType.Bool);
-            Add(Instruction.ShuffleDown,              AggregateType.FP32,   AggregateType.FP32,    AggregateType.U32,     AggregateType.U32,     AggregateType.Bool);
-            Add(Instruction.ShuffleUp,                AggregateType.FP32,   AggregateType.FP32,    AggregateType.U32,     AggregateType.U32,     AggregateType.Bool);
-            Add(Instruction.ShuffleXor,               AggregateType.FP32,   AggregateType.FP32,    AggregateType.U32,     AggregateType.U32,     AggregateType.Bool);
+            Add(Instruction.Shuffle,                  AggregateType.FP32,   AggregateType.FP32,    AggregateType.U32);
+            Add(Instruction.ShuffleDown,              AggregateType.FP32,   AggregateType.FP32,    AggregateType.U32);
+            Add(Instruction.ShuffleUp,                AggregateType.FP32,   AggregateType.FP32,    AggregateType.U32);
+            Add(Instruction.ShuffleXor,               AggregateType.FP32,   AggregateType.FP32,    AggregateType.U32);
             Add(Instruction.Sine,                     AggregateType.Scalar, AggregateType.Scalar);
             Add(Instruction.SquareRoot,               AggregateType.Scalar, AggregateType.Scalar);
             Add(Instruction.Store,                    AggregateType.Void);
             Add(Instruction.Subtract,                 AggregateType.Scalar, AggregateType.Scalar,  AggregateType.Scalar);
             Add(Instruction.SwizzleAdd,               AggregateType.FP32,   AggregateType.FP32,    AggregateType.FP32,    AggregateType.S32);
             Add(Instruction.TextureSample,            AggregateType.FP32);
-            Add(Instruction.TextureSize,              AggregateType.S32,    AggregateType.S32,     AggregateType.S32);
+            Add(Instruction.TextureQuerySamples,      AggregateType.S32,    AggregateType.S32);
+            Add(Instruction.TextureQuerySize,         AggregateType.S32,    AggregateType.S32,     AggregateType.S32);
             Add(Instruction.Truncate,                 AggregateType.Scalar, AggregateType.Scalar);
             Add(Instruction.UnpackDouble2x32,         AggregateType.U32,    AggregateType.FP64);
             Add(Instruction.UnpackHalf2x16,           AggregateType.FP32,   AggregateType.U32);
@@ -131,7 +133,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
             Add(Instruction.VoteAll,                  AggregateType.Bool,   AggregateType.Bool);
             Add(Instruction.VoteAllEqual,             AggregateType.Bool,   AggregateType.Bool);
             Add(Instruction.VoteAny,                  AggregateType.Bool,   AggregateType.Bool);
-#pragma warning restore IDE0055v
+#pragma warning restore IDE0055
         }
 
         private static void Add(Instruction inst, AggregateType destType, params AggregateType[] srcTypes)
diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs
index 862fef126..2e2df7546 100644
--- a/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs
+++ b/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs
@@ -2,17 +2,22 @@ using Ryujinx.Graphics.Shader.IntermediateRepresentation;
 using Ryujinx.Graphics.Shader.Translation;
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Numerics;
 
 namespace Ryujinx.Graphics.Shader.StructuredIr
 {
     static class StructuredProgram
     {
+        // TODO: Eventually it should be possible to specify the parameter types for the function instead of using S32 for everything.
+        private const AggregateType FuncParameterType = AggregateType.S32;
+
         public static StructuredProgramInfo MakeStructuredProgram(
             IReadOnlyList<Function> functions,
             AttributeUsage attributeUsage,
             ShaderDefinitions definitions,
             ResourceManager resourceManager,
+            TargetLanguage targetLanguage,
             bool debugMode)
         {
             StructuredProgramContext context = new(attributeUsage, definitions, resourceManager, debugMode);
@@ -23,19 +28,19 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
 
                 BasicBlock[] blocks = function.Blocks;
 
-                AggregateType returnType = function.ReturnsValue ? AggregateType.S32 : AggregateType.Void;
+                AggregateType returnType = function.ReturnsValue ? FuncParameterType : AggregateType.Void;
 
                 AggregateType[] inArguments = new AggregateType[function.InArgumentsCount];
                 AggregateType[] outArguments = new AggregateType[function.OutArgumentsCount];
 
                 for (int i = 0; i < inArguments.Length; i++)
                 {
-                    inArguments[i] = AggregateType.S32;
+                    inArguments[i] = FuncParameterType;
                 }
 
                 for (int i = 0; i < outArguments.Length; i++)
                 {
-                    outArguments[i] = AggregateType.S32;
+                    outArguments[i] = FuncParameterType;
                 }
 
                 context.EnterFunction(blocks.Length, function.Name, returnType, inArguments, outArguments);
@@ -58,7 +63,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
                         }
                         else
                         {
-                            AddOperation(context, operation);
+                            AddOperation(context, operation, targetLanguage, functions);
                         }
                     }
                 }
@@ -73,7 +78,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
             return context.Info;
         }
 
-        private static void AddOperation(StructuredProgramContext context, Operation operation)
+        private static void AddOperation(StructuredProgramContext context, Operation operation, TargetLanguage targetLanguage, IReadOnlyList<Function> functions)
         {
             Instruction inst = operation.Inst;
             StorageKind storageKind = operation.StorageKind;
@@ -114,9 +119,43 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
 
             IAstNode[] sources = new IAstNode[sourcesCount + outDestsCount];
 
-            for (int index = 0; index < operation.SourcesCount; index++)
+            if (inst == Instruction.Call && targetLanguage == TargetLanguage.Spirv)
             {
-                sources[index] = context.GetOperandOrCbLoad(operation.GetSource(index));
+                // SPIR-V requires that all function parameters are copied to a local variable before the call
+                // (or at least that's what the Khronos compiler does).
+
+                // First one is the function index.
+                Operand funcIndexOperand = operation.GetSource(0);
+                Debug.Assert(funcIndexOperand.Type == OperandType.Constant);
+                int funcIndex = funcIndexOperand.Value;
+
+                sources[0] = new AstOperand(OperandType.Constant, funcIndex);
+
+                int inArgsCount = functions[funcIndex].InArgumentsCount;
+
+                // Remaining ones are parameters, copy them to a temp local variable.
+                for (int index = 1; index < operation.SourcesCount; index++)
+                {
+                    IAstNode source = context.GetOperandOrCbLoad(operation.GetSource(index));
+
+                    if (index - 1 < inArgsCount)
+                    {
+                        AstOperand argTemp = context.NewTemp(FuncParameterType);
+                        context.AddNode(new AstAssignment(argTemp, source));
+                        sources[index] = argTemp;
+                    }
+                    else
+                    {
+                        sources[index] = source;
+                    }
+                }
+            }
+            else
+            {
+                for (int index = 0; index < operation.SourcesCount; index++)
+                {
+                    sources[index] = context.GetOperandOrCbLoad(operation.GetSource(index));
+                }
             }
 
             for (int index = 0; index < outDestsCount; index++)
@@ -282,18 +321,6 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
                 case Instruction.MultiplyHighU32:
                     context.Info.HelperFunctionsMask |= HelperFunctionsMask.MultiplyHighU32;
                     break;
-                case Instruction.Shuffle:
-                    context.Info.HelperFunctionsMask |= HelperFunctionsMask.Shuffle;
-                    break;
-                case Instruction.ShuffleDown:
-                    context.Info.HelperFunctionsMask |= HelperFunctionsMask.ShuffleDown;
-                    break;
-                case Instruction.ShuffleUp:
-                    context.Info.HelperFunctionsMask |= HelperFunctionsMask.ShuffleUp;
-                    break;
-                case Instruction.ShuffleXor:
-                    context.Info.HelperFunctionsMask |= HelperFunctionsMask.ShuffleXor;
-                    break;
                 case Instruction.SwizzleAdd:
                     context.Info.HelperFunctionsMask |= HelperFunctionsMask.SwizzleAdd;
                     break;
diff --git a/src/Ryujinx.Graphics.Shader/SupportBuffer.cs b/src/Ryujinx.Graphics.Shader/SupportBuffer.cs
index 0b7a2edd6..d4d3cbf8f 100644
--- a/src/Ryujinx.Graphics.Shader/SupportBuffer.cs
+++ b/src/Ryujinx.Graphics.Shader/SupportBuffer.cs
@@ -22,11 +22,13 @@ namespace Ryujinx.Graphics.Shader
         ViewportSize,
         FragmentRenderScaleCount,
         RenderScale,
+        TfeOffset,
+        TfeVertexCount,
     }
 
     public struct SupportBuffer
     {
-        internal const int Binding = 0;
+        public const int Binding = 0;
 
         public static readonly int FieldSize;
         public static readonly int RequiredSize;
@@ -38,6 +40,8 @@ namespace Ryujinx.Graphics.Shader
         public static readonly int FragmentRenderScaleCountOffset;
         public static readonly int GraphicsRenderScaleOffset;
         public static readonly int ComputeRenderScaleOffset;
+        public static readonly int TfeOffsetOffset;
+        public static readonly int TfeVertexCountOffset;
 
         public const int FragmentIsBgraCount = 8;
         // One for the render target, 64 for the textures, and 8 for the images.
@@ -62,18 +66,22 @@ namespace Ryujinx.Graphics.Shader
             FragmentRenderScaleCountOffset = OffsetOf(ref instance, ref instance.FragmentRenderScaleCount);
             GraphicsRenderScaleOffset = OffsetOf(ref instance, ref instance.RenderScale);
             ComputeRenderScaleOffset = GraphicsRenderScaleOffset + FieldSize;
+            TfeOffsetOffset = OffsetOf(ref instance, ref instance.TfeOffset);
+            TfeVertexCountOffset = OffsetOf(ref instance, ref instance.TfeVertexCount);
         }
 
         internal static StructureType GetStructureType()
         {
             return new StructureType(new[]
             {
-                new StructureField(AggregateType.U32, "s_alpha_test"),
-                new StructureField(AggregateType.Array | AggregateType.U32, "s_is_bgra", FragmentIsBgraCount),
-                new StructureField(AggregateType.Vector4 | AggregateType.FP32, "s_viewport_inverse"),
-                new StructureField(AggregateType.Vector4 | AggregateType.FP32, "s_viewport_size"),
-                new StructureField(AggregateType.S32, "s_frag_scale_count"),
-                new StructureField(AggregateType.Array | AggregateType.FP32, "s_render_scale", RenderScaleMaxCount),
+                new StructureField(AggregateType.U32, "alpha_test"),
+                new StructureField(AggregateType.Array | AggregateType.U32, "is_bgra", FragmentIsBgraCount),
+                new StructureField(AggregateType.Vector4 | AggregateType.FP32, "viewport_inverse"),
+                new StructureField(AggregateType.Vector4 | AggregateType.FP32, "viewport_size"),
+                new StructureField(AggregateType.S32, "frag_scale_count"),
+                new StructureField(AggregateType.Array | AggregateType.FP32, "render_scale", RenderScaleMaxCount),
+                new StructureField(AggregateType.Vector4 | AggregateType.S32, "tfe_offset"),
+                new StructureField(AggregateType.S32, "tfe_vertex_count"),
             });
         }
 
@@ -85,5 +93,8 @@ namespace Ryujinx.Graphics.Shader
 
         // Render scale max count: 1 + 64 + 8. First scale is fragment output scale, others are textures/image inputs.
         public Array73<Vector4<float>> RenderScale;
+
+        public Vector4<int> TfeOffset;
+        public Vector4<int> TfeVertexCount;
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/Translation/AttributeConsts.cs b/src/Ryujinx.Graphics.Shader/Translation/AttributeConsts.cs
index f749cecb8..c4bd2cbfa 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/AttributeConsts.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/AttributeConsts.cs
@@ -4,6 +4,7 @@ namespace Ryujinx.Graphics.Shader.Translation
     {
         public const int PrimitiveId = 0x060;
         public const int Layer = 0x064;
+        public const int ViewportIndex = 0x068;
         public const int PositionX = 0x070;
         public const int PositionY = 0x074;
         public const int FrontColorDiffuseR = 0x280;
@@ -24,6 +25,7 @@ namespace Ryujinx.Graphics.Shader.Translation
         public const int TexCoordCount = 10;
         public const int TexCoordBase = 0x300;
         public const int TexCoordEnd = TexCoordBase + TexCoordCount * 16;
+        public const int ViewportMask = 0x3a0;
         public const int FrontFacing = 0x3fc;
 
         public const int UserAttributesCount = 32;
diff --git a/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs b/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
index 43263dd4b..f1dffb351 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
@@ -14,6 +14,8 @@ namespace Ryujinx.Graphics.Shader.Translation
         public TranslatorContext TranslatorContext { get; }
         public ResourceManager ResourceManager { get; }
 
+        public bool VertexAsCompute { get; }
+
         public bool IsNonMain { get; }
 
         public Block CurrBlock { get; set; }
@@ -59,11 +61,13 @@ namespace Ryujinx.Graphics.Shader.Translation
             TranslatorContext translatorContext,
             ResourceManager resourceManager,
             DecodedProgram program,
+            bool vertexAsCompute,
             bool isNonMain) : this()
         {
             TranslatorContext = translatorContext;
             ResourceManager = resourceManager;
             Program = program;
+            VertexAsCompute = vertexAsCompute;
             IsNonMain = isNonMain;
 
             EmitStart();
@@ -71,13 +75,87 @@ namespace Ryujinx.Graphics.Shader.Translation
 
         private void EmitStart()
         {
-            if (TranslatorContext.Definitions.Stage == ShaderStage.Vertex &&
-                TranslatorContext.Options.TargetApi == TargetApi.Vulkan &&
-                (TranslatorContext.Options.Flags & TranslationFlags.VertexA) == 0)
+            if (TranslatorContext.Options.Flags.HasFlag(TranslationFlags.VertexA))
+            {
+                return;
+            }
+
+            if (TranslatorContext.Definitions.Stage == ShaderStage.Vertex && TranslatorContext.Options.TargetApi == TargetApi.Vulkan)
             {
                 // Vulkan requires the point size to be always written on the shader if the primitive topology is points.
                 this.Store(StorageKind.Output, IoVariable.PointSize, null, ConstF(TranslatorContext.Definitions.PointSize));
             }
+
+            if (VertexAsCompute)
+            {
+                int vertexInfoCbBinding = ResourceManager.Reservations.VertexInfoConstantBufferBinding;
+                int countFieldIndex = TranslatorContext.Stage == ShaderStage.Vertex
+                    ? (int)VertexInfoBufferField.VertexCounts
+                    : (int)VertexInfoBufferField.GeometryCounts;
+
+                Operand outputVertexOffset = this.Load(StorageKind.Input, IoVariable.GlobalId, Const(0));
+                Operand vertexCount = this.Load(StorageKind.ConstantBuffer, vertexInfoCbBinding, Const(countFieldIndex), Const(0));
+                Operand isVertexOob = this.ICompareGreaterOrEqualUnsigned(outputVertexOffset, vertexCount);
+
+                Operand lblVertexInBounds = Label();
+
+                this.BranchIfFalse(lblVertexInBounds, isVertexOob);
+                this.Return();
+                this.MarkLabel(lblVertexInBounds);
+
+                Operand outputInstanceOffset = this.Load(StorageKind.Input, IoVariable.GlobalId, Const(1));
+                Operand instanceCount = this.Load(StorageKind.ConstantBuffer, vertexInfoCbBinding, Const((int)VertexInfoBufferField.VertexCounts), Const(1));
+                Operand firstVertex = this.Load(StorageKind.ConstantBuffer, vertexInfoCbBinding, Const((int)VertexInfoBufferField.VertexCounts), Const(2));
+                Operand firstInstance = this.Load(StorageKind.ConstantBuffer, vertexInfoCbBinding, Const((int)VertexInfoBufferField.VertexCounts), Const(3));
+                Operand ibBaseOffset = this.Load(StorageKind.ConstantBuffer, vertexInfoCbBinding, Const((int)VertexInfoBufferField.GeometryCounts), Const(3));
+                Operand isInstanceOob = this.ICompareGreaterOrEqualUnsigned(outputInstanceOffset, instanceCount);
+
+                Operand lblInstanceInBounds = Label();
+
+                this.BranchIfFalse(lblInstanceInBounds, isInstanceOob);
+                this.Return();
+                this.MarkLabel(lblInstanceInBounds);
+
+                if (TranslatorContext.Stage == ShaderStage.Vertex)
+                {
+                    Operand vertexIndexVr = Local();
+
+                    this.TextureSample(
+                        SamplerType.TextureBuffer,
+                        TextureFlags.IntCoords,
+                        ResourceManager.Reservations.IndexBufferTextureBinding,
+                        1,
+                        new[] { vertexIndexVr },
+                        new[] { this.IAdd(ibBaseOffset, outputVertexOffset) });
+
+                    this.Store(StorageKind.LocalMemory, ResourceManager.LocalVertexIndexVertexRateMemoryId, this.IAdd(firstVertex, vertexIndexVr));
+                    this.Store(StorageKind.LocalMemory, ResourceManager.LocalVertexIndexInstanceRateMemoryId, this.IAdd(firstInstance, outputInstanceOffset));
+                }
+                else if (TranslatorContext.Stage == ShaderStage.Geometry)
+                {
+                    int inputVertices = TranslatorContext.Definitions.InputTopology.ToInputVertices();
+
+                    Operand baseVertex = this.IMultiply(outputVertexOffset, Const(inputVertices));
+
+                    for (int index = 0; index < inputVertices; index++)
+                    {
+                        Operand vertexIndex = Local();
+
+                        this.TextureSample(
+                            SamplerType.TextureBuffer,
+                            TextureFlags.IntCoords,
+                            ResourceManager.Reservations.TopologyRemapBufferTextureBinding,
+                            1,
+                            new[] { vertexIndex },
+                            new[] { this.IAdd(baseVertex, Const(index)) });
+
+                        this.Store(StorageKind.LocalMemory, ResourceManager.LocalTopologyRemapMemoryId, Const(index), vertexIndex);
+                    }
+
+                    this.Store(StorageKind.LocalMemory, ResourceManager.LocalGeometryOutputVertexCountMemoryId, Const(0));
+                    this.Store(StorageKind.LocalMemory, ResourceManager.LocalGeometryOutputIndexCountMemoryId, Const(0));
+                }
+            }
         }
 
         public T GetOp<T>() where T : unmanaged
@@ -166,16 +244,21 @@ namespace Ryujinx.Graphics.Shader.Translation
 
         public void PrepareForVertexReturn()
         {
-            if (!TranslatorContext.GpuAccessor.QueryHostSupportsTransformFeedback() && TranslatorContext.GpuAccessor.QueryTransformFeedbackEnabled())
-            {
-                Operand vertexCount = this.Load(StorageKind.StorageBuffer, Constants.TfeInfoBinding, Const(1));
+            // TODO: Support transform feedback emulation on stages other than vertex.
+            // Those stages might produce more primitives, so it needs a way to "compact" the output after it is written.
 
-                for (int tfbIndex = 0; tfbIndex < Constants.TfeBuffersCount; tfbIndex++)
+            if (!TranslatorContext.GpuAccessor.QueryHostSupportsTransformFeedback() &&
+                TranslatorContext.GpuAccessor.QueryTransformFeedbackEnabled() &&
+                TranslatorContext.Stage == ShaderStage.Vertex)
+            {
+                Operand vertexCount = this.Load(StorageKind.ConstantBuffer, SupportBuffer.Binding, Const((int)SupportBufferField.TfeVertexCount));
+
+                for (int tfbIndex = 0; tfbIndex < ResourceReservations.TfeBuffersCount; tfbIndex++)
                 {
                     var locations = TranslatorContext.GpuAccessor.QueryTransformFeedbackVaryingLocations(tfbIndex);
                     var stride = TranslatorContext.GpuAccessor.QueryTransformFeedbackStride(tfbIndex);
 
-                    Operand baseOffset = this.Load(StorageKind.StorageBuffer, Constants.TfeInfoBinding, Const(0), Const(tfbIndex));
+                    Operand baseOffset = this.Load(StorageKind.ConstantBuffer, SupportBuffer.Binding, Const((int)SupportBufferField.TfeOffset), Const(tfbIndex));
                     Operand baseVertex = this.Load(StorageKind.Input, IoVariable.BaseVertex);
                     Operand baseInstance = this.Load(StorageKind.Input, IoVariable.BaseInstance);
                     Operand vertexIndex = this.Load(StorageKind.Input, IoVariable.VertexIndex);
@@ -200,7 +283,9 @@ namespace Ryujinx.Graphics.Shader.Translation
                         Operand offset = this.IAdd(baseOffset, Const(j));
                         Operand value = Instructions.AttributeMap.GenerateAttributeLoad(this, null, location * 4, isOutput: true, isPerPatch: false);
 
-                        this.Store(StorageKind.StorageBuffer, Constants.TfeBufferBaseBinding + tfbIndex, Const(0), offset, value);
+                        int binding = ResourceManager.Reservations.GetTfeBufferStorageBufferBinding(tfbIndex);
+
+                        this.Store(StorageKind.StorageBuffer, binding, Const(0), offset, value);
                     }
                 }
             }
@@ -225,16 +310,6 @@ namespace Ryujinx.Graphics.Shader.Translation
 
                 this.Store(StorageKind.Output, IoVariable.Position, null, Const(2), this.FPFusedMultiplyAdd(z, ConstF(0.5f), halfW));
             }
-
-            if (TranslatorContext.Definitions.Stage != ShaderStage.Geometry && TranslatorContext.HasLayerInputAttribute)
-            {
-                int attrVecIndex = TranslatorContext.GpLayerInputAttribute >> 2;
-                int attrComponentIndex = TranslatorContext.GpLayerInputAttribute & 3;
-
-                Operand layer = this.Load(StorageKind.Output, IoVariable.UserDefined, null, Const(attrVecIndex), Const(attrComponentIndex));
-
-                this.Store(StorageKind.Output, IoVariable.Layer, null, layer);
-            }
         }
 
         public void PrepareForVertexReturn(out Operand oldXLocal, out Operand oldYLocal, out Operand oldZLocal)
@@ -308,9 +383,30 @@ namespace Ryujinx.Graphics.Shader.Translation
 
                 if (TranslatorContext.Definitions.GpPassthrough && !TranslatorContext.GpuAccessor.QueryHostSupportsGeometryShaderPassthrough())
                 {
-                    int inputVertices = TranslatorContext.Definitions.InputTopology.ToInputVertices();
+                    int inputStart, inputEnd, inputStep;
 
-                    for (int primIndex = 0; primIndex < inputVertices; primIndex++)
+                    InputTopology topology = TranslatorContext.Definitions.InputTopology;
+
+                    if (topology == InputTopology.LinesAdjacency)
+                    {
+                        inputStart = 1;
+                        inputEnd = 3;
+                        inputStep = 1;
+                    }
+                    else if (topology == InputTopology.TrianglesAdjacency)
+                    {
+                        inputStart = 0;
+                        inputEnd = 6;
+                        inputStep = 2;
+                    }
+                    else
+                    {
+                        inputStart = 0;
+                        inputEnd = topology.ToInputVerticesNoAdjacency();
+                        inputStep = 1;
+                    }
+
+                    for (int primIndex = inputStart; primIndex < inputEnd; primIndex += inputStep)
                     {
                         WritePositionOutput(primIndex);
 
@@ -428,6 +524,65 @@ namespace Ryujinx.Graphics.Shader.Translation
                 }
             }
 
+            if (VertexAsCompute)
+            {
+                if (TranslatorContext.Stage == ShaderStage.Vertex)
+                {
+                    int vertexInfoCbBinding = ResourceManager.Reservations.VertexInfoConstantBufferBinding;
+                    int vertexOutputSbBinding = ResourceManager.Reservations.VertexOutputStorageBufferBinding;
+                    int stride = ResourceManager.Reservations.OutputSizePerInvocation;
+
+                    Operand vertexCount = this.Load(StorageKind.ConstantBuffer, vertexInfoCbBinding, Const((int)VertexInfoBufferField.VertexCounts), Const(0));
+
+                    Operand outputVertexOffset = this.Load(StorageKind.Input, IoVariable.GlobalId, Const(0));
+                    Operand outputInstanceOffset = this.Load(StorageKind.Input, IoVariable.GlobalId, Const(1));
+
+                    Operand outputBaseVertex = this.IMultiply(outputInstanceOffset, vertexCount);
+
+                    Operand baseOffset = this.IMultiply(this.IAdd(outputBaseVertex, outputVertexOffset), Const(stride));
+
+                    for (int offset = 0; offset < stride; offset++)
+                    {
+                        Operand vertexOffset = this.IAdd(baseOffset, Const(offset));
+                        Operand value = this.Load(StorageKind.LocalMemory, ResourceManager.LocalVertexDataMemoryId, Const(offset));
+
+                        this.Store(StorageKind.StorageBuffer, vertexOutputSbBinding, Const(0), vertexOffset, value);
+                    }
+                }
+                else if (TranslatorContext.Stage == ShaderStage.Geometry)
+                {
+                    Operand lblLoopHead = Label();
+                    Operand lblExit = Label();
+
+                    this.MarkLabel(lblLoopHead);
+
+                    Operand writtenIndices = this.Load(StorageKind.LocalMemory, ResourceManager.LocalGeometryOutputIndexCountMemoryId);
+
+                    int maxIndicesPerPrimitiveInvocation = TranslatorContext.Definitions.GetGeometryOutputIndexBufferStridePerInstance();
+                    int maxIndicesPerPrimitive = maxIndicesPerPrimitiveInvocation * TranslatorContext.Definitions.ThreadsPerInputPrimitive;
+
+                    this.BranchIfTrue(lblExit, this.ICompareGreaterOrEqualUnsigned(writtenIndices, Const(maxIndicesPerPrimitiveInvocation)));
+
+                    int vertexInfoCbBinding = ResourceManager.Reservations.VertexInfoConstantBufferBinding;
+
+                    Operand primitiveIndex = this.Load(StorageKind.Input, IoVariable.GlobalId, Const(0));
+                    Operand instanceIndex = this.Load(StorageKind.Input, IoVariable.GlobalId, Const(1));
+                    Operand invocationId = this.Load(StorageKind.Input, IoVariable.GlobalId, Const(2));
+                    Operand vertexCount = this.Load(StorageKind.ConstantBuffer, vertexInfoCbBinding, Const((int)VertexInfoBufferField.VertexCounts), Const(0));
+                    Operand primitiveId = this.IAdd(this.IMultiply(instanceIndex, vertexCount), primitiveIndex);
+                    Operand ibOffset = this.IMultiply(primitiveId, Const(maxIndicesPerPrimitive));
+                    ibOffset = this.IAdd(ibOffset, this.IMultiply(invocationId, Const(maxIndicesPerPrimitiveInvocation)));
+                    ibOffset = this.IAdd(ibOffset, writtenIndices);
+
+                    this.Store(StorageKind.StorageBuffer, ResourceManager.Reservations.GeometryIndexOutputStorageBufferBinding, Const(0), ibOffset, Const(-1));
+                    this.Store(StorageKind.LocalMemory, ResourceManager.LocalGeometryOutputIndexCountMemoryId, this.IAdd(writtenIndices, Const(1)));
+
+                    this.Branch(lblLoopHead);
+
+                    this.MarkLabel(lblExit);
+                }
+            }
+
             return true;
         }
 
diff --git a/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs b/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs
index 6cb572381..9e314c620 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs
@@ -112,9 +112,13 @@ namespace Ryujinx.Graphics.Shader.Translation
             return context.Add(Instruction.AtomicXor, storageKind, Local(), Const(binding), e0, e1, value);
         }
 
-        public static Operand Ballot(this EmitterContext context, Operand a)
+        public static Operand Ballot(this EmitterContext context, Operand a, int index)
         {
-            return context.Add(Instruction.Ballot, Local(), a);
+            Operand dest = Local();
+
+            context.Add(new Operation(Instruction.Ballot, index, dest, a));
+
+            return dest;
         }
 
         public static Operand Barrier(this EmitterContext context)
@@ -782,21 +786,41 @@ namespace Ryujinx.Graphics.Shader.Translation
             return context.Add(Instruction.ShiftRightU32, Local(), a, b);
         }
 
+        public static Operand Shuffle(this EmitterContext context, Operand a, Operand b)
+        {
+            return context.Add(Instruction.Shuffle, Local(), a, b);
+        }
+
         public static (Operand, Operand) Shuffle(this EmitterContext context, Operand a, Operand b, Operand c)
         {
             return context.Add(Instruction.Shuffle, (Local(), Local()), a, b, c);
         }
 
+        public static Operand ShuffleDown(this EmitterContext context, Operand a, Operand b)
+        {
+            return context.Add(Instruction.ShuffleDown, Local(), a, b);
+        }
+
         public static (Operand, Operand) ShuffleDown(this EmitterContext context, Operand a, Operand b, Operand c)
         {
             return context.Add(Instruction.ShuffleDown, (Local(), Local()), a, b, c);
         }
 
+        public static Operand ShuffleUp(this EmitterContext context, Operand a, Operand b)
+        {
+            return context.Add(Instruction.ShuffleUp, Local(), a, b);
+        }
+
         public static (Operand, Operand) ShuffleUp(this EmitterContext context, Operand a, Operand b, Operand c)
         {
             return context.Add(Instruction.ShuffleUp, (Local(), Local()), a, b, c);
         }
 
+        public static Operand ShuffleXor(this EmitterContext context, Operand a, Operand b)
+        {
+            return context.Add(Instruction.ShuffleXor, Local(), a, b);
+        }
+
         public static (Operand, Operand) ShuffleXor(this EmitterContext context, Operand a, Operand b, Operand c)
         {
             return context.Add(Instruction.ShuffleXor, (Local(), Local()), a, b, c);
@@ -807,6 +831,11 @@ namespace Ryujinx.Graphics.Shader.Translation
             return context.Add(Instruction.Store, storageKind, null, e0, e1, value);
         }
 
+        public static Operand Store(this EmitterContext context, StorageKind storageKind, int binding, Operand value)
+        {
+            return context.Add(Instruction.Store, storageKind, null, Const(binding), value);
+        }
+
         public static Operand Store(this EmitterContext context, StorageKind storageKind, int binding, Operand e0, Operand value)
         {
             return context.Add(Instruction.Store, storageKind, null, Const(binding), e0, value);
@@ -868,7 +897,21 @@ namespace Ryujinx.Graphics.Shader.Translation
             context.Add(new TextureOperation(Instruction.TextureSample, type, TextureFormat.Unknown, flags, binding, compMask, dests, sources));
         }
 
-        public static Operand TextureSize(
+        public static Operand TextureQuerySamples(
+            this EmitterContext context,
+            SamplerType type,
+            TextureFlags flags,
+            int binding,
+            Operand[] sources)
+        {
+            Operand dest = Local();
+
+            context.Add(new TextureOperation(Instruction.TextureQuerySamples, type, TextureFormat.Unknown, flags, binding, 0, new[] { dest }, sources));
+
+            return dest;
+        }
+
+        public static Operand TextureQuerySize(
             this EmitterContext context,
             SamplerType type,
             TextureFlags flags,
@@ -878,7 +921,7 @@ namespace Ryujinx.Graphics.Shader.Translation
         {
             Operand dest = Local();
 
-            context.Add(new TextureOperation(Instruction.TextureSize, type, TextureFormat.Unknown, flags, binding, compIndex, new[] { dest }, sources));
+            context.Add(new TextureOperation(Instruction.TextureQuerySize, type, TextureFormat.Unknown, flags, binding, compIndex, new[] { dest }, sources));
 
             return dest;
         }
diff --git a/src/Ryujinx.Graphics.Shader/Translation/FeatureFlags.cs b/src/Ryujinx.Graphics.Shader/Translation/FeatureFlags.cs
index 5b7226acd..88525462d 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/FeatureFlags.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/FeatureFlags.cs
@@ -18,8 +18,13 @@ namespace Ryujinx.Graphics.Shader.Translation
         InstanceId = 1 << 3,
         DrawParameters = 1 << 4,
         RtLayer = 1 << 5,
+        Shuffle = 1 << 6,
+        ViewportIndex = 1 << 7,
+        ViewportMask = 1 << 8,
         FixedFuncAttr = 1 << 9,
         LocalMemory = 1 << 10,
         SharedMemory = 1 << 11,
+        Store = 1 << 12,
+        VtgAsCompute = 1 << 13,
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/Translation/HelperFunctionManager.cs b/src/Ryujinx.Graphics.Shader/Translation/HelperFunctionManager.cs
index 2addff5c0..ef2f8759d 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/HelperFunctionManager.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/HelperFunctionManager.cs
@@ -56,6 +56,20 @@ namespace Ryujinx.Graphics.Shader.Translation
             return functionId;
         }
 
+        public int GetOrCreateShuffleFunctionId(HelperFunctionName functionName, int subgroupSize)
+        {
+            if (_functionIds.TryGetValue((int)functionName, out int functionId))
+            {
+                return functionId;
+            }
+
+            Function function = GenerateShuffleFunction(functionName, subgroupSize);
+            functionId = AddFunction(function);
+            _functionIds.Add((int)functionName, functionId);
+
+            return functionId;
+        }
+
         private Function GenerateFunction(HelperFunctionName functionName)
         {
             return functionName switch
@@ -216,6 +230,137 @@ namespace Ryujinx.Graphics.Shader.Translation
             return new Function(ControlFlowGraph.Create(context.GetOperations()).Blocks, $"SharedStore{bitSize}_{id}", false, 2, 0);
         }
 
+        private static Function GenerateShuffleFunction(HelperFunctionName functionName, int subgroupSize)
+        {
+            return functionName switch
+            {
+                HelperFunctionName.Shuffle => GenerateShuffle(subgroupSize),
+                HelperFunctionName.ShuffleDown => GenerateShuffleDown(subgroupSize),
+                HelperFunctionName.ShuffleUp => GenerateShuffleUp(subgroupSize),
+                HelperFunctionName.ShuffleXor => GenerateShuffleXor(subgroupSize),
+                _ => throw new ArgumentException($"Invalid function name {functionName}"),
+            };
+        }
+
+        private static Function GenerateShuffle(int subgroupSize)
+        {
+            EmitterContext context = new();
+
+            Operand value = Argument(0);
+            Operand index = Argument(1);
+            Operand mask = Argument(2);
+
+            Operand clamp = context.BitwiseAnd(mask, Const(0x1f));
+            Operand segMask = context.BitwiseAnd(context.ShiftRightU32(mask, Const(8)), Const(0x1f));
+            Operand minThreadId = context.BitwiseAnd(GenerateLoadSubgroupLaneId(context, subgroupSize), segMask);
+            Operand maxThreadId = context.BitwiseOr(context.BitwiseAnd(clamp, context.BitwiseNot(segMask)), minThreadId);
+            Operand srcThreadId = context.BitwiseOr(context.BitwiseAnd(index, context.BitwiseNot(segMask)), minThreadId);
+            Operand valid = context.ICompareLessOrEqualUnsigned(srcThreadId, maxThreadId);
+
+            context.Copy(Argument(3), valid);
+
+            Operand result = context.Shuffle(value, GenerateSubgroupShuffleIndex(context, srcThreadId, subgroupSize));
+
+            context.Return(context.ConditionalSelect(valid, result, value));
+
+            return new Function(ControlFlowGraph.Create(context.GetOperations()).Blocks, "Shuffle", true, 3, 1);
+        }
+
+        private static Function GenerateShuffleDown(int subgroupSize)
+        {
+            EmitterContext context = new();
+
+            Operand value = Argument(0);
+            Operand index = Argument(1);
+            Operand mask = Argument(2);
+
+            Operand clamp = context.BitwiseAnd(mask, Const(0x1f));
+            Operand segMask = context.BitwiseAnd(context.ShiftRightU32(mask, Const(8)), Const(0x1f));
+            Operand laneId = GenerateLoadSubgroupLaneId(context, subgroupSize);
+            Operand minThreadId = context.BitwiseAnd(laneId, segMask);
+            Operand maxThreadId = context.BitwiseOr(context.BitwiseAnd(clamp, context.BitwiseNot(segMask)), minThreadId);
+            Operand srcThreadId = context.IAdd(laneId, index);
+            Operand valid = context.ICompareLessOrEqualUnsigned(srcThreadId, maxThreadId);
+
+            context.Copy(Argument(3), valid);
+
+            Operand result = context.Shuffle(value, GenerateSubgroupShuffleIndex(context, srcThreadId, subgroupSize));
+
+            context.Return(context.ConditionalSelect(valid, result, value));
+
+            return new Function(ControlFlowGraph.Create(context.GetOperations()).Blocks, "ShuffleDown", true, 3, 1);
+        }
+
+        private static Function GenerateShuffleUp(int subgroupSize)
+        {
+            EmitterContext context = new();
+
+            Operand value = Argument(0);
+            Operand index = Argument(1);
+            Operand mask = Argument(2);
+
+            Operand segMask = context.BitwiseAnd(context.ShiftRightU32(mask, Const(8)), Const(0x1f));
+            Operand laneId = GenerateLoadSubgroupLaneId(context, subgroupSize);
+            Operand minThreadId = context.BitwiseAnd(laneId, segMask);
+            Operand srcThreadId = context.ISubtract(laneId, index);
+            Operand valid = context.ICompareGreaterOrEqual(srcThreadId, minThreadId);
+
+            context.Copy(Argument(3), valid);
+
+            Operand result = context.Shuffle(value, GenerateSubgroupShuffleIndex(context, srcThreadId, subgroupSize));
+
+            context.Return(context.ConditionalSelect(valid, result, value));
+
+            return new Function(ControlFlowGraph.Create(context.GetOperations()).Blocks, "ShuffleUp", true, 3, 1);
+        }
+
+        private static Function GenerateShuffleXor(int subgroupSize)
+        {
+            EmitterContext context = new();
+
+            Operand value = Argument(0);
+            Operand index = Argument(1);
+            Operand mask = Argument(2);
+
+            Operand clamp = context.BitwiseAnd(mask, Const(0x1f));
+            Operand segMask = context.BitwiseAnd(context.ShiftRightU32(mask, Const(8)), Const(0x1f));
+            Operand laneId = GenerateLoadSubgroupLaneId(context, subgroupSize);
+            Operand minThreadId = context.BitwiseAnd(laneId, segMask);
+            Operand maxThreadId = context.BitwiseOr(context.BitwiseAnd(clamp, context.BitwiseNot(segMask)), minThreadId);
+            Operand srcThreadId = context.BitwiseExclusiveOr(laneId, index);
+            Operand valid = context.ICompareLessOrEqualUnsigned(srcThreadId, maxThreadId);
+
+            context.Copy(Argument(3), valid);
+
+            Operand result = context.Shuffle(value, GenerateSubgroupShuffleIndex(context, srcThreadId, subgroupSize));
+
+            context.Return(context.ConditionalSelect(valid, result, value));
+
+            return new Function(ControlFlowGraph.Create(context.GetOperations()).Blocks, "ShuffleXor", true, 3, 1);
+        }
+
+        private static Operand GenerateLoadSubgroupLaneId(EmitterContext context, int subgroupSize)
+        {
+            if (subgroupSize <= 32)
+            {
+                return context.Load(StorageKind.Input, IoVariable.SubgroupLaneId);
+            }
+
+            return context.BitwiseAnd(context.Load(StorageKind.Input, IoVariable.SubgroupLaneId), Const(0x1f));
+        }
+
+        private static Operand GenerateSubgroupShuffleIndex(EmitterContext context, Operand srcThreadId, int subgroupSize)
+        {
+            if (subgroupSize <= 32)
+            {
+                return srcThreadId;
+            }
+
+            return context.BitwiseOr(
+                context.BitwiseAnd(context.Load(StorageKind.Input, IoVariable.SubgroupLaneId), Const(0x60)),
+                srcThreadId);
+        }
+
         private Function GenerateTexelFetchScaleFunction()
         {
             EmitterContext context = new();
diff --git a/src/Ryujinx.Graphics.Shader/Translation/HelperFunctionName.cs b/src/Ryujinx.Graphics.Shader/Translation/HelperFunctionName.cs
index e5af17355..09b17729d 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/HelperFunctionName.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/HelperFunctionName.cs
@@ -2,12 +2,18 @@ namespace Ryujinx.Graphics.Shader.Translation
 {
     enum HelperFunctionName
     {
+        Invalid,
+
         ConvertDoubleToFloat,
         ConvertFloatToDouble,
         SharedAtomicMaxS32,
         SharedAtomicMinS32,
         SharedStore8,
         SharedStore16,
+        Shuffle,
+        ShuffleDown,
+        ShuffleUp,
+        ShuffleXor,
         TexelFetchScale,
         TextureSizeUnscale,
     }
diff --git a/src/Ryujinx.Graphics.Shader/Translation/IoUsage.cs b/src/Ryujinx.Graphics.Shader/Translation/IoUsage.cs
new file mode 100644
index 000000000..8ce2da4a0
--- /dev/null
+++ b/src/Ryujinx.Graphics.Shader/Translation/IoUsage.cs
@@ -0,0 +1,28 @@
+namespace Ryujinx.Graphics.Shader.Translation
+{
+    readonly struct IoUsage
+    {
+        private readonly FeatureFlags _usedFeatures;
+
+        public readonly bool UsesRtLayer => _usedFeatures.HasFlag(FeatureFlags.RtLayer);
+        public readonly bool UsesViewportIndex => _usedFeatures.HasFlag(FeatureFlags.ViewportIndex);
+        public readonly bool UsesViewportMask => _usedFeatures.HasFlag(FeatureFlags.ViewportMask);
+        public readonly byte ClipDistancesWritten { get; }
+        public readonly int UserDefinedMap { get; }
+
+        public IoUsage(FeatureFlags usedFeatures, byte clipDistancesWritten, int userDefinedMap)
+        {
+            _usedFeatures = usedFeatures;
+            ClipDistancesWritten = clipDistancesWritten;
+            UserDefinedMap = userDefinedMap;
+        }
+
+        public readonly IoUsage Combine(IoUsage other)
+        {
+            return new IoUsage(
+                _usedFeatures | other._usedFeatures,
+                (byte)(ClipDistancesWritten | other.ClipDistancesWritten),
+                UserDefinedMap | other.UserDefinedMap);
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs
index 43d98d3cb..19b7999a7 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs
@@ -27,9 +27,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
                     continue;
                 }
 
-                if (texOp.Inst == Instruction.Lod ||
-                    texOp.Inst == Instruction.TextureSample ||
-                    texOp.Inst == Instruction.TextureSize)
+                if (texOp.Inst == Instruction.TextureSample || texOp.Inst.IsTextureQuery())
                 {
                     Operand bindlessHandle = Utils.FindLastOperation(texOp.GetSource(0), block);
 
@@ -40,7 +38,8 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
                     // as long bindless elimination is successful and we know where the texture descriptor is located.
                     bool rewriteSamplerType =
                         texOp.Type == SamplerType.TextureBuffer ||
-                        texOp.Inst == Instruction.TextureSize;
+                        texOp.Inst == Instruction.TextureQuerySamples ||
+                        texOp.Inst == Instruction.TextureQuerySize;
 
                     if (bindlessHandle.Type == OperandType.ConstantBuffer)
                     {
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs
index 0f043f772..8a730ef74 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs
@@ -2,6 +2,7 @@ using Ryujinx.Graphics.Shader.IntermediateRepresentation;
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Text;
 using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper;
 
 namespace Ryujinx.Graphics.Shader.Translation.Optimizations
@@ -785,30 +786,31 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
 
         private static string GetFunctionName(Operation baseOp, bool isMultiTarget, IReadOnlyList<uint> targetCbs)
         {
-            string name = baseOp.Inst.ToString();
+            StringBuilder nameBuilder = new();
+            nameBuilder.Append(baseOp.Inst.ToString());
 
-            name += baseOp.StorageKind switch
+            nameBuilder.Append(baseOp.StorageKind switch
             {
                 StorageKind.GlobalMemoryS8 => "S8",
                 StorageKind.GlobalMemoryS16 => "S16",
                 StorageKind.GlobalMemoryU8 => "U8",
                 StorageKind.GlobalMemoryU16 => "U16",
                 _ => string.Empty,
-            };
+            });
 
             if (isMultiTarget)
             {
-                name += "Multi";
+                nameBuilder.Append("Multi");
             }
 
             foreach (uint targetCb in targetCbs)
             {
                 (int sbCbSlot, int sbCbOffset) = UnpackCbSlotAndOffset(targetCb);
 
-                name += $"_c{sbCbSlot}o{sbCbOffset}";
+                nameBuilder.Append($"_c{sbCbSlot}o{sbCbOffset}");
             }
 
-            return name;
+            return nameBuilder.ToString();
         }
 
         private static bool TryGenerateStorageOp(
@@ -1126,7 +1128,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
             // so we want to get the byte offset back, since each one of those word
             // offsets are a new "local variable" which will not match.
 
-            if (operation.GetSource(0).AsgOp is Operation shiftRightOp &&
+            if (operation.GetSource(1).AsgOp is Operation shiftRightOp &&
                 shiftRightOp.Inst == Instruction.ShiftRightU32 &&
                 shiftRightOp.GetSource(1).Type == OperandType.Constant &&
                 shiftRightOp.GetSource(1).Value == 2)
@@ -1158,9 +1160,11 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
 
         private static bool TryGetLocalMemoryOffset(Operation operation, out int constOffset)
         {
-            if (operation.GetSource(0).Type == OperandType.Constant)
+            Operand offset = operation.GetSource(1);
+
+            if (offset.Type == OperandType.Constant)
             {
-                constOffset = operation.GetSource(0).Value;
+                constOffset = offset.Value;
                 return true;
             }
 
diff --git a/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs b/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs
index d07d8dce5..83332711f 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs
@@ -48,12 +48,22 @@ namespace Ryujinx.Graphics.Shader.Translation
         public int LocalMemoryId { get; private set; }
         public int SharedMemoryId { get; private set; }
 
+        public int LocalVertexDataMemoryId { get; private set; }
+        public int LocalTopologyRemapMemoryId { get; private set; }
+        public int LocalVertexIndexVertexRateMemoryId { get; private set; }
+        public int LocalVertexIndexInstanceRateMemoryId { get; private set; }
+        public int LocalGeometryOutputVertexCountMemoryId { get; private set; }
+        public int LocalGeometryOutputIndexCountMemoryId { get; private set; }
+
         public ShaderProperties Properties { get; }
 
-        public ResourceManager(ShaderStage stage, IGpuAccessor gpuAccessor)
+        public ResourceReservations Reservations { get; }
+
+        public ResourceManager(ShaderStage stage, IGpuAccessor gpuAccessor, ResourceReservations reservations = null)
         {
             _gpuAccessor = gpuAccessor;
             Properties = new();
+            Reservations = reservations;
             _stage = stage;
             _stagePrefix = GetShaderStagePrefix(stage);
 
@@ -114,6 +124,29 @@ namespace Ryujinx.Graphics.Shader.Translation
             }
         }
 
+        public void SetVertexAsComputeLocalMemories(ShaderStage stage, InputTopology inputTopology)
+        {
+            LocalVertexDataMemoryId = AddMemoryDefinition("local_vertex_data", AggregateType.Array | AggregateType.FP32, Reservations.OutputSizePerInvocation);
+
+            if (stage == ShaderStage.Vertex)
+            {
+                LocalVertexIndexVertexRateMemoryId = AddMemoryDefinition("local_vertex_index_vr", AggregateType.U32);
+                LocalVertexIndexInstanceRateMemoryId = AddMemoryDefinition("local_vertex_index_ir", AggregateType.U32);
+            }
+            else if (stage == ShaderStage.Geometry)
+            {
+                LocalTopologyRemapMemoryId = AddMemoryDefinition("local_topology_remap", AggregateType.Array | AggregateType.U32, inputTopology.ToInputVertices());
+
+                LocalGeometryOutputVertexCountMemoryId = AddMemoryDefinition("local_geometry_output_vertex", AggregateType.U32);
+                LocalGeometryOutputIndexCountMemoryId = AddMemoryDefinition("local_geometry_output_index", AggregateType.U32);
+            }
+        }
+
+        private int AddMemoryDefinition(string name, AggregateType type, int arrayLength = 1)
+        {
+            return Properties.AddLocalMemory(new MemoryDefinition(name, type, arrayLength));
+        }
+
         public int GetConstantBufferBinding(int slot)
         {
             int binding = _cbSlotToBindingMap[slot];
@@ -199,8 +232,8 @@ namespace Ryujinx.Graphics.Shader.Translation
             inst &= Instruction.Mask;
             bool isImage = inst == Instruction.ImageLoad || inst == Instruction.ImageStore || inst == Instruction.ImageAtomic;
             bool isWrite = inst == Instruction.ImageStore || inst == Instruction.ImageAtomic;
-            bool accurateType = inst != Instruction.Lod && inst != Instruction.TextureSize;
-            bool intCoords = isImage || flags.HasFlag(TextureFlags.IntCoords) || inst == Instruction.TextureSize;
+            bool accurateType = !inst.IsTextureQuery();
+            bool intCoords = isImage || flags.HasFlag(TextureFlags.IntCoords) || inst == Instruction.TextureQuerySize;
             bool coherent = flags.HasFlag(TextureFlags.Coherent);
 
             if (!isImage)
@@ -465,17 +498,22 @@ namespace Ryujinx.Graphics.Shader.Translation
             return descriptors;
         }
 
-        public (int, int) GetCbufSlotAndHandleForTexture(int binding)
+        public bool TryGetCbufSlotAndHandleForTexture(int binding, out int cbufSlot, out int handle)
         {
             foreach ((TextureInfo info, TextureMeta meta) in _usedTextures)
             {
                 if (meta.Binding == binding)
                 {
-                    return (info.CbufSlot, info.Handle);
+                    cbufSlot = info.CbufSlot;
+                    handle = info.Handle;
+
+                    return true;
                 }
             }
 
-            throw new ArgumentException($"Binding {binding} is invalid.");
+            cbufSlot = 0;
+            handle = 0;
+            return false;
         }
 
         private static int FindDescriptorIndex(TextureDescriptor[] array, int binding)
diff --git a/src/Ryujinx.Graphics.Shader/Translation/ResourceReservations.cs b/src/Ryujinx.Graphics.Shader/Translation/ResourceReservations.cs
new file mode 100644
index 000000000..d559f6699
--- /dev/null
+++ b/src/Ryujinx.Graphics.Shader/Translation/ResourceReservations.cs
@@ -0,0 +1,186 @@
+using Ryujinx.Graphics.Shader.IntermediateRepresentation;
+using Ryujinx.Graphics.Shader.StructuredIr;
+using System.Collections.Generic;
+using System.Numerics;
+
+namespace Ryujinx.Graphics.Shader.Translation
+{
+    public class ResourceReservations
+    {
+        public const int TfeBuffersCount = 4;
+
+        public const int MaxVertexBufferTextures = 32;
+
+        public int VertexInfoConstantBufferBinding { get; }
+        public int VertexOutputStorageBufferBinding { get; }
+        public int GeometryVertexOutputStorageBufferBinding { get; }
+        public int GeometryIndexOutputStorageBufferBinding { get; }
+        public int IndexBufferTextureBinding { get; }
+        public int TopologyRemapBufferTextureBinding { get; }
+
+        public int ReservedConstantBuffers { get; }
+        public int ReservedStorageBuffers { get; }
+        public int ReservedTextures { get; }
+        public int ReservedImages { get; }
+        public int InputSizePerInvocation { get; }
+        public int OutputSizePerInvocation { get; }
+        public int OutputSizeInBytesPerInvocation => OutputSizePerInvocation * sizeof(uint);
+
+        private readonly int _tfeBufferSbBaseBinding;
+        private readonly int _vertexBufferTextureBaseBinding;
+
+        private readonly Dictionary<IoDefinition, int> _offsets;
+        internal IReadOnlyDictionary<IoDefinition, int> Offsets => _offsets;
+
+        internal ResourceReservations(bool isTransformFeedbackEmulated, bool vertexAsCompute)
+        {
+            // All stages reserves the first constant buffer binding for the support buffer.
+            ReservedConstantBuffers = 1;
+            ReservedStorageBuffers = 0;
+            ReservedTextures = 0;
+            ReservedImages = 0;
+
+            if (isTransformFeedbackEmulated)
+            {
+                // Transform feedback emulation currently always uses 4 storage buffers.
+                _tfeBufferSbBaseBinding = ReservedStorageBuffers;
+                ReservedStorageBuffers = TfeBuffersCount;
+            }
+
+            if (vertexAsCompute)
+            {
+                // One constant buffer reserved for vertex related state.
+                VertexInfoConstantBufferBinding = ReservedConstantBuffers++;
+
+                // One storage buffer for the output vertex data.
+                VertexOutputStorageBufferBinding = ReservedStorageBuffers++;
+
+                // One storage buffer for the output geometry vertex data.
+                GeometryVertexOutputStorageBufferBinding = ReservedStorageBuffers++;
+
+                // One storage buffer for the output geometry index data.
+                GeometryIndexOutputStorageBufferBinding = ReservedStorageBuffers++;
+
+                // Enough textures reserved for all vertex attributes, plus the index buffer.
+                IndexBufferTextureBinding = ReservedTextures;
+                TopologyRemapBufferTextureBinding = ReservedTextures + 1;
+                _vertexBufferTextureBaseBinding = ReservedTextures + 2;
+                ReservedTextures += 2 + MaxVertexBufferTextures;
+            }
+        }
+
+        internal ResourceReservations(
+            IGpuAccessor gpuAccessor,
+            bool isTransformFeedbackEmulated,
+            bool vertexAsCompute,
+            IoUsage? vacInput,
+            IoUsage vacOutput) : this(isTransformFeedbackEmulated, vertexAsCompute)
+        {
+            if (vertexAsCompute)
+            {
+                _offsets = new();
+
+                if (vacInput.HasValue)
+                {
+                    InputSizePerInvocation = FillIoOffsetMap(gpuAccessor, StorageKind.Input, vacInput.Value);
+                }
+
+                OutputSizePerInvocation = FillIoOffsetMap(gpuAccessor, StorageKind.Output, vacOutput);
+            }
+        }
+
+        private int FillIoOffsetMap(IGpuAccessor gpuAccessor, StorageKind storageKind, IoUsage vacUsage)
+        {
+            int offset = 0;
+
+            for (int c = 0; c < 4; c++)
+            {
+                _offsets.Add(new IoDefinition(storageKind, IoVariable.Position, 0, c), offset++);
+            }
+
+            _offsets.Add(new IoDefinition(storageKind, IoVariable.PointSize), offset++);
+
+            int clipDistancesWrittenMap = vacUsage.ClipDistancesWritten;
+
+            while (clipDistancesWrittenMap != 0)
+            {
+                int index = BitOperations.TrailingZeroCount(clipDistancesWrittenMap);
+
+                _offsets.Add(new IoDefinition(storageKind, IoVariable.ClipDistance, 0, index), offset++);
+
+                clipDistancesWrittenMap &= ~(1 << index);
+            }
+
+            if (vacUsage.UsesRtLayer)
+            {
+                _offsets.Add(new IoDefinition(storageKind, IoVariable.Layer), offset++);
+            }
+
+            if (vacUsage.UsesViewportIndex && gpuAccessor.QueryHostSupportsViewportIndexVertexTessellation())
+            {
+                _offsets.Add(new IoDefinition(storageKind, IoVariable.VertexIndex), offset++);
+            }
+
+            if (vacUsage.UsesViewportMask && gpuAccessor.QueryHostSupportsViewportMask())
+            {
+                _offsets.Add(new IoDefinition(storageKind, IoVariable.ViewportMask), offset++);
+            }
+
+            int usedDefinedMap = vacUsage.UserDefinedMap;
+
+            while (usedDefinedMap != 0)
+            {
+                int location = BitOperations.TrailingZeroCount(usedDefinedMap);
+
+                for (int c = 0; c < 4; c++)
+                {
+                    _offsets.Add(new IoDefinition(storageKind, IoVariable.UserDefined, location, c), offset++);
+                }
+
+                usedDefinedMap &= ~(1 << location);
+            }
+
+            return offset;
+        }
+
+        internal static bool IsVectorOrArrayVariable(IoVariable variable)
+        {
+            return variable switch
+            {
+                IoVariable.ClipDistance or
+                IoVariable.Position => true,
+                _ => false,
+            };
+        }
+
+        public int GetTfeBufferStorageBufferBinding(int bufferIndex)
+        {
+            return _tfeBufferSbBaseBinding + bufferIndex;
+        }
+
+        public int GetVertexBufferTextureBinding(int vaLocation)
+        {
+            return _vertexBufferTextureBaseBinding + vaLocation;
+        }
+
+        internal bool TryGetOffset(StorageKind storageKind, int location, int component, out int offset)
+        {
+            return _offsets.TryGetValue(new IoDefinition(storageKind, IoVariable.UserDefined, location, component), out offset);
+        }
+
+        internal bool TryGetOffset(StorageKind storageKind, IoVariable ioVariable, int location, int component, out int offset)
+        {
+            return _offsets.TryGetValue(new IoDefinition(storageKind, ioVariable, location, component), out offset);
+        }
+
+        internal bool TryGetOffset(StorageKind storageKind, IoVariable ioVariable, int component, out int offset)
+        {
+            return _offsets.TryGetValue(new IoDefinition(storageKind, ioVariable, 0, component), out offset);
+        }
+
+        internal bool TryGetOffset(StorageKind storageKind, IoVariable ioVariable, out int offset)
+        {
+            return _offsets.TryGetValue(new IoDefinition(storageKind, ioVariable, 0, 0), out offset);
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.Shader/Translation/ShaderDefinitions.cs b/src/Ryujinx.Graphics.Shader/Translation/ShaderDefinitions.cs
index 204f4278f..3246e2594 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/ShaderDefinitions.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/ShaderDefinitions.cs
@@ -32,7 +32,7 @@ namespace Ryujinx.Graphics.Shader.Translation
         public bool GpPassthrough { get; }
         public bool LastInVertexPipeline { get; set; }
 
-        public int ThreadsPerInputPrimitive { get; }
+        public int ThreadsPerInputPrimitive { get; private set; }
 
         public InputTopology InputTopology => _graphicsState.Topology;
         public OutputTopology OutputTopology { get; }
@@ -97,9 +97,14 @@ namespace Ryujinx.Graphics.Shader.Translation
 
         private readonly Dictionary<TransformFeedbackVariable, TransformFeedbackOutput> _transformFeedbackDefinitions;
 
-        public ShaderDefinitions(ShaderStage stage)
+        public ShaderDefinitions(ShaderStage stage, ulong transformFeedbackVecMap, TransformFeedbackOutput[] transformFeedbackOutputs)
         {
             Stage = stage;
+            TransformFeedbackEnabled = transformFeedbackOutputs != null;
+            _transformFeedbackOutputs = transformFeedbackOutputs;
+            _transformFeedbackDefinitions = new();
+
+            PopulateTransformFeedbackDefinitions(transformFeedbackVecMap, transformFeedbackOutputs);
         }
 
         public ShaderDefinitions(
@@ -142,7 +147,6 @@ namespace Ryujinx.Graphics.Shader.Translation
             bool omapSampleMask,
             bool omapDepth,
             bool supportsScaledVertexFormats,
-            bool transformFeedbackEnabled,
             ulong transformFeedbackVecMap,
             TransformFeedbackOutput[] transformFeedbackOutputs)
         {
@@ -151,17 +155,22 @@ namespace Ryujinx.Graphics.Shader.Translation
             GpPassthrough = gpPassthrough;
             ThreadsPerInputPrimitive = threadsPerInputPrimitive;
             OutputTopology = outputTopology;
-            MaxOutputVertices = maxOutputVertices;
+            MaxOutputVertices = gpPassthrough ? graphicsState.Topology.ToInputVerticesNoAdjacency() : maxOutputVertices;
             ImapTypes = imapTypes;
             OmapTargets = omapTargets;
             OmapSampleMask = omapSampleMask;
             OmapDepth = omapDepth;
             LastInVertexPipeline = stage < ShaderStage.Fragment;
             SupportsScaledVertexFormats = supportsScaledVertexFormats;
-            TransformFeedbackEnabled = transformFeedbackEnabled;
+            TransformFeedbackEnabled = transformFeedbackOutputs != null;
             _transformFeedbackOutputs = transformFeedbackOutputs;
             _transformFeedbackDefinitions = new();
 
+            PopulateTransformFeedbackDefinitions(transformFeedbackVecMap, transformFeedbackOutputs);
+        }
+
+        private void PopulateTransformFeedbackDefinitions(ulong transformFeedbackVecMap, TransformFeedbackOutput[] transformFeedbackOutputs)
+        {
             while (transformFeedbackVecMap != 0)
             {
                 int vecIndex = BitOperations.TrailingZeroCount(transformFeedbackVecMap);
@@ -200,16 +209,6 @@ namespace Ryujinx.Graphics.Shader.Translation
             OaIndexing = true;
         }
 
-        public TransformFeedbackOutput[] GetTransformFeedbackOutputs()
-        {
-            if (!HasTransformFeedbackOutputs())
-            {
-                return null;
-            }
-
-            return _transformFeedbackOutputs;
-        }
-
         public bool TryGetTransformFeedbackOutput(IoVariable ioVariable, int location, int component, out TransformFeedbackOutput transformFeedbackOutput)
         {
             if (!HasTransformFeedbackOutputs())
@@ -320,5 +319,35 @@ namespace Ryujinx.Graphics.Shader.Translation
         {
             return _graphicsState.AttributeTypes[location];
         }
+
+        public bool IsAttributeSint(int location)
+        {
+            return (_graphicsState.AttributeTypes[location] & ~AttributeType.AnyPacked) == AttributeType.Sint;
+        }
+
+        public bool IsAttributePacked(int location)
+        {
+            return _graphicsState.AttributeTypes[location].HasFlag(AttributeType.Packed);
+        }
+
+        public bool IsAttributePackedRgb10A2Signed(int location)
+        {
+            return _graphicsState.AttributeTypes[location].HasFlag(AttributeType.PackedRgb10A2Signed);
+        }
+
+        public int GetGeometryOutputIndexBufferStridePerInstance()
+        {
+            return MaxOutputVertices + OutputTopology switch
+            {
+                OutputTopology.LineStrip => MaxOutputVertices / 2,
+                OutputTopology.TriangleStrip => MaxOutputVertices / 3,
+                _ => MaxOutputVertices,
+            };
+        }
+
+        public int GetGeometryOutputIndexBufferStride()
+        {
+            return GetGeometryOutputIndexBufferStridePerInstance() * ThreadsPerInputPrimitive;
+        }
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/Translation/ShaderIdentifier.cs b/src/Ryujinx.Graphics.Shader/Translation/ShaderIdentifier.cs
deleted file mode 100644
index c077e1cde..000000000
--- a/src/Ryujinx.Graphics.Shader/Translation/ShaderIdentifier.cs
+++ /dev/null
@@ -1,187 +0,0 @@
-using Ryujinx.Graphics.Shader.IntermediateRepresentation;
-using System.Collections.Generic;
-
-namespace Ryujinx.Graphics.Shader.Translation
-{
-    static class ShaderIdentifier
-    {
-        public static ShaderIdentification Identify(
-            IReadOnlyList<Function> functions,
-            IGpuAccessor gpuAccessor,
-            ShaderStage stage,
-            InputTopology inputTopology,
-            out int layerInputAttr)
-        {
-            if (stage == ShaderStage.Geometry &&
-                inputTopology == InputTopology.Triangles &&
-                !gpuAccessor.QueryHostSupportsGeometryShader() &&
-                IsLayerPassthroughGeometryShader(functions, out layerInputAttr))
-            {
-                return ShaderIdentification.GeometryLayerPassthrough;
-            }
-
-            layerInputAttr = 0;
-            return ShaderIdentification.None;
-        }
-
-        private static bool IsLayerPassthroughGeometryShader(IReadOnlyList<Function> functions, out int layerInputAttr)
-        {
-            bool writesLayer = false;
-            layerInputAttr = 0;
-
-            if (functions.Count != 1)
-            {
-                return false;
-            }
-
-            int verticesCount = 0;
-            int totalVerticesCount = 0;
-
-            foreach (BasicBlock block in functions[0].Blocks)
-            {
-                // We are not expecting loops or any complex control flow here, so fail in those cases.
-                if (block.Branch != null && block.Branch.Index <= block.Index)
-                {
-                    return false;
-                }
-
-                foreach (INode node in block.Operations)
-                {
-                    if (node is not Operation operation)
-                    {
-                        continue;
-                    }
-
-                    if (IsResourceWrite(operation.Inst, operation.StorageKind))
-                    {
-                        return false;
-                    }
-
-                    if (operation.Inst == Instruction.Store && operation.StorageKind == StorageKind.Output)
-                    {
-                        Operand src = operation.GetSource(operation.SourcesCount - 1);
-                        Operation srcAttributeAsgOp = null;
-
-                        if (src.Type == OperandType.LocalVariable &&
-                            src.AsgOp is Operation asgOp &&
-                            asgOp.Inst == Instruction.Load &&
-                            asgOp.StorageKind.IsInputOrOutput())
-                        {
-                            if (asgOp.StorageKind != StorageKind.Input)
-                            {
-                                return false;
-                            }
-
-                            srcAttributeAsgOp = asgOp;
-                        }
-
-                        if (srcAttributeAsgOp != null)
-                        {
-                            IoVariable dstAttribute = (IoVariable)operation.GetSource(0).Value;
-                            IoVariable srcAttribute = (IoVariable)srcAttributeAsgOp.GetSource(0).Value;
-
-                            if (dstAttribute == IoVariable.Layer && srcAttribute == IoVariable.UserDefined)
-                            {
-                                if (srcAttributeAsgOp.SourcesCount != 4)
-                                {
-                                    return false;
-                                }
-
-                                writesLayer = true;
-                                layerInputAttr = srcAttributeAsgOp.GetSource(1).Value * 4 + srcAttributeAsgOp.GetSource(3).Value;
-                            }
-                            else
-                            {
-                                if (dstAttribute != srcAttribute)
-                                {
-                                    return false;
-                                }
-
-                                int inputsCount = operation.SourcesCount - 2;
-
-                                if (dstAttribute == IoVariable.UserDefined)
-                                {
-                                    if (operation.GetSource(1).Value != srcAttributeAsgOp.GetSource(1).Value)
-                                    {
-                                        return false;
-                                    }
-
-                                    inputsCount--;
-                                }
-
-                                for (int i = 0; i < inputsCount; i++)
-                                {
-                                    int dstIndex = operation.SourcesCount - 2 - i;
-                                    int srcIndex = srcAttributeAsgOp.SourcesCount - 1 - i;
-
-                                    if ((dstIndex | srcIndex) < 0)
-                                    {
-                                        return false;
-                                    }
-
-                                    if (operation.GetSource(dstIndex).Type != OperandType.Constant ||
-                                        srcAttributeAsgOp.GetSource(srcIndex).Type != OperandType.Constant ||
-                                        operation.GetSource(dstIndex).Value != srcAttributeAsgOp.GetSource(srcIndex).Value)
-                                    {
-                                        return false;
-                                    }
-                                }
-                            }
-                        }
-                        else if (src.Type == OperandType.Constant)
-                        {
-                            int dstComponent = operation.GetSource(operation.SourcesCount - 2).Value;
-                            float expectedValue = dstComponent == 3 ? 1f : 0f;
-
-                            if (src.AsFloat() != expectedValue)
-                            {
-                                return false;
-                            }
-                        }
-                        else
-                        {
-                            return false;
-                        }
-                    }
-                    else if (operation.Inst == Instruction.EmitVertex)
-                    {
-                        verticesCount++;
-                    }
-                    else if (operation.Inst == Instruction.EndPrimitive)
-                    {
-                        totalVerticesCount += verticesCount;
-                        verticesCount = 0;
-                    }
-                }
-            }
-
-            return totalVerticesCount + verticesCount == 3 && writesLayer;
-        }
-
-        private static bool IsResourceWrite(Instruction inst, StorageKind storageKind)
-        {
-            switch (inst)
-            {
-                case Instruction.AtomicAdd:
-                case Instruction.AtomicAnd:
-                case Instruction.AtomicCompareAndSwap:
-                case Instruction.AtomicMaxS32:
-                case Instruction.AtomicMaxU32:
-                case Instruction.AtomicMinS32:
-                case Instruction.AtomicMinU32:
-                case Instruction.AtomicOr:
-                case Instruction.AtomicSwap:
-                case Instruction.AtomicXor:
-                case Instruction.ImageAtomic:
-                case Instruction.ImageStore:
-                    return true;
-                case Instruction.Store:
-                    return storageKind == StorageKind.StorageBuffer ||
-                           storageKind == StorageKind.SharedMemory ||
-                           storageKind == StorageKind.LocalMemory;
-            }
-
-            return false;
-        }
-    }
-}
diff --git a/src/Ryujinx.Graphics.Shader/Translation/TransformContext.cs b/src/Ryujinx.Graphics.Shader/Translation/TransformContext.cs
index fa687eca0..87ebb8e7c 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/TransformContext.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/TransformContext.cs
@@ -6,6 +6,7 @@ namespace Ryujinx.Graphics.Shader.Translation
     {
         public readonly HelperFunctionManager Hfm;
         public readonly BasicBlock[] Blocks;
+        public readonly ShaderDefinitions Definitions;
         public readonly ResourceManager ResourceManager;
         public readonly IGpuAccessor GpuAccessor;
         public readonly TargetLanguage TargetLanguage;
@@ -15,6 +16,7 @@ namespace Ryujinx.Graphics.Shader.Translation
         public TransformContext(
             HelperFunctionManager hfm,
             BasicBlock[] blocks,
+            ShaderDefinitions definitions,
             ResourceManager resourceManager,
             IGpuAccessor gpuAccessor,
             TargetLanguage targetLanguage,
@@ -23,6 +25,7 @@ namespace Ryujinx.Graphics.Shader.Translation
         {
             Hfm = hfm;
             Blocks = blocks;
+            Definitions = definitions;
             ResourceManager = resourceManager;
             GpuAccessor = gpuAccessor;
             TargetLanguage = targetLanguage;
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Transforms/GeometryToCompute.cs b/src/Ryujinx.Graphics.Shader/Translation/Transforms/GeometryToCompute.cs
new file mode 100644
index 000000000..0013cf0eb
--- /dev/null
+++ b/src/Ryujinx.Graphics.Shader/Translation/Transforms/GeometryToCompute.cs
@@ -0,0 +1,378 @@
+using Ryujinx.Graphics.Shader.IntermediateRepresentation;
+using Ryujinx.Graphics.Shader.Translation.Optimizations;
+using System.Collections.Generic;
+
+using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper;
+
+namespace Ryujinx.Graphics.Shader.Translation.Transforms
+{
+    class GeometryToCompute : ITransformPass
+    {
+        public static bool IsEnabled(IGpuAccessor gpuAccessor, ShaderStage stage, TargetLanguage targetLanguage, FeatureFlags usedFeatures)
+        {
+            return usedFeatures.HasFlag(FeatureFlags.VtgAsCompute);
+        }
+
+        public static LinkedListNode<INode> RunPass(TransformContext context, LinkedListNode<INode> node)
+        {
+            if (context.Definitions.Stage != ShaderStage.Geometry)
+            {
+                return node;
+            }
+
+            Operation operation = (Operation)node.Value;
+
+            LinkedListNode<INode> newNode = node;
+
+            switch (operation.Inst)
+            {
+                case Instruction.EmitVertex:
+                    newNode = GenerateEmitVertex(context.Definitions, context.ResourceManager, node);
+                    break;
+                case Instruction.EndPrimitive:
+                    newNode = GenerateEndPrimitive(context.Definitions, context.ResourceManager, node);
+                    break;
+                case Instruction.Load:
+                    if (operation.StorageKind == StorageKind.Input)
+                    {
+                        IoVariable ioVariable = (IoVariable)operation.GetSource(0).Value;
+
+                        if (TryGetOffset(context.ResourceManager, operation, StorageKind.Input, out int inputOffset))
+                        {
+                            Operand primVertex = ioVariable == IoVariable.UserDefined
+                                ? operation.GetSource(2)
+                                : operation.GetSource(1);
+
+                            Operand vertexElemOffset = GenerateVertexOffset(context.ResourceManager, node, inputOffset, primVertex);
+
+                            newNode = node.List.AddBefore(node, new Operation(
+                                Instruction.Load,
+                                StorageKind.StorageBuffer,
+                                operation.Dest,
+                                new[] { Const(context.ResourceManager.Reservations.VertexOutputStorageBufferBinding), Const(0), vertexElemOffset }));
+                        }
+                        else
+                        {
+                            switch (ioVariable)
+                            {
+                                case IoVariable.InvocationId:
+                                    newNode = GenerateInvocationId(node, operation.Dest);
+                                    break;
+                                case IoVariable.PrimitiveId:
+                                    newNode = GeneratePrimitiveId(context.ResourceManager, node, operation.Dest);
+                                    break;
+                                case IoVariable.GlobalId:
+                                case IoVariable.SubgroupEqMask:
+                                case IoVariable.SubgroupGeMask:
+                                case IoVariable.SubgroupGtMask:
+                                case IoVariable.SubgroupLaneId:
+                                case IoVariable.SubgroupLeMask:
+                                case IoVariable.SubgroupLtMask:
+                                    // Those are valid or expected for geometry shaders.
+                                    break;
+                                default:
+                                    context.GpuAccessor.Log($"Invalid input \"{ioVariable}\".");
+                                    break;
+                            }
+                        }
+                    }
+                    else if (operation.StorageKind == StorageKind.Output)
+                    {
+                        if (TryGetOffset(context.ResourceManager, operation, StorageKind.Output, out int outputOffset))
+                        {
+                            newNode = node.List.AddBefore(node, new Operation(
+                                Instruction.Load,
+                                StorageKind.LocalMemory,
+                                operation.Dest,
+                                new[] { Const(context.ResourceManager.LocalVertexDataMemoryId), Const(outputOffset) }));
+                        }
+                        else
+                        {
+                            context.GpuAccessor.Log($"Invalid output \"{(IoVariable)operation.GetSource(0).Value}\".");
+                        }
+                    }
+                    break;
+                case Instruction.Store:
+                    if (operation.StorageKind == StorageKind.Output)
+                    {
+                        if (TryGetOffset(context.ResourceManager, operation, StorageKind.Output, out int outputOffset))
+                        {
+                            Operand value = operation.GetSource(operation.SourcesCount - 1);
+
+                            newNode = node.List.AddBefore(node, new Operation(
+                                Instruction.Store,
+                                StorageKind.LocalMemory,
+                                (Operand)null,
+                                new[] { Const(context.ResourceManager.LocalVertexDataMemoryId), Const(outputOffset), value }));
+                        }
+                        else
+                        {
+                            context.GpuAccessor.Log($"Invalid output \"{(IoVariable)operation.GetSource(0).Value}\".");
+                        }
+                    }
+                    break;
+            }
+
+            if (newNode != node)
+            {
+                Utils.DeleteNode(node, operation);
+            }
+
+            return newNode;
+        }
+
+        private static LinkedListNode<INode> GenerateEmitVertex(ShaderDefinitions definitions, ResourceManager resourceManager, LinkedListNode<INode> node)
+        {
+            int vbOutputBinding = resourceManager.Reservations.GeometryVertexOutputStorageBufferBinding;
+            int ibOutputBinding = resourceManager.Reservations.GeometryIndexOutputStorageBufferBinding;
+            int stride = resourceManager.Reservations.OutputSizePerInvocation;
+
+            Operand outputPrimVertex = IncrementLocalMemory(node, resourceManager.LocalGeometryOutputVertexCountMemoryId);
+            Operand baseVertexOffset = GenerateBaseOffset(
+                resourceManager,
+                node,
+                definitions.MaxOutputVertices * definitions.ThreadsPerInputPrimitive,
+                definitions.ThreadsPerInputPrimitive);
+            Operand outputBaseVertex = Local();
+            node.List.AddBefore(node, new Operation(Instruction.Add, outputBaseVertex, new[] { baseVertexOffset, outputPrimVertex }));
+
+            Operand outputPrimIndex = IncrementLocalMemory(node, resourceManager.LocalGeometryOutputIndexCountMemoryId);
+            Operand baseIndexOffset = GenerateBaseOffset(
+                resourceManager,
+                node,
+                definitions.GetGeometryOutputIndexBufferStride(),
+                definitions.ThreadsPerInputPrimitive);
+            Operand outputBaseIndex = Local();
+            node.List.AddBefore(node, new Operation(Instruction.Add, outputBaseIndex, new[] { baseIndexOffset, outputPrimIndex }));
+
+            node.List.AddBefore(node, new Operation(
+                Instruction.Store,
+                StorageKind.StorageBuffer,
+                null,
+                new[] { Const(ibOutputBinding), Const(0), outputBaseIndex, outputBaseVertex }));
+
+            Operand baseOffset = Local();
+            node.List.AddBefore(node, new Operation(Instruction.Multiply, baseOffset, new[] { outputBaseVertex, Const(stride) }));
+
+            LinkedListNode<INode> newNode = node;
+
+            for (int offset = 0; offset < stride; offset++)
+            {
+                Operand vertexOffset;
+
+                if (offset > 0)
+                {
+                    vertexOffset = Local();
+                    node.List.AddBefore(node, new Operation(Instruction.Add, vertexOffset, new[] { baseOffset, Const(offset) }));
+                }
+                else
+                {
+                    vertexOffset = baseOffset;
+                }
+
+                Operand value = Local();
+                node.List.AddBefore(node, new Operation(
+                    Instruction.Load,
+                    StorageKind.LocalMemory,
+                    value,
+                    new[] { Const(resourceManager.LocalVertexDataMemoryId), Const(offset) }));
+
+                newNode = node.List.AddBefore(node, new Operation(
+                    Instruction.Store,
+                    StorageKind.StorageBuffer,
+                    null,
+                    new[] { Const(vbOutputBinding), Const(0), vertexOffset, value }));
+            }
+
+            return newNode;
+        }
+
+        private static LinkedListNode<INode> GenerateEndPrimitive(ShaderDefinitions definitions, ResourceManager resourceManager, LinkedListNode<INode> node)
+        {
+            int ibOutputBinding = resourceManager.Reservations.GeometryIndexOutputStorageBufferBinding;
+
+            Operand outputPrimIndex = IncrementLocalMemory(node, resourceManager.LocalGeometryOutputIndexCountMemoryId);
+            Operand baseIndexOffset = GenerateBaseOffset(
+                resourceManager,
+                node,
+                definitions.GetGeometryOutputIndexBufferStride(),
+                definitions.ThreadsPerInputPrimitive);
+            Operand outputBaseIndex = Local();
+            node.List.AddBefore(node, new Operation(Instruction.Add, outputBaseIndex, new[] { baseIndexOffset, outputPrimIndex }));
+
+            return node.List.AddBefore(node, new Operation(
+                Instruction.Store,
+                StorageKind.StorageBuffer,
+                null,
+                new[] { Const(ibOutputBinding), Const(0), outputBaseIndex, Const(-1) }));
+        }
+
+        private static Operand GenerateBaseOffset(ResourceManager resourceManager, LinkedListNode<INode> node, int stride, int threadsPerInputPrimitive)
+        {
+            Operand primitiveId = Local();
+            GeneratePrimitiveId(resourceManager, node, primitiveId);
+
+            Operand baseOffset = Local();
+            node.List.AddBefore(node, new Operation(Instruction.Multiply, baseOffset, new[] { primitiveId, Const(stride) }));
+
+            Operand invocationId = Local();
+            GenerateInvocationId(node, invocationId);
+
+            Operand invocationOffset = Local();
+            node.List.AddBefore(node, new Operation(Instruction.Multiply, invocationOffset, new[] { invocationId, Const(stride / threadsPerInputPrimitive) }));
+
+            Operand combinedOffset = Local();
+            node.List.AddBefore(node, new Operation(Instruction.Add, combinedOffset, new[] { baseOffset, invocationOffset }));
+
+            return combinedOffset;
+        }
+
+        private static Operand IncrementLocalMemory(LinkedListNode<INode> node, int memoryId)
+        {
+            Operand oldValue = Local();
+            node.List.AddBefore(node, new Operation(
+                Instruction.Load,
+                StorageKind.LocalMemory,
+                oldValue,
+                new[] { Const(memoryId) }));
+
+            Operand newValue = Local();
+            node.List.AddBefore(node, new Operation(Instruction.Add, newValue, new[] { oldValue, Const(1) }));
+
+            node.List.AddBefore(node, new Operation(Instruction.Store, StorageKind.LocalMemory, null, new[] { Const(memoryId), newValue }));
+
+            return oldValue;
+        }
+
+        private static Operand GenerateVertexOffset(
+            ResourceManager resourceManager,
+            LinkedListNode<INode> node,
+            int elementOffset,
+            Operand primVertex)
+        {
+            int vertexInfoCbBinding = resourceManager.Reservations.VertexInfoConstantBufferBinding;
+
+            Operand vertexCount = Local();
+            node.List.AddBefore(node, new Operation(
+                Instruction.Load,
+                StorageKind.ConstantBuffer,
+                vertexCount,
+                new[] { Const(vertexInfoCbBinding), Const((int)VertexInfoBufferField.VertexCounts), Const(0) }));
+
+            Operand primInputVertex = Local();
+            node.List.AddBefore(node, new Operation(
+                Instruction.Load,
+                StorageKind.LocalMemory,
+                primInputVertex,
+                new[] { Const(resourceManager.LocalTopologyRemapMemoryId), primVertex }));
+
+            Operand instanceIndex = Local();
+            node.List.AddBefore(node, new Operation(
+                Instruction.Load,
+                StorageKind.Input,
+                instanceIndex,
+                new[] { Const((int)IoVariable.GlobalId), Const(1) }));
+
+            Operand baseVertex = Local();
+            node.List.AddBefore(node, new Operation(Instruction.Multiply, baseVertex, new[] { instanceIndex, vertexCount }));
+
+            Operand vertexIndex = Local();
+            node.List.AddBefore(node, new Operation(Instruction.Add, vertexIndex, new[] { baseVertex, primInputVertex }));
+
+            Operand vertexBaseOffset = Local();
+            node.List.AddBefore(node, new Operation(
+                Instruction.Multiply,
+                vertexBaseOffset,
+                new[] { vertexIndex, Const(resourceManager.Reservations.InputSizePerInvocation) }));
+
+            Operand vertexElemOffset;
+
+            if (elementOffset != 0)
+            {
+                vertexElemOffset = Local();
+
+                node.List.AddBefore(node, new Operation(Instruction.Add, vertexElemOffset, new[] { vertexBaseOffset, Const(elementOffset) }));
+            }
+            else
+            {
+                vertexElemOffset = vertexBaseOffset;
+            }
+
+            return vertexElemOffset;
+        }
+
+        private static LinkedListNode<INode> GeneratePrimitiveId(ResourceManager resourceManager, LinkedListNode<INode> node, Operand dest)
+        {
+            int vertexInfoCbBinding = resourceManager.Reservations.VertexInfoConstantBufferBinding;
+
+            Operand vertexCount = Local();
+            node.List.AddBefore(node, new Operation(
+                Instruction.Load,
+                StorageKind.ConstantBuffer,
+                vertexCount,
+                new[] { Const(vertexInfoCbBinding), Const((int)VertexInfoBufferField.VertexCounts), Const(0) }));
+
+            Operand vertexIndex = Local();
+            node.List.AddBefore(node, new Operation(
+                Instruction.Load,
+                StorageKind.Input,
+                vertexIndex,
+                new[] { Const((int)IoVariable.GlobalId), Const(0) }));
+
+            Operand instanceIndex = Local();
+            node.List.AddBefore(node, new Operation(
+                Instruction.Load,
+                StorageKind.Input,
+                instanceIndex,
+                new[] { Const((int)IoVariable.GlobalId), Const(1) }));
+
+            Operand baseVertex = Local();
+            node.List.AddBefore(node, new Operation(Instruction.Multiply, baseVertex, new[] { instanceIndex, vertexCount }));
+
+            return node.List.AddBefore(node, new Operation(Instruction.Add, dest, new[] { baseVertex, vertexIndex }));
+        }
+
+        private static LinkedListNode<INode> GenerateInvocationId(LinkedListNode<INode> node, Operand dest)
+        {
+            return node.List.AddBefore(node, new Operation(
+                Instruction.Load,
+                StorageKind.Input,
+                dest,
+                new[] { Const((int)IoVariable.GlobalId), Const(2) }));
+        }
+
+        private static bool TryGetOffset(ResourceManager resourceManager, Operation operation, StorageKind storageKind, out int outputOffset)
+        {
+            bool isStore = operation.Inst == Instruction.Store;
+
+            IoVariable ioVariable = (IoVariable)operation.GetSource(0).Value;
+
+            bool isValidOutput;
+
+            if (ioVariable == IoVariable.UserDefined)
+            {
+                int lastIndex = operation.SourcesCount - (isStore ? 2 : 1);
+
+                int location = operation.GetSource(1).Value;
+                int component = operation.GetSource(lastIndex).Value;
+
+                isValidOutput = resourceManager.Reservations.TryGetOffset(storageKind, location, component, out outputOffset);
+            }
+            else
+            {
+                if (ResourceReservations.IsVectorOrArrayVariable(ioVariable))
+                {
+                    int component = operation.GetSource(operation.SourcesCount - (isStore ? 2 : 1)).Value;
+
+                    isValidOutput = resourceManager.Reservations.TryGetOffset(storageKind, ioVariable, component, out outputOffset);
+                }
+                else
+                {
+                    isValidOutput = resourceManager.Reservations.TryGetOffset(storageKind, ioVariable, out outputOffset);
+                }
+            }
+
+            return isValidOutput;
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Transforms/ShufflePass.cs b/src/Ryujinx.Graphics.Shader/Translation/Transforms/ShufflePass.cs
new file mode 100644
index 000000000..839d4f818
--- /dev/null
+++ b/src/Ryujinx.Graphics.Shader/Translation/Transforms/ShufflePass.cs
@@ -0,0 +1,52 @@
+using Ryujinx.Graphics.Shader.IntermediateRepresentation;
+using Ryujinx.Graphics.Shader.Translation.Optimizations;
+using System.Collections.Generic;
+using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper;
+
+namespace Ryujinx.Graphics.Shader.Translation.Transforms
+{
+    class ShufflePass : ITransformPass
+    {
+        public static bool IsEnabled(IGpuAccessor gpuAccessor, ShaderStage stage, TargetLanguage targetLanguage, FeatureFlags usedFeatures)
+        {
+            return usedFeatures.HasFlag(FeatureFlags.Shuffle);
+        }
+
+        public static LinkedListNode<INode> RunPass(TransformContext context, LinkedListNode<INode> node)
+        {
+            Operation operation = (Operation)node.Value;
+
+            HelperFunctionName functionName = operation.Inst switch
+            {
+                Instruction.Shuffle => HelperFunctionName.Shuffle,
+                Instruction.ShuffleDown => HelperFunctionName.ShuffleDown,
+                Instruction.ShuffleUp => HelperFunctionName.ShuffleUp,
+                Instruction.ShuffleXor => HelperFunctionName.ShuffleXor,
+                _ => HelperFunctionName.Invalid,
+            };
+
+            if (functionName == HelperFunctionName.Invalid || operation.SourcesCount != 3 || operation.DestsCount != 2)
+            {
+                return node;
+            }
+
+            int functionId = context.Hfm.GetOrCreateShuffleFunctionId(functionName, context.GpuAccessor.QueryHostSubgroupSize());
+
+            Operand result = operation.GetDest(0);
+            Operand valid = operation.GetDest(1);
+            Operand value = operation.GetSource(0);
+            Operand index = operation.GetSource(1);
+            Operand mask = operation.GetSource(2);
+
+            operation.Dest = null;
+
+            Operand[] callArgs = new Operand[] { Const(functionId), value, index, mask, valid };
+
+            LinkedListNode<INode> newNode = node.List.AddBefore(node, new Operation(Instruction.Call, 0, result, callArgs));
+
+            Utils.DeleteNode(node, operation);
+
+            return newNode;
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs b/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs
index 5ceed4b7f..dbfe6269e 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs
@@ -23,7 +23,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
                 {
                     node = InsertCoordNormalization(context.Hfm, node, context.ResourceManager, context.GpuAccessor, context.Stage);
                     node = InsertCoordGatherBias(node, context.ResourceManager, context.GpuAccessor);
-                    node = InsertConstOffsets(node, context.ResourceManager, context.GpuAccessor);
+                    node = InsertConstOffsets(node, context.GpuAccessor, context.Stage);
 
                     if (texOp.Type == SamplerType.TextureBuffer && !context.GpuAccessor.QueryHostSupportsSnormBufferTextureFormat())
                     {
@@ -99,7 +99,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
             bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
             bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
 
-            if (texOp.Inst == Instruction.TextureSize &&
+            if (texOp.Inst == Instruction.TextureQuerySize &&
                 texOp.Index < 2 &&
                 !isBindless &&
                 !isIndexed &&
@@ -153,15 +153,13 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
 
             bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
 
-            if (isBindless)
+            if (isBindless || !resourceManager.TryGetCbufSlotAndHandleForTexture(texOp.Binding, out int cbufSlot, out int handle))
             {
                 return node;
             }
 
             bool intCoords = (texOp.Flags & TextureFlags.IntCoords) != 0;
 
-            (int cbufSlot, int handle) = resourceManager.GetCbufSlotAndHandleForTexture(texOp.Binding);
-
             bool isCoordNormalized = gpuAccessor.QueryTextureCoordNormalized(handle, cbufSlot);
 
             if (isCoordNormalized || intCoords)
@@ -192,7 +190,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
                 }
 
                 LinkedListNode<INode> textureSizeNode = node.List.AddBefore(node, new TextureOperation(
-                    Instruction.TextureSize,
+                    Instruction.TextureQuerySize,
                     texOp.Type,
                     texOp.Format,
                     texOp.Flags,
@@ -261,7 +259,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
                 }
 
                 node.List.AddBefore(node, new TextureOperation(
-                    Instruction.TextureSize,
+                    Instruction.TextureQuerySize,
                     texOp.Type,
                     texOp.Format,
                     texOp.Flags,
@@ -289,7 +287,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
             return node;
         }
 
-        private static LinkedListNode<INode> InsertConstOffsets(LinkedListNode<INode> node, ResourceManager resourceManager, IGpuAccessor gpuAccessor)
+        private static LinkedListNode<INode> InsertConstOffsets(LinkedListNode<INode> node, IGpuAccessor gpuAccessor, ShaderStage stage)
         {
             // Non-constant texture offsets are not allowed (according to the spec),
             // however some GPUs does support that.
@@ -442,7 +440,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
 
                 sources.CopyTo(newSources, 0);
 
-                Operand[] texSizes = InsertTextureLod(node, texOp, lodSources, bindlessHandle, coordsCount);
+                Operand[] texSizes = InsertTextureLod(node, texOp, lodSources, bindlessHandle, coordsCount, stage);
 
                 int destIndex = 0;
 
@@ -504,7 +502,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
                 }
                 else
                 {
-                    Operand[] texSizes = InsertTextureLod(node, texOp, lodSources, bindlessHandle, coordsCount);
+                    Operand[] texSizes = InsertTextureLod(node, texOp, lodSources, bindlessHandle, coordsCount, stage);
 
                     for (int index = 0; index < coordsCount; index++)
                     {
@@ -556,21 +554,31 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
             TextureOperation texOp,
             Operand[] lodSources,
             Operand bindlessHandle,
-            int coordsCount)
+            int coordsCount,
+            ShaderStage stage)
         {
             Operand[] texSizes = new Operand[coordsCount];
 
-            Operand lod = Local();
+            Operand lod;
 
-            node.List.AddBefore(node, new TextureOperation(
-                Instruction.Lod,
-                texOp.Type,
-                texOp.Format,
-                texOp.Flags,
-                texOp.Binding,
-                0,
-                new[] { lod },
-                lodSources));
+            if (stage == ShaderStage.Fragment)
+            {
+                lod = Local();
+
+                node.List.AddBefore(node, new TextureOperation(
+                    Instruction.Lod,
+                    texOp.Type,
+                    texOp.Format,
+                    texOp.Flags,
+                    texOp.Binding,
+                    0,
+                    new[] { lod },
+                    lodSources));
+            }
+            else
+            {
+                lod = Const(0);
+            }
 
             for (int index = 0; index < coordsCount; index++)
             {
@@ -588,7 +596,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
                 }
 
                 node.List.AddBefore(node, new TextureOperation(
-                    Instruction.TextureSize,
+                    Instruction.TextureQuerySize,
                     texOp.Type,
                     texOp.Format,
                     texOp.Flags,
@@ -607,13 +615,11 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
 
             // We can't query the format of a bindless texture,
             // because the handle is unknown, it can have any format.
-            if (texOp.Flags.HasFlag(TextureFlags.Bindless))
+            if (texOp.Flags.HasFlag(TextureFlags.Bindless) || !resourceManager.TryGetCbufSlotAndHandleForTexture(texOp.Binding, out int cbufSlot, out int handle))
             {
                 return node;
             }
 
-            (int cbufSlot, int handle) = resourceManager.GetCbufSlotAndHandleForTexture(texOp.Binding);
-
             TextureFormat format = gpuAccessor.QueryTextureFormat(handle, cbufSlot);
 
             int maxPositive = format switch
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Transforms/TransformPasses.cs b/src/Ryujinx.Graphics.Shader/Translation/Transforms/TransformPasses.cs
index c3bbe7ddf..7ff3b8bf1 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Transforms/TransformPasses.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Transforms/TransformPasses.cs
@@ -13,6 +13,9 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
             RunPass<TexturePass>(context);
             RunPass<SharedStoreSmallIntCas>(context);
             RunPass<SharedAtomicSignedCas>(context);
+            RunPass<ShufflePass>(context);
+            RunPass<VertexToCompute>(context);
+            RunPass<GeometryToCompute>(context);
         }
 
         private static void RunPass<T>(TransformContext context) where T : ITransformPass
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Transforms/VertexToCompute.cs b/src/Ryujinx.Graphics.Shader/Translation/Transforms/VertexToCompute.cs
new file mode 100644
index 000000000..d71ada865
--- /dev/null
+++ b/src/Ryujinx.Graphics.Shader/Translation/Transforms/VertexToCompute.cs
@@ -0,0 +1,364 @@
+using Ryujinx.Graphics.Shader.IntermediateRepresentation;
+using Ryujinx.Graphics.Shader.Translation.Optimizations;
+using System.Collections.Generic;
+
+using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper;
+
+namespace Ryujinx.Graphics.Shader.Translation.Transforms
+{
+    class VertexToCompute : ITransformPass
+    {
+        public static bool IsEnabled(IGpuAccessor gpuAccessor, ShaderStage stage, TargetLanguage targetLanguage, FeatureFlags usedFeatures)
+        {
+            return usedFeatures.HasFlag(FeatureFlags.VtgAsCompute);
+        }
+
+        public static LinkedListNode<INode> RunPass(TransformContext context, LinkedListNode<INode> node)
+        {
+            if (context.Definitions.Stage != ShaderStage.Vertex)
+            {
+                return node;
+            }
+
+            Operation operation = (Operation)node.Value;
+
+            LinkedListNode<INode> newNode = node;
+
+            if (operation.Inst == Instruction.Load && operation.StorageKind == StorageKind.Input)
+            {
+                Operand dest = operation.Dest;
+
+                switch ((IoVariable)operation.GetSource(0).Value)
+                {
+                    case IoVariable.BaseInstance:
+                        newNode = GenerateBaseInstanceLoad(context.ResourceManager, node, dest);
+                        break;
+                    case IoVariable.BaseVertex:
+                        newNode = GenerateBaseVertexLoad(context.ResourceManager, node, dest);
+                        break;
+                    case IoVariable.InstanceId:
+                        newNode = GenerateInstanceIdLoad(node, dest);
+                        break;
+                    case IoVariable.InstanceIndex:
+                        newNode = GenerateInstanceIndexLoad(context.ResourceManager, node, dest);
+                        break;
+                    case IoVariable.VertexId:
+                    case IoVariable.VertexIndex:
+                        newNode = GenerateVertexIndexLoad(context.ResourceManager, node, dest);
+                        break;
+                    case IoVariable.UserDefined:
+                        int location = operation.GetSource(1).Value;
+                        int component = operation.GetSource(2).Value;
+
+                        if (context.Definitions.IsAttributePacked(location))
+                        {
+                            bool needsSextNorm = context.Definitions.IsAttributePackedRgb10A2Signed(location);
+
+                            Operand temp = needsSextNorm ? Local() : dest;
+                            Operand vertexElemOffset = GenerateVertexOffset(context.ResourceManager, node, location, 0);
+
+                            newNode = node.List.AddBefore(node, new TextureOperation(
+                                Instruction.TextureSample,
+                                SamplerType.TextureBuffer,
+                                TextureFormat.Unknown,
+                                TextureFlags.IntCoords,
+                                context.ResourceManager.Reservations.GetVertexBufferTextureBinding(location),
+                                1 << component,
+                                new[] { temp },
+                                new[] { vertexElemOffset }));
+
+                            if (needsSextNorm)
+                            {
+                                bool sint = context.Definitions.IsAttributeSint(location);
+                                CopySignExtendedNormalized(node, component == 3 ? 2 : 10, !sint, dest, temp);
+                            }
+                        }
+                        else
+                        {
+                            Operand temp = component > 0 ? Local() : dest;
+                            Operand vertexElemOffset = GenerateVertexOffset(context.ResourceManager, node, location, component);
+
+                            newNode = node.List.AddBefore(node, new TextureOperation(
+                                Instruction.TextureSample,
+                                SamplerType.TextureBuffer,
+                                TextureFormat.Unknown,
+                                TextureFlags.IntCoords,
+                                context.ResourceManager.Reservations.GetVertexBufferTextureBinding(location),
+                                1,
+                                new[] { temp },
+                                new[] { vertexElemOffset }));
+
+                            if (component > 0)
+                            {
+                                newNode = CopyMasked(context.ResourceManager, newNode, location, component, dest, temp);
+                            }
+                        }
+                        break;
+                    case IoVariable.GlobalId:
+                    case IoVariable.SubgroupEqMask:
+                    case IoVariable.SubgroupGeMask:
+                    case IoVariable.SubgroupGtMask:
+                    case IoVariable.SubgroupLaneId:
+                    case IoVariable.SubgroupLeMask:
+                    case IoVariable.SubgroupLtMask:
+                        // Those are valid or expected for vertex shaders.
+                        break;
+                    default:
+                        context.GpuAccessor.Log($"Invalid input \"{(IoVariable)operation.GetSource(0).Value}\".");
+                        break;
+                }
+            }
+            else if (operation.Inst == Instruction.Load && operation.StorageKind == StorageKind.Output)
+            {
+                if (TryGetOutputOffset(context.ResourceManager, operation, out int outputOffset))
+                {
+                    newNode = node.List.AddBefore(node, new Operation(
+                        Instruction.Load,
+                        StorageKind.LocalMemory,
+                        operation.Dest,
+                        new[] { Const(context.ResourceManager.LocalVertexDataMemoryId), Const(outputOffset) }));
+                }
+                else
+                {
+                    context.GpuAccessor.Log($"Invalid output \"{(IoVariable)operation.GetSource(0).Value}\".");
+                }
+            }
+            else if (operation.Inst == Instruction.Store && operation.StorageKind == StorageKind.Output)
+            {
+                if (TryGetOutputOffset(context.ResourceManager, operation, out int outputOffset))
+                {
+                    Operand value = operation.GetSource(operation.SourcesCount - 1);
+
+                    newNode = node.List.AddBefore(node, new Operation(
+                        Instruction.Store,
+                        StorageKind.LocalMemory,
+                        (Operand)null,
+                        new[] { Const(context.ResourceManager.LocalVertexDataMemoryId), Const(outputOffset), value }));
+                }
+                else
+                {
+                    context.GpuAccessor.Log($"Invalid output \"{(IoVariable)operation.GetSource(0).Value}\".");
+                }
+            }
+
+            if (newNode != node)
+            {
+                Utils.DeleteNode(node, operation);
+            }
+
+            return newNode;
+        }
+
+        private static Operand GenerateVertexOffset(ResourceManager resourceManager, LinkedListNode<INode> node, int location, int component)
+        {
+            int vertexInfoCbBinding = resourceManager.Reservations.VertexInfoConstantBufferBinding;
+
+            Operand vertexIdVr = Local();
+            GenerateVertexIdVertexRateLoad(resourceManager, node, vertexIdVr);
+
+            Operand vertexIdIr = Local();
+            GenerateVertexIdInstanceRateLoad(resourceManager, node, vertexIdIr);
+
+            Operand attributeOffset = Local();
+            node.List.AddBefore(node, new Operation(
+                Instruction.Load,
+                StorageKind.ConstantBuffer,
+                attributeOffset,
+                new[] { Const(vertexInfoCbBinding), Const((int)VertexInfoBufferField.VertexOffsets), Const(location), Const(0) }));
+
+            Operand isInstanceRate = Local();
+            node.List.AddBefore(node, new Operation(
+                Instruction.Load,
+                StorageKind.ConstantBuffer,
+                isInstanceRate,
+                new[] { Const(vertexInfoCbBinding), Const((int)VertexInfoBufferField.VertexOffsets), Const(location), Const(1) }));
+
+            Operand vertexId = Local();
+            node.List.AddBefore(node, new Operation(
+                Instruction.ConditionalSelect,
+                vertexId,
+                new[] { isInstanceRate, vertexIdIr, vertexIdVr }));
+
+            Operand vertexStride = Local();
+            node.List.AddBefore(node, new Operation(
+                Instruction.Load,
+                StorageKind.ConstantBuffer,
+                vertexStride,
+                new[] { Const(vertexInfoCbBinding), Const((int)VertexInfoBufferField.VertexStrides), Const(location), Const(0) }));
+
+            Operand vertexBaseOffset = Local();
+            node.List.AddBefore(node, new Operation(Instruction.Multiply, vertexBaseOffset, new[] { vertexId, vertexStride }));
+
+            Operand vertexOffset = Local();
+            node.List.AddBefore(node, new Operation(Instruction.Add, vertexOffset, new[] { attributeOffset, vertexBaseOffset }));
+
+            Operand vertexElemOffset;
+
+            if (component != 0)
+            {
+                vertexElemOffset = Local();
+
+                node.List.AddBefore(node, new Operation(Instruction.Add, vertexElemOffset, new[] { vertexOffset, Const(component) }));
+            }
+            else
+            {
+                vertexElemOffset = vertexOffset;
+            }
+
+            return vertexElemOffset;
+        }
+
+        private static LinkedListNode<INode> CopySignExtendedNormalized(LinkedListNode<INode> node, int bits, bool normalize, Operand dest, Operand src)
+        {
+            Operand leftShifted = Local();
+            node = node.List.AddAfter(node, new Operation(
+                Instruction.ShiftLeft,
+                leftShifted,
+                new[] { src, Const(32 - bits) }));
+
+            Operand rightShifted = normalize ? Local() : dest;
+            node = node.List.AddAfter(node, new Operation(
+                Instruction.ShiftRightS32,
+                rightShifted,
+                new[] { leftShifted, Const(32 - bits) }));
+
+            if (normalize)
+            {
+                Operand asFloat = Local();
+                node = node.List.AddAfter(node, new Operation(Instruction.ConvertS32ToFP32, asFloat, new[] { rightShifted }));
+                node = node.List.AddAfter(node, new Operation(
+                    Instruction.FP32 | Instruction.Multiply,
+                    dest,
+                    new[] { asFloat, ConstF(1f / (1 << (bits - 1))) }));
+            }
+
+            return node;
+        }
+
+        private static LinkedListNode<INode> CopyMasked(
+            ResourceManager resourceManager,
+            LinkedListNode<INode> node,
+            int location,
+            int component,
+            Operand dest,
+            Operand src)
+        {
+            Operand componentExists = Local();
+            int vertexInfoCbBinding = resourceManager.Reservations.VertexInfoConstantBufferBinding;
+            node = node.List.AddAfter(node, new Operation(
+                Instruction.Load,
+                StorageKind.ConstantBuffer,
+                componentExists,
+                new[] { Const(vertexInfoCbBinding), Const((int)VertexInfoBufferField.VertexStrides), Const(location), Const(component) }));
+
+            return node.List.AddAfter(node, new Operation(
+                Instruction.ConditionalSelect,
+                dest,
+                new[] { componentExists, src, ConstF(component == 3 ? 1f : 0f) }));
+        }
+
+        private static LinkedListNode<INode> GenerateBaseVertexLoad(ResourceManager resourceManager, LinkedListNode<INode> node, Operand dest)
+        {
+            int vertexInfoCbBinding = resourceManager.Reservations.VertexInfoConstantBufferBinding;
+
+            return node.List.AddBefore(node, new Operation(
+                Instruction.Load,
+                StorageKind.ConstantBuffer,
+                dest,
+                new[] { Const(vertexInfoCbBinding), Const((int)VertexInfoBufferField.VertexCounts), Const(2) }));
+        }
+
+        private static LinkedListNode<INode> GenerateBaseInstanceLoad(ResourceManager resourceManager, LinkedListNode<INode> node, Operand dest)
+        {
+            int vertexInfoCbBinding = resourceManager.Reservations.VertexInfoConstantBufferBinding;
+
+            return node.List.AddBefore(node, new Operation(
+                Instruction.Load,
+                StorageKind.ConstantBuffer,
+                dest,
+                new[] { Const(vertexInfoCbBinding), Const((int)VertexInfoBufferField.VertexCounts), Const(3) }));
+        }
+
+        private static LinkedListNode<INode> GenerateVertexIndexLoad(ResourceManager resourceManager, LinkedListNode<INode> node, Operand dest)
+        {
+            Operand baseVertex = Local();
+            Operand vertexId = Local();
+
+            GenerateBaseVertexLoad(resourceManager, node, baseVertex);
+            GenerateVertexIdVertexRateLoad(resourceManager, node, vertexId);
+
+            return node.List.AddBefore(node, new Operation(Instruction.Add, dest, new[] { baseVertex, vertexId }));
+        }
+
+        private static LinkedListNode<INode> GenerateInstanceIndexLoad(ResourceManager resourceManager, LinkedListNode<INode> node, Operand dest)
+        {
+            Operand baseInstance = Local();
+            Operand instanceId = Local();
+
+            GenerateBaseInstanceLoad(resourceManager, node, baseInstance);
+
+            node.List.AddBefore(node, new Operation(
+                Instruction.Load,
+                StorageKind.Input,
+                instanceId,
+                new[] { Const((int)IoVariable.GlobalId), Const(1) }));
+
+            return node.List.AddBefore(node, new Operation(Instruction.Add, dest, new[] { baseInstance, instanceId }));
+        }
+
+        private static LinkedListNode<INode> GenerateVertexIdVertexRateLoad(ResourceManager resourceManager, LinkedListNode<INode> node, Operand dest)
+        {
+            Operand[] sources = new Operand[] { Const(resourceManager.LocalVertexIndexVertexRateMemoryId) };
+
+            return node.List.AddBefore(node, new Operation(Instruction.Load, StorageKind.LocalMemory, dest, sources));
+        }
+
+        private static LinkedListNode<INode> GenerateVertexIdInstanceRateLoad(ResourceManager resourceManager, LinkedListNode<INode> node, Operand dest)
+        {
+            Operand[] sources = new Operand[] { Const(resourceManager.LocalVertexIndexInstanceRateMemoryId) };
+
+            return node.List.AddBefore(node, new Operation(Instruction.Load, StorageKind.LocalMemory, dest, sources));
+        }
+
+        private static LinkedListNode<INode> GenerateInstanceIdLoad(LinkedListNode<INode> node, Operand dest)
+        {
+            Operand[] sources = new Operand[] { Const((int)IoVariable.GlobalId), Const(1) };
+
+            return node.List.AddBefore(node, new Operation(Instruction.Load, StorageKind.Input, dest, sources));
+        }
+
+        private static bool TryGetOutputOffset(ResourceManager resourceManager, Operation operation, out int outputOffset)
+        {
+            bool isStore = operation.Inst == Instruction.Store;
+
+            IoVariable ioVariable = (IoVariable)operation.GetSource(0).Value;
+
+            bool isValidOutput;
+
+            if (ioVariable == IoVariable.UserDefined)
+            {
+                int lastIndex = operation.SourcesCount - (isStore ? 2 : 1);
+
+                int location = operation.GetSource(1).Value;
+                int component = operation.GetSource(lastIndex).Value;
+
+                isValidOutput = resourceManager.Reservations.TryGetOffset(StorageKind.Output, location, component, out outputOffset);
+            }
+            else
+            {
+                if (ResourceReservations.IsVectorOrArrayVariable(ioVariable))
+                {
+                    int component = operation.GetSource(operation.SourcesCount - (isStore ? 2 : 1)).Value;
+
+                    isValidOutput = resourceManager.Reservations.TryGetOffset(StorageKind.Output, ioVariable, component, out outputOffset);
+                }
+                else
+                {
+                    isValidOutput = resourceManager.Reservations.TryGetOffset(StorageKind.Output, ioVariable, out outputOffset);
+                }
+            }
+
+            return isValidOutput;
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Translator.cs b/src/Ryujinx.Graphics.Shader/Translation/Translator.cs
index 93a70ace8..6a31ea2e7 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Translator.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Translator.cs
@@ -77,12 +77,32 @@ namespace Ryujinx.Graphics.Shader.Translation
         }
 
         private static ShaderDefinitions CreateGraphicsDefinitions(IGpuAccessor gpuAccessor, ShaderHeader header)
+        {
+            TransformFeedbackOutput[] transformFeedbackOutputs = GetTransformFeedbackOutputs(gpuAccessor, out ulong transformFeedbackVecMap);
+
+            return new ShaderDefinitions(
+                header.Stage,
+                gpuAccessor.QueryGraphicsState(),
+                header.Stage == ShaderStage.Geometry && header.GpPassthrough,
+                header.ThreadsPerInputPrimitive,
+                header.OutputTopology,
+                header.MaxOutputVertexCount,
+                header.ImapTypes,
+                header.OmapTargets,
+                header.OmapSampleMask,
+                header.OmapDepth,
+                gpuAccessor.QueryHostSupportsScaledVertexFormats(),
+                transformFeedbackVecMap,
+                transformFeedbackOutputs);
+        }
+
+        internal static TransformFeedbackOutput[] GetTransformFeedbackOutputs(IGpuAccessor gpuAccessor, out ulong transformFeedbackVecMap)
         {
             bool transformFeedbackEnabled =
                 gpuAccessor.QueryTransformFeedbackEnabled() &&
                 gpuAccessor.QueryHostSupportsTransformFeedback();
             TransformFeedbackOutput[] transformFeedbackOutputs = null;
-            ulong transformFeedbackVecMap = 0UL;
+            transformFeedbackVecMap = 0UL;
 
             if (transformFeedbackEnabled)
             {
@@ -105,21 +125,7 @@ namespace Ryujinx.Graphics.Shader.Translation
                 }
             }
 
-            return new ShaderDefinitions(
-                header.Stage,
-                gpuAccessor.QueryGraphicsState(),
-                header.Stage == ShaderStage.Geometry && header.GpPassthrough,
-                header.ThreadsPerInputPrimitive,
-                header.OutputTopology,
-                header.MaxOutputVertexCount,
-                header.ImapTypes,
-                header.OmapTargets,
-                header.OmapSampleMask,
-                header.OmapDepth,
-                gpuAccessor.QueryHostSupportsScaledVertexFormats(),
-                transformFeedbackEnabled,
-                transformFeedbackVecMap,
-                transformFeedbackOutputs);
+            return transformFeedbackOutputs;
         }
 
         private static int GetLocalMemorySize(ShaderHeader header)
@@ -131,6 +137,7 @@ namespace Ryujinx.Graphics.Shader.Translation
             TranslatorContext translatorContext,
             ResourceManager resourceManager,
             DecodedProgram program,
+            bool vertexAsCompute,
             bool initializeOutputs,
             out int initializationOperations)
         {
@@ -147,7 +154,7 @@ namespace Ryujinx.Graphics.Shader.Translation
 
             for (int index = 0; index < functions.Length; index++)
             {
-                EmitterContext context = new(translatorContext, resourceManager, program, index != 0);
+                EmitterContext context = new(translatorContext, resourceManager, program, vertexAsCompute, index != 0);
 
                 if (initializeOutputs && index == 0)
                 {
diff --git a/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs b/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs
index 39ce92c9d..a112991e9 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs
@@ -8,7 +8,6 @@ using Ryujinx.Graphics.Shader.Translation.Optimizations;
 using Ryujinx.Graphics.Shader.Translation.Transforms;
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using System.Numerics;
 using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper;
 using static Ryujinx.Graphics.Shader.Translation.Translator;
@@ -19,14 +18,12 @@ namespace Ryujinx.Graphics.Shader.Translation
     {
         private readonly DecodedProgram _program;
         private readonly int _localMemorySize;
+        private IoUsage _vertexOutput;
 
         public ulong Address { get; }
         public int Size { get; }
         public int Cb1DataSize => _program.Cb1DataSize;
 
-        internal bool HasLayerInputAttribute { get; private set; }
-        internal int GpLayerInputAttribute { get; private set; }
-
         internal AttributeUsage AttributeUsage => _program.AttributeUsage;
 
         internal ShaderDefinitions Definitions { get; }
@@ -37,7 +34,8 @@ namespace Ryujinx.Graphics.Shader.Translation
 
         internal TranslationOptions Options { get; }
 
-        internal FeatureFlags UsedFeatures { get; private set; }
+        private bool IsTransformFeedbackEmulated => !GpuAccessor.QueryHostSupportsTransformFeedback() && GpuAccessor.QueryTransformFeedbackEnabled();
+        public bool HasStore => _program.UsedFeatures.HasFlag(FeatureFlags.Store) || (IsTransformFeedbackEmulated && Definitions.LastInVertexPipeline);
 
         public bool LayerOutputWritten { get; private set; }
         public int LayerOutputAttribute { get; private set; }
@@ -55,10 +53,10 @@ namespace Ryujinx.Graphics.Shader.Translation
             Size = size;
             _program = program;
             _localMemorySize = localMemorySize;
+            _vertexOutput = new IoUsage(FeatureFlags.None, 0, -1);
             Definitions = definitions;
             GpuAccessor = gpuAccessor;
             Options = options;
-            UsedFeatures = program.UsedFeatures;
         }
 
         private static bool IsLoadUserDefined(Operation operation)
@@ -171,13 +169,6 @@ namespace Ryujinx.Graphics.Shader.Translation
             LayerOutputAttribute = attr;
         }
 
-        public void SetGeometryShaderLayerInputAttribute(int attr)
-        {
-            UsedFeatures |= FeatureFlags.RtLayer;
-            HasLayerInputAttribute = true;
-            GpLayerInputAttribute = attr;
-        }
-
         public void SetLastInVertexPipeline()
         {
             Definitions.LastInVertexPipeline = true;
@@ -187,7 +178,7 @@ namespace Ryujinx.Graphics.Shader.Translation
         {
             AttributeUsage.MergeFromtNextStage(
                 Definitions.GpPassthrough,
-                nextStage.UsedFeatures.HasFlag(FeatureFlags.FixedFuncAttr),
+                nextStage._program.UsedFeatures.HasFlag(FeatureFlags.FixedFuncAttr),
                 nextStage.AttributeUsage);
 
             // We don't consider geometry shaders using the geometry shader passthrough feature
@@ -200,9 +191,9 @@ namespace Ryujinx.Graphics.Shader.Translation
             }
         }
 
-        public ShaderProgram Translate()
+        public ShaderProgram Translate(bool asCompute = false)
         {
-            ResourceManager resourceManager = CreateResourceManager();
+            ResourceManager resourceManager = CreateResourceManager(asCompute);
 
             bool usesLocalMemory = _program.UsedFeatures.HasFlag(FeatureFlags.LocalMemory);
 
@@ -215,36 +206,42 @@ namespace Ryujinx.Graphics.Shader.Translation
                 resourceManager.SetCurrentSharedMemory(GpuAccessor.QueryComputeSharedMemorySize(), usesSharedMemory);
             }
 
-            FunctionCode[] code = EmitShader(this, resourceManager, _program, initializeOutputs: true, out _);
+            FunctionCode[] code = EmitShader(this, resourceManager, _program, asCompute, initializeOutputs: true, out _);
 
-            return Translate(code, resourceManager, UsedFeatures, _program.ClipDistancesWritten);
+            return Translate(code, resourceManager, _program.UsedFeatures, _program.ClipDistancesWritten, asCompute);
         }
 
-        public ShaderProgram Translate(TranslatorContext other)
+        public ShaderProgram Translate(TranslatorContext other, bool asCompute = false)
         {
-            ResourceManager resourceManager = CreateResourceManager();
+            ResourceManager resourceManager = CreateResourceManager(asCompute);
 
             bool usesLocalMemory = _program.UsedFeatures.HasFlag(FeatureFlags.LocalMemory);
             resourceManager.SetCurrentLocalMemory(_localMemorySize, usesLocalMemory);
 
-            FunctionCode[] code = EmitShader(this, resourceManager, _program, initializeOutputs: false, out _);
+            FunctionCode[] code = EmitShader(this, resourceManager, _program, asCompute, initializeOutputs: false, out _);
 
             bool otherUsesLocalMemory = other._program.UsedFeatures.HasFlag(FeatureFlags.LocalMemory);
             resourceManager.SetCurrentLocalMemory(other._localMemorySize, otherUsesLocalMemory);
 
-            FunctionCode[] otherCode = EmitShader(other, resourceManager, other._program, initializeOutputs: true, out int aStart);
+            FunctionCode[] otherCode = EmitShader(other, resourceManager, other._program, asCompute, initializeOutputs: true, out int aStart);
 
             code = Combine(otherCode, code, aStart);
 
             return Translate(
                 code,
                 resourceManager,
-                UsedFeatures | other.UsedFeatures,
-                (byte)(_program.ClipDistancesWritten | other._program.ClipDistancesWritten));
+                _program.UsedFeatures | other._program.UsedFeatures,
+                (byte)(_program.ClipDistancesWritten | other._program.ClipDistancesWritten),
+                asCompute);
         }
 
-        private ShaderProgram Translate(FunctionCode[] functions, ResourceManager resourceManager, FeatureFlags usedFeatures, byte clipDistancesWritten)
+        private ShaderProgram Translate(FunctionCode[] functions, ResourceManager resourceManager, FeatureFlags usedFeatures, byte clipDistancesWritten, bool asCompute)
         {
+            if (asCompute)
+            {
+                usedFeatures |= FeatureFlags.VtgAsCompute;
+            }
+
             var cfgs = new ControlFlowGraph[functions.Length];
             var frus = new RegisterUsage.FunctionRegisterUsage[functions.Length];
 
@@ -294,6 +291,7 @@ namespace Ryujinx.Graphics.Shader.Translation
                     TransformContext context = new(
                         hfm,
                         cfg.Blocks,
+                        Definitions,
                         resourceManager,
                         GpuAccessor,
                         Options.TargetLanguage,
@@ -307,50 +305,55 @@ namespace Ryujinx.Graphics.Shader.Translation
                 funcs[i] = new Function(cfg.Blocks, $"fun{i}", false, inArgumentsCount, outArgumentsCount);
             }
 
-            var identification = ShaderIdentifier.Identify(funcs, GpuAccessor, Definitions.Stage, Definitions.InputTopology, out int layerInputAttr);
-
             return Generate(
                 funcs,
                 AttributeUsage,
+                GetDefinitions(asCompute),
                 Definitions,
                 resourceManager,
                 usedFeatures,
-                clipDistancesWritten,
-                identification,
-                layerInputAttr);
+                clipDistancesWritten);
         }
 
         private ShaderProgram Generate(
             IReadOnlyList<Function> funcs,
             AttributeUsage attributeUsage,
             ShaderDefinitions definitions,
+            ShaderDefinitions originalDefinitions,
             ResourceManager resourceManager,
             FeatureFlags usedFeatures,
-            byte clipDistancesWritten,
-            ShaderIdentification identification = ShaderIdentification.None,
-            int layerInputAttr = 0)
+            byte clipDistancesWritten)
         {
             var sInfo = StructuredProgram.MakeStructuredProgram(
                 funcs,
                 attributeUsage,
                 definitions,
                 resourceManager,
+                Options.TargetLanguage,
                 Options.Flags.HasFlag(TranslationFlags.DebugMode));
 
+            int geometryVerticesPerPrimitive = Definitions.OutputTopology switch
+            {
+                OutputTopology.LineStrip => 2,
+                OutputTopology.TriangleStrip => 3,
+                _ => 1
+            };
+
             var info = new ShaderProgramInfo(
                 resourceManager.GetConstantBufferDescriptors(),
                 resourceManager.GetStorageBufferDescriptors(),
                 resourceManager.GetTextureDescriptors(),
                 resourceManager.GetImageDescriptors(),
-                identification,
-                layerInputAttr,
-                definitions.Stage,
+                originalDefinitions.Stage,
+                geometryVerticesPerPrimitive,
+                originalDefinitions.MaxOutputVertices,
+                originalDefinitions.ThreadsPerInputPrimitive,
                 usedFeatures.HasFlag(FeatureFlags.FragCoordXY),
                 usedFeatures.HasFlag(FeatureFlags.InstanceId),
                 usedFeatures.HasFlag(FeatureFlags.DrawParameters),
                 usedFeatures.HasFlag(FeatureFlags.RtLayer),
                 clipDistancesWritten,
-                definitions.OmapTargets);
+                originalDefinitions.OmapTargets);
 
             var hostCapabilities = new HostCapabilities(
                 GpuAccessor.QueryHostReducedPrecision(),
@@ -372,42 +375,213 @@ namespace Ryujinx.Graphics.Shader.Translation
             };
         }
 
-        private ResourceManager CreateResourceManager()
+        private ResourceManager CreateResourceManager(bool vertexAsCompute)
         {
-            ResourceManager resourceManager = new(Definitions.Stage, GpuAccessor);
+            ResourceManager resourceManager = new(Definitions.Stage, GpuAccessor, GetResourceReservations());
 
-            if (!GpuAccessor.QueryHostSupportsTransformFeedback() && GpuAccessor.QueryTransformFeedbackEnabled())
+            if (IsTransformFeedbackEmulated)
             {
-                StructureType tfeInfoStruct = new(new StructureField[]
-                {
-                    new StructureField(AggregateType.Array | AggregateType.U32, "base_offset", 4),
-                    new StructureField(AggregateType.U32, "vertex_count")
-                });
-
-                BufferDefinition tfeInfoBuffer = new(BufferLayout.Std430, 1, Constants.TfeInfoBinding, "tfe_info", tfeInfoStruct);
-                resourceManager.Properties.AddOrUpdateStorageBuffer(tfeInfoBuffer);
-
                 StructureType tfeDataStruct = new(new StructureField[]
                 {
                     new StructureField(AggregateType.Array | AggregateType.U32, "data", 0)
                 });
 
-                for (int i = 0; i < Constants.TfeBuffersCount; i++)
+                for (int i = 0; i < ResourceReservations.TfeBuffersCount; i++)
                 {
-                    int binding = Constants.TfeBufferBaseBinding + i;
+                    int binding = resourceManager.Reservations.GetTfeBufferStorageBufferBinding(i);
                     BufferDefinition tfeDataBuffer = new(BufferLayout.Std430, 1, binding, $"tfe_data{i}", tfeDataStruct);
                     resourceManager.Properties.AddOrUpdateStorageBuffer(tfeDataBuffer);
                 }
             }
 
+            if (vertexAsCompute)
+            {
+                int vertexInfoCbBinding = resourceManager.Reservations.VertexInfoConstantBufferBinding;
+                BufferDefinition vertexInfoBuffer = new(BufferLayout.Std140, 0, vertexInfoCbBinding, "vb_info", VertexInfoBuffer.GetStructureType());
+                resourceManager.Properties.AddOrUpdateConstantBuffer(vertexInfoBuffer);
+
+                StructureType vertexOutputStruct = new(new StructureField[]
+                {
+                    new StructureField(AggregateType.Array | AggregateType.FP32, "data", 0)
+                });
+
+                int vertexOutputSbBinding = resourceManager.Reservations.VertexOutputStorageBufferBinding;
+                BufferDefinition vertexOutputBuffer = new(BufferLayout.Std430, 1, vertexOutputSbBinding, "vertex_output", vertexOutputStruct);
+                resourceManager.Properties.AddOrUpdateStorageBuffer(vertexOutputBuffer);
+
+                if (Stage == ShaderStage.Vertex)
+                {
+                    int ibBinding = resourceManager.Reservations.IndexBufferTextureBinding;
+                    TextureDefinition indexBuffer = new(2, ibBinding, "ib_data", SamplerType.TextureBuffer, TextureFormat.Unknown, TextureUsageFlags.None);
+                    resourceManager.Properties.AddOrUpdateTexture(indexBuffer);
+
+                    int inputMap = _program.AttributeUsage.UsedInputAttributes;
+
+                    while (inputMap != 0)
+                    {
+                        int location = BitOperations.TrailingZeroCount(inputMap);
+                        int binding = resourceManager.Reservations.GetVertexBufferTextureBinding(location);
+                        TextureDefinition vaBuffer = new(2, binding, $"vb_data{location}", SamplerType.TextureBuffer, TextureFormat.Unknown, TextureUsageFlags.None);
+                        resourceManager.Properties.AddOrUpdateTexture(vaBuffer);
+
+                        inputMap &= ~(1 << location);
+                    }
+                }
+                else if (Stage == ShaderStage.Geometry)
+                {
+                    int trbBinding = resourceManager.Reservations.TopologyRemapBufferTextureBinding;
+                    TextureDefinition remapBuffer = new(2, trbBinding, "trb_data", SamplerType.TextureBuffer, TextureFormat.Unknown, TextureUsageFlags.None);
+                    resourceManager.Properties.AddOrUpdateTexture(remapBuffer);
+
+                    int geometryVbOutputSbBinding = resourceManager.Reservations.GeometryVertexOutputStorageBufferBinding;
+                    BufferDefinition geometryVbOutputBuffer = new(BufferLayout.Std430, 1, geometryVbOutputSbBinding, "geometry_vb_output", vertexOutputStruct);
+                    resourceManager.Properties.AddOrUpdateStorageBuffer(geometryVbOutputBuffer);
+
+                    StructureType geometryIbOutputStruct = new(new StructureField[]
+                    {
+                        new StructureField(AggregateType.Array | AggregateType.U32, "data", 0)
+                    });
+
+                    int geometryIbOutputSbBinding = resourceManager.Reservations.GeometryIndexOutputStorageBufferBinding;
+                    BufferDefinition geometryIbOutputBuffer = new(BufferLayout.Std430, 1, geometryIbOutputSbBinding, "geometry_ib_output", geometryIbOutputStruct);
+                    resourceManager.Properties.AddOrUpdateStorageBuffer(geometryIbOutputBuffer);
+                }
+
+                resourceManager.SetVertexAsComputeLocalMemories(Definitions.Stage, Definitions.InputTopology);
+            }
+
             return resourceManager;
         }
 
+        private ShaderDefinitions GetDefinitions(bool vertexAsCompute)
+        {
+            if (vertexAsCompute)
+            {
+                return new ShaderDefinitions(ShaderStage.Compute, 32, 32, 1);
+            }
+            else
+            {
+                return Definitions;
+            }
+        }
+
+        public ResourceReservations GetResourceReservations()
+        {
+            IoUsage ioUsage = _program.GetIoUsage();
+
+            if (Definitions.GpPassthrough)
+            {
+                ioUsage = ioUsage.Combine(_vertexOutput);
+            }
+
+            return new ResourceReservations(GpuAccessor, IsTransformFeedbackEmulated, vertexAsCompute: true, _vertexOutput, ioUsage);
+        }
+
+        public void SetVertexOutputMapForGeometryAsCompute(TranslatorContext vertexContext)
+        {
+            _vertexOutput = vertexContext._program.GetIoUsage();
+        }
+
+        public ShaderProgram GenerateVertexPassthroughForCompute()
+        {
+            var attributeUsage = new AttributeUsage(GpuAccessor);
+            var resourceManager = new ResourceManager(ShaderStage.Vertex, GpuAccessor);
+
+            var reservations = GetResourceReservations();
+
+            int vertexInfoCbBinding = reservations.VertexInfoConstantBufferBinding;
+
+            if (Stage == ShaderStage.Vertex)
+            {
+                BufferDefinition vertexInfoBuffer = new(BufferLayout.Std140, 0, vertexInfoCbBinding, "vb_info", VertexInfoBuffer.GetStructureType());
+                resourceManager.Properties.AddOrUpdateConstantBuffer(vertexInfoBuffer);
+            }
+
+            StructureType vertexInputStruct = new(new StructureField[]
+            {
+                new StructureField(AggregateType.Array | AggregateType.FP32, "data", 0)
+            });
+
+            int vertexDataSbBinding = reservations.VertexOutputStorageBufferBinding;
+            BufferDefinition vertexOutputBuffer = new(BufferLayout.Std430, 1, vertexDataSbBinding, "vb_input", vertexInputStruct);
+            resourceManager.Properties.AddOrUpdateStorageBuffer(vertexOutputBuffer);
+
+            var context = new EmitterContext();
+
+            Operand vertexIndex = Options.TargetApi == TargetApi.OpenGL
+                ? context.Load(StorageKind.Input, IoVariable.VertexId)
+                : context.Load(StorageKind.Input, IoVariable.VertexIndex);
+
+            if (Stage == ShaderStage.Vertex)
+            {
+                Operand vertexCount = context.Load(StorageKind.ConstantBuffer, vertexInfoCbBinding, Const((int)VertexInfoBufferField.VertexCounts), Const(0));
+
+                // Base instance will be always zero when this shader is used, so which one we use here doesn't really matter.
+                Operand instanceId = Options.TargetApi == TargetApi.OpenGL
+                    ? context.Load(StorageKind.Input, IoVariable.InstanceId)
+                    : context.Load(StorageKind.Input, IoVariable.InstanceIndex);
+
+                vertexIndex = context.IAdd(context.IMultiply(instanceId, vertexCount), vertexIndex);
+            }
+
+            Operand baseOffset = context.IMultiply(vertexIndex, Const(reservations.OutputSizePerInvocation));
+
+            foreach ((IoDefinition ioDefinition, int inputOffset) in reservations.Offsets)
+            {
+                if (ioDefinition.StorageKind != StorageKind.Output)
+                {
+                    continue;
+                }
+
+                Operand vertexOffset = inputOffset != 0 ? context.IAdd(baseOffset, Const(inputOffset)) : baseOffset;
+                Operand value = context.Load(StorageKind.StorageBuffer, vertexDataSbBinding, Const(0), vertexOffset);
+
+                if (ioDefinition.IoVariable == IoVariable.UserDefined)
+                {
+                    context.Store(StorageKind.Output, ioDefinition.IoVariable, null, Const(ioDefinition.Location), Const(ioDefinition.Component), value);
+                    attributeUsage.SetOutputUserAttribute(ioDefinition.Location);
+                }
+                else if (ResourceReservations.IsVectorOrArrayVariable(ioDefinition.IoVariable))
+                {
+                    context.Store(StorageKind.Output, ioDefinition.IoVariable, null, Const(ioDefinition.Component), value);
+                }
+                else
+                {
+                    context.Store(StorageKind.Output, ioDefinition.IoVariable, null, value);
+                }
+            }
+
+            var operations = context.GetOperations();
+            var cfg = ControlFlowGraph.Create(operations);
+            var function = new Function(cfg.Blocks, "main", false, 0, 0);
+
+            var transformFeedbackOutputs = GetTransformFeedbackOutputs(GpuAccessor, out ulong transformFeedbackVecMap);
+
+            var definitions = new ShaderDefinitions(ShaderStage.Vertex, transformFeedbackVecMap, transformFeedbackOutputs)
+            {
+                LastInVertexPipeline = true
+            };
+
+            return Generate(
+                new[] { function },
+                attributeUsage,
+                definitions,
+                definitions,
+                resourceManager,
+                FeatureFlags.None,
+                0);
+        }
+
         public ShaderProgram GenerateGeometryPassthrough()
         {
             int outputAttributesMask = AttributeUsage.UsedOutputAttributes;
             int layerOutputAttr = LayerOutputAttribute;
 
+            if (LayerOutputWritten)
+            {
+                outputAttributesMask |= 1 << ((layerOutputAttr - AttributeConsts.UserAttributeBase) / 16);
+            }
+
             OutputTopology outputTopology;
             int maxOutputVertices;
 
@@ -484,7 +658,14 @@ namespace Ryujinx.Graphics.Shader.Translation
                 outputTopology,
                 maxOutputVertices);
 
-            return Generate(new[] { function }, attributeUsage, definitions, resourceManager, FeatureFlags.RtLayer, 0);
+            return Generate(
+                new[] { function },
+                attributeUsage,
+                definitions,
+                definitions,
+                resourceManager,
+                FeatureFlags.RtLayer,
+                0);
         }
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/VertexInfoBuffer.cs b/src/Ryujinx.Graphics.Shader/VertexInfoBuffer.cs
new file mode 100644
index 000000000..845135f86
--- /dev/null
+++ b/src/Ryujinx.Graphics.Shader/VertexInfoBuffer.cs
@@ -0,0 +1,59 @@
+using Ryujinx.Common.Memory;
+using Ryujinx.Graphics.Shader.StructuredIr;
+using Ryujinx.Graphics.Shader.Translation;
+using System.Runtime.CompilerServices;
+
+namespace Ryujinx.Graphics.Shader
+{
+    enum VertexInfoBufferField
+    {
+        // Must match the order of the fields on the struct.
+        VertexCounts,
+        GeometryCounts,
+        VertexStrides,
+        VertexOffsets,
+    }
+
+    public struct VertexInfoBuffer
+    {
+        public static readonly int RequiredSize;
+
+        public static readonly int VertexCountsOffset;
+        public static readonly int GeometryCountsOffset;
+        public static readonly int VertexStridesOffset;
+        public static readonly int VertexOffsetsOffset;
+
+        private static int OffsetOf<T>(ref VertexInfoBuffer storage, ref T target)
+        {
+            return (int)Unsafe.ByteOffset(ref Unsafe.As<VertexInfoBuffer, T>(ref storage), ref target);
+        }
+
+        static VertexInfoBuffer()
+        {
+            RequiredSize = Unsafe.SizeOf<VertexInfoBuffer>();
+
+            VertexInfoBuffer instance = new();
+
+            VertexCountsOffset = OffsetOf(ref instance, ref instance.VertexCounts);
+            GeometryCountsOffset = OffsetOf(ref instance, ref instance.GeometryCounts);
+            VertexStridesOffset = OffsetOf(ref instance, ref instance.VertexStrides);
+            VertexOffsetsOffset = OffsetOf(ref instance, ref instance.VertexOffsets);
+        }
+
+        internal static StructureType GetStructureType()
+        {
+            return new StructureType(new[]
+            {
+                new StructureField(AggregateType.Vector4 | AggregateType.U32, "vertex_counts"),
+                new StructureField(AggregateType.Vector4 | AggregateType.U32, "geometry_counts"),
+                new StructureField(AggregateType.Array | AggregateType.Vector4 | AggregateType.U32, "vertex_strides", ResourceReservations.MaxVertexBufferTextures),
+                new StructureField(AggregateType.Array | AggregateType.Vector4 | AggregateType.U32, "vertex_offsets", ResourceReservations.MaxVertexBufferTextures),
+            });
+        }
+
+        public Vector4<int> VertexCounts;
+        public Vector4<int> GeometryCounts;
+        public Array32<Vector4<int>> VertexStrides;
+        public Array32<Vector4<int>> VertexOffsets;
+    }
+}
diff --git a/src/Ryujinx.Graphics.Texture/SizeCalculator.cs b/src/Ryujinx.Graphics.Texture/SizeCalculator.cs
index 7fe89e7e2..e6122a6ca 100644
--- a/src/Ryujinx.Graphics.Texture/SizeCalculator.cs
+++ b/src/Ryujinx.Graphics.Texture/SizeCalculator.cs
@@ -38,6 +38,7 @@ namespace Ryujinx.Graphics.Texture
             bool is3D = depth > 1 || gobBlocksInZ > 1;
 
             int layerSize = 0;
+            int layerSizeAligned = 0;
 
             int[] allOffsets = new int[is3D ? Calculate3DOffsetCount(levels, depth) : levels * layers * depth];
             int[] mipOffsets = new int[levels];
@@ -91,6 +92,8 @@ namespace Ryujinx.Graphics.Texture
                 sliceSizes[level] = totalBlocksOfGobsInY * robSize;
                 levelSizes[level] = totalBlocksOfGobsInZ * sliceSizes[level];
 
+                layerSizeAligned += levelSizes[level];
+
                 if (is3D)
                 {
                     int gobSize = mipGobBlocksInY * GobSize;
@@ -130,28 +133,32 @@ namespace Ryujinx.Graphics.Texture
                 depthLevelOffset += d;
             }
 
+            int totalSize;
+
             if (layers > 1)
             {
-                layerSize = AlignLayerSize(
-                    layerSize,
+                layerSizeAligned = AlignLayerSize(
+                    layerSizeAligned,
                     height,
                     depth,
                     blockHeight,
                     gobBlocksInY,
                     gobBlocksInZ,
                     gobBlocksInTileX);
-            }
 
-            int totalSize;
-
-            if (layerSize < gpuLayerSize)
-            {
-                totalSize = (layers - 1) * gpuLayerSize + layerSize;
-                layerSize = gpuLayerSize;
+                if (layerSizeAligned < gpuLayerSize)
+                {
+                    totalSize = (layers - 1) * gpuLayerSize + layerSizeAligned;
+                    layerSizeAligned = gpuLayerSize;
+                }
+                else
+                {
+                    totalSize = layerSizeAligned * layers;
+                }
             }
             else
             {
-                totalSize = layerSize * layers;
+                totalSize = layerSize;
             }
 
             if (!is3D)
@@ -159,7 +166,7 @@ namespace Ryujinx.Graphics.Texture
                 for (int layer = 0; layer < layers; layer++)
                 {
                     int baseIndex = layer * levels;
-                    int baseOffset = layer * layerSize;
+                    int baseOffset = layer * layerSizeAligned;
 
                     for (int level = 0; level < levels; level++)
                     {
@@ -168,7 +175,7 @@ namespace Ryujinx.Graphics.Texture
                 }
             }
 
-            return new SizeInfo(mipOffsets, allOffsets, sliceSizes, levelSizes, depth, levels, layerSize, totalSize, is3D);
+            return new SizeInfo(mipOffsets, allOffsets, sliceSizes, levelSizes, depth, levels, layerSizeAligned, totalSize, is3D);
         }
 
         public static SizeInfo GetLinearTextureSize(int stride, int height, int blockHeight)
diff --git a/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs b/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs
index c767a57a7..d3a3cae11 100644
--- a/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs
+++ b/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs
@@ -58,7 +58,7 @@ namespace Ryujinx.Graphics.Vulkan
         private int _flushTemp;
         private int _lastFlushWrite = -1;
 
-        private readonly ReaderWriterLock _flushLock;
+        private readonly ReaderWriterLockSlim _flushLock;
         private FenceHolder _flushFence;
         private int _flushWaiting;
 
@@ -85,7 +85,7 @@ namespace Ryujinx.Graphics.Vulkan
             _currentType = currentType;
             DesiredType = currentType;
 
-            _flushLock = new ReaderWriterLock();
+            _flushLock = new ReaderWriterLockSlim();
             _useMirrors = gd.IsTBDR;
         }
 
@@ -106,7 +106,7 @@ namespace Ryujinx.Graphics.Vulkan
             _currentType = currentType;
             DesiredType = currentType;
 
-            _flushLock = new ReaderWriterLock();
+            _flushLock = new ReaderWriterLockSlim();
         }
 
         public bool TryBackingSwap(ref CommandBufferScoped? cbs)
@@ -116,7 +116,7 @@ namespace Ryujinx.Graphics.Vulkan
                 // Only swap if the buffer is not used in any queued command buffer.
                 bool isRented = _buffer.HasRentedCommandBufferDependency(_gd.CommandBufferPool);
 
-                if (!isRented && _gd.CommandBufferPool.OwnedByCurrentThread && !_flushLock.IsReaderLockHeld && (_pendingData == null || cbs != null))
+                if (!isRented && _gd.CommandBufferPool.OwnedByCurrentThread && !_flushLock.IsReadLockHeld && (_pendingData == null || cbs != null))
                 {
                     var currentAllocation = _allocationAuto;
                     var currentBuffer = _buffer;
@@ -131,7 +131,7 @@ namespace Ryujinx.Graphics.Vulkan
                             ClearMirrors(cbs.Value, 0, Size);
                         }
 
-                        _flushLock.AcquireWriterLock(Timeout.Infinite);
+                        _flushLock.EnterWriteLock();
 
                         ClearFlushFence();
 
@@ -185,7 +185,7 @@ namespace Ryujinx.Graphics.Vulkan
 
                         _gd.PipelineInternal.SwapBuffer(currentBuffer, _buffer);
 
-                        _flushLock.ReleaseWriterLock();
+                        _flushLock.ExitWriteLock();
                     }
 
                     _swapQueued = false;
@@ -548,42 +548,44 @@ namespace Ryujinx.Graphics.Vulkan
 
         private void WaitForFlushFence()
         {
-            // Assumes the _flushLock is held as reader, returns in same state.
+            if (_flushFence == null)
+            {
+                return;
+            }
+
+            // If storage has changed, make sure the fence has been reached so that the data is in place.
+            _flushLock.ExitReadLock();
+            _flushLock.EnterWriteLock();
 
             if (_flushFence != null)
             {
-                // If storage has changed, make sure the fence has been reached so that the data is in place.
+                var fence = _flushFence;
+                Interlocked.Increment(ref _flushWaiting);
 
-                var cookie = _flushLock.UpgradeToWriterLock(Timeout.Infinite);
+                // Don't wait in the lock.
 
-                if (_flushFence != null)
+                _flushLock.ExitWriteLock();
+
+                fence.Wait();
+
+                _flushLock.EnterWriteLock();
+
+                if (Interlocked.Decrement(ref _flushWaiting) == 0)
                 {
-                    var fence = _flushFence;
-                    Interlocked.Increment(ref _flushWaiting);
-
-                    // Don't wait in the lock.
-
-                    var restoreCookie = _flushLock.ReleaseLock();
-
-                    fence.Wait();
-
-                    _flushLock.RestoreLock(ref restoreCookie);
-
-                    if (Interlocked.Decrement(ref _flushWaiting) == 0)
-                    {
-                        fence.Put();
-                    }
-
-                    _flushFence = null;
+                    fence.Put();
                 }
 
-                _flushLock.DowngradeFromWriterLock(ref cookie);
+                _flushFence = null;
             }
+
+            // Assumes the _flushLock is held as reader, returns in same state.
+            _flushLock.ExitWriteLock();
+            _flushLock.EnterReadLock();
         }
 
         public PinnedSpan<byte> GetData(int offset, int size)
         {
-            _flushLock.AcquireReaderLock(Timeout.Infinite);
+            _flushLock.EnterReadLock();
 
             WaitForFlushFence();
 
@@ -603,7 +605,7 @@ namespace Ryujinx.Graphics.Vulkan
                 // Need to be careful here, the buffer can't be unmapped while the data is being used.
                 _buffer.IncrementReferenceCount();
 
-                _flushLock.ReleaseReaderLock();
+                _flushLock.ExitReadLock();
 
                 return PinnedSpan<byte>.UnsafeFromSpan(result, _buffer.DecrementReferenceCount);
             }
@@ -621,7 +623,7 @@ namespace Ryujinx.Graphics.Vulkan
                 result = resource.GetFlushBuffer().GetBufferData(resource.GetPool(), this, offset, size);
             }
 
-            _flushLock.ReleaseReaderLock();
+            _flushLock.ExitReadLock();
 
             // Flush buffer is pinned until the next GetBufferData on the thread, which is fine for current uses.
             return PinnedSpan<byte>.UnsafeFromSpan(result);
@@ -967,7 +969,7 @@ namespace Ryujinx.Graphics.Vulkan
 
             if (!_cachedConvertedBuffers.TryGetValue(offset, size, key, out var holder))
             {
-                holder = _gd.BufferManager.Create(_gd, (size * 2 + 3) & ~3);
+                holder = _gd.BufferManager.Create(_gd, (size * 2 + 3) & ~3, baseType: BufferAllocationType.DeviceLocal);
 
                 _gd.PipelineInternal.EndRenderPass();
                 _gd.HelperShader.ConvertI8ToI16(_gd, cbs, this, holder, offset, size);
@@ -993,7 +995,7 @@ namespace Ryujinx.Graphics.Vulkan
             {
                 int alignedStride = (stride + (alignment - 1)) & -alignment;
 
-                holder = _gd.BufferManager.Create(_gd, (size / stride) * alignedStride);
+                holder = _gd.BufferManager.Create(_gd, (size / stride) * alignedStride, baseType: BufferAllocationType.DeviceLocal);
 
                 _gd.PipelineInternal.EndRenderPass();
                 _gd.HelperShader.ChangeStride(_gd, cbs, this, holder, offset, size, stride, alignedStride);
@@ -1023,7 +1025,7 @@ namespace Ryujinx.Graphics.Vulkan
 
                 int convertedCount = pattern.GetConvertedCount(indexCount);
 
-                holder = _gd.BufferManager.Create(_gd, convertedCount * 4);
+                holder = _gd.BufferManager.Create(_gd, convertedCount * 4, baseType: BufferAllocationType.DeviceLocal);
 
                 _gd.PipelineInternal.EndRenderPass();
                 _gd.HelperShader.ConvertIndexBuffer(_gd, cbs, this, holder, pattern, indexSize, offset, indexCount);
@@ -1073,11 +1075,11 @@ namespace Ryujinx.Graphics.Vulkan
                 _allocationAuto.Dispose();
             }
 
-            _flushLock.AcquireWriterLock(Timeout.Infinite);
+            _flushLock.EnterWriteLock();
 
             ClearFlushFence();
 
-            _flushLock.ReleaseWriterLock();
+            _flushLock.ExitWriteLock();
         }
     }
 }
diff --git a/src/Ryujinx.Graphics.Vulkan/CommandBufferPool.cs b/src/Ryujinx.Graphics.Vulkan/CommandBufferPool.cs
index 17eeef68a..4d3b8640f 100644
--- a/src/Ryujinx.Graphics.Vulkan/CommandBufferPool.cs
+++ b/src/Ryujinx.Graphics.Vulkan/CommandBufferPool.cs
@@ -27,6 +27,7 @@ namespace Ryujinx.Graphics.Vulkan
         {
             public bool InUse;
             public bool InConsumption;
+            public int SubmissionCount;
             public CommandBuffer CommandBuffer;
             public FenceHolder Fence;
             public SemaphoreHolder Semaphore;
@@ -193,6 +194,11 @@ namespace Ryujinx.Graphics.Vulkan
             return _commandBuffers[cbIndex].Fence;
         }
 
+        public int GetSubmissionCount(int cbIndex)
+        {
+            return _commandBuffers[cbIndex].SubmissionCount;
+        }
+
         private int FreeConsumed(bool wait)
         {
             int freeEntry = 0;
@@ -282,6 +288,7 @@ namespace Ryujinx.Graphics.Vulkan
                 Debug.Assert(entry.CommandBuffer.Handle == cbs.CommandBuffer.Handle);
                 entry.InUse = false;
                 entry.InConsumption = true;
+                entry.SubmissionCount++;
                 _inUseCount--;
 
                 var commandBuffer = entry.CommandBuffer;
diff --git a/src/Ryujinx.Graphics.Vulkan/DescriptorSetManager.cs b/src/Ryujinx.Graphics.Vulkan/DescriptorSetManager.cs
index 2f7b604c4..7594384d6 100644
--- a/src/Ryujinx.Graphics.Vulkan/DescriptorSetManager.cs
+++ b/src/Ryujinx.Graphics.Vulkan/DescriptorSetManager.cs
@@ -6,7 +6,7 @@ namespace Ryujinx.Graphics.Vulkan
 {
     class DescriptorSetManager : IDisposable
     {
-        private const uint DescriptorPoolMultiplier = 16;
+        public const uint MaxSets = 16;
 
         public class DescriptorPoolHolder : IDisposable
         {
@@ -14,36 +14,28 @@ namespace Ryujinx.Graphics.Vulkan
             public Device Device { get; }
 
             private readonly DescriptorPool _pool;
-            private readonly uint _capacity;
+            private int _freeDescriptors;
             private int _totalSets;
             private int _setsInUse;
             private bool _done;
 
-            public unsafe DescriptorPoolHolder(Vk api, Device device)
+            public unsafe DescriptorPoolHolder(Vk api, Device device, ReadOnlySpan<DescriptorPoolSize> poolSizes, bool updateAfterBind)
             {
                 Api = api;
                 Device = device;
 
-                var poolSizes = new[]
+                foreach (var poolSize in poolSizes)
                 {
-                    new DescriptorPoolSize(DescriptorType.UniformBuffer, (1 + Constants.MaxUniformBufferBindings) * DescriptorPoolMultiplier),
-                    new DescriptorPoolSize(DescriptorType.StorageBuffer, Constants.MaxStorageBufferBindings * DescriptorPoolMultiplier),
-                    new DescriptorPoolSize(DescriptorType.CombinedImageSampler, Constants.MaxTextureBindings * DescriptorPoolMultiplier),
-                    new DescriptorPoolSize(DescriptorType.StorageImage, Constants.MaxImageBindings * DescriptorPoolMultiplier),
-                    new DescriptorPoolSize(DescriptorType.UniformTexelBuffer, Constants.MaxTextureBindings * DescriptorPoolMultiplier),
-                    new DescriptorPoolSize(DescriptorType.StorageTexelBuffer, Constants.MaxImageBindings * DescriptorPoolMultiplier),
-                };
-
-                uint maxSets = (uint)poolSizes.Length * DescriptorPoolMultiplier;
-
-                _capacity = maxSets;
+                    _freeDescriptors += (int)poolSize.DescriptorCount;
+                }
 
                 fixed (DescriptorPoolSize* pPoolsSize = poolSizes)
                 {
                     var descriptorPoolCreateInfo = new DescriptorPoolCreateInfo
                     {
                         SType = StructureType.DescriptorPoolCreateInfo,
-                        MaxSets = maxSets,
+                        Flags = updateAfterBind ? DescriptorPoolCreateFlags.UpdateAfterBindBit : DescriptorPoolCreateFlags.None,
+                        MaxSets = MaxSets,
                         PoolSizeCount = (uint)poolSizes.Length,
                         PPoolSizes = pPoolsSize,
                     };
@@ -52,18 +44,22 @@ namespace Ryujinx.Graphics.Vulkan
                 }
             }
 
-            public DescriptorSetCollection AllocateDescriptorSets(ReadOnlySpan<DescriptorSetLayout> layouts)
+            public unsafe DescriptorSetCollection AllocateDescriptorSets(ReadOnlySpan<DescriptorSetLayout> layouts, int consumedDescriptors)
             {
-                TryAllocateDescriptorSets(layouts, isTry: false, out var dsc);
+                TryAllocateDescriptorSets(layouts, consumedDescriptors, isTry: false, out var dsc);
                 return dsc;
             }
 
-            public bool TryAllocateDescriptorSets(ReadOnlySpan<DescriptorSetLayout> layouts, out DescriptorSetCollection dsc)
+            public bool TryAllocateDescriptorSets(ReadOnlySpan<DescriptorSetLayout> layouts, int consumedDescriptors, out DescriptorSetCollection dsc)
             {
-                return TryAllocateDescriptorSets(layouts, isTry: true, out dsc);
+                return TryAllocateDescriptorSets(layouts, consumedDescriptors, isTry: true, out dsc);
             }
 
-            private unsafe bool TryAllocateDescriptorSets(ReadOnlySpan<DescriptorSetLayout> layouts, bool isTry, out DescriptorSetCollection dsc)
+            private unsafe bool TryAllocateDescriptorSets(
+                ReadOnlySpan<DescriptorSetLayout> layouts,
+                int consumedDescriptors,
+                bool isTry,
+                out DescriptorSetCollection dsc)
             {
                 Debug.Assert(!_done);
 
@@ -84,7 +80,7 @@ namespace Ryujinx.Graphics.Vulkan
                         var result = Api.AllocateDescriptorSets(Device, &descriptorSetAllocateInfo, pDescriptorSets);
                         if (isTry && result == Result.ErrorOutOfPoolMemory)
                         {
-                            _totalSets = (int)_capacity;
+                            _totalSets = (int)MaxSets;
                             _done = true;
                             DestroyIfDone();
                             dsc = default;
@@ -95,6 +91,7 @@ namespace Ryujinx.Graphics.Vulkan
                     }
                 }
 
+                _freeDescriptors -= consumedDescriptors;
                 _totalSets += layouts.Length;
                 _setsInUse += layouts.Length;
 
@@ -109,9 +106,15 @@ namespace Ryujinx.Graphics.Vulkan
                 DestroyIfDone();
             }
 
-            public bool CanFit(int count)
+            public bool CanFit(int setsCount, int descriptorsCount)
             {
-                if (_totalSets + count <= _capacity)
+                // Try to determine if an allocation with the given parameters will succeed.
+                // An allocation may fail if the sets count or descriptors count exceeds the available counts
+                // of the pool.
+                // Not getting that right is not fatal, it will just create a new pool and try again,
+                // but it is less efficient.
+
+                if (_totalSets + setsCount <= MaxSets && _freeDescriptors >= descriptorsCount)
                 {
                     return true;
                 }
@@ -148,46 +151,74 @@ namespace Ryujinx.Graphics.Vulkan
         }
 
         private readonly Device _device;
-        private DescriptorPoolHolder _currentPool;
+        private readonly DescriptorPoolHolder[] _currentPools;
 
-        public DescriptorSetManager(Device device)
+        public DescriptorSetManager(Device device, int poolCount)
         {
             _device = device;
+            _currentPools = new DescriptorPoolHolder[poolCount];
         }
 
-        public Auto<DescriptorSetCollection> AllocateDescriptorSet(Vk api, DescriptorSetLayout layout)
+        public Auto<DescriptorSetCollection> AllocateDescriptorSet(
+            Vk api,
+            DescriptorSetLayout layout,
+            ReadOnlySpan<DescriptorPoolSize> poolSizes,
+            int poolIndex,
+            int consumedDescriptors,
+            bool updateAfterBind)
         {
             Span<DescriptorSetLayout> layouts = stackalloc DescriptorSetLayout[1];
             layouts[0] = layout;
-            return AllocateDescriptorSets(api, layouts);
+            return AllocateDescriptorSets(api, layouts, poolSizes, poolIndex, consumedDescriptors, updateAfterBind);
         }
 
-        public Auto<DescriptorSetCollection> AllocateDescriptorSets(Vk api, ReadOnlySpan<DescriptorSetLayout> layouts)
+        public Auto<DescriptorSetCollection> AllocateDescriptorSets(
+            Vk api,
+            ReadOnlySpan<DescriptorSetLayout> layouts,
+            ReadOnlySpan<DescriptorPoolSize> poolSizes,
+            int poolIndex,
+            int consumedDescriptors,
+            bool updateAfterBind)
         {
             // If we fail the first time, just create a new pool and try again.
-            if (!GetPool(api, layouts.Length).TryAllocateDescriptorSets(layouts, out var dsc))
+
+            var pool = GetPool(api, poolSizes, poolIndex, layouts.Length, consumedDescriptors, updateAfterBind);
+            if (!pool.TryAllocateDescriptorSets(layouts, consumedDescriptors, out var dsc))
             {
-                dsc = GetPool(api, layouts.Length).AllocateDescriptorSets(layouts);
+                pool = GetPool(api, poolSizes, poolIndex, layouts.Length, consumedDescriptors, updateAfterBind);
+                dsc = pool.AllocateDescriptorSets(layouts, consumedDescriptors);
             }
 
             return new Auto<DescriptorSetCollection>(dsc);
         }
 
-        private DescriptorPoolHolder GetPool(Vk api, int requiredCount)
+        private DescriptorPoolHolder GetPool(
+            Vk api,
+            ReadOnlySpan<DescriptorPoolSize> poolSizes,
+            int poolIndex,
+            int setsCount,
+            int descriptorsCount,
+            bool updateAfterBind)
         {
-            if (_currentPool == null || !_currentPool.CanFit(requiredCount))
+            ref DescriptorPoolHolder currentPool = ref _currentPools[poolIndex];
+
+            if (currentPool == null || !currentPool.CanFit(setsCount, descriptorsCount))
             {
-                _currentPool = new DescriptorPoolHolder(api, _device);
+                currentPool = new DescriptorPoolHolder(api, _device, poolSizes, updateAfterBind);
             }
 
-            return _currentPool;
+            return currentPool;
         }
 
         protected virtual void Dispose(bool disposing)
         {
             if (disposing)
             {
-                _currentPool?.Dispose();
+                for (int index = 0; index < _currentPools.Length; index++)
+                {
+                    _currentPools[index]?.Dispose();
+                    _currentPools[index] = null;
+                }
             }
         }
 
diff --git a/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs b/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs
index 14e4c02f0..a9a92df1d 100644
--- a/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs
+++ b/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs
@@ -59,6 +59,8 @@ namespace Ryujinx.Graphics.Vulkan
         private BitMapStruct<Array2<long>> _uniformMirrored;
         private BitMapStruct<Array2<long>> _storageMirrored;
 
+        private bool _updateDescriptorCacheCbIndex;
+
         [Flags]
         private enum DirtyFlags
         {
@@ -218,6 +220,7 @@ namespace Ryujinx.Graphics.Vulkan
         public void SetProgram(ShaderCollection program)
         {
             _program = program;
+            _updateDescriptorCacheCbIndex = true;
             _dirty = DirtyFlags.All;
         }
 
@@ -490,7 +493,13 @@ namespace Ryujinx.Graphics.Vulkan
 
             var dummyBuffer = _dummyBuffer?.GetBuffer();
 
-            var dsc = program.GetNewDescriptorSetCollection(_gd, cbs.CommandBufferIndex, setIndex, out var isNew).Get(cbs);
+            if (_updateDescriptorCacheCbIndex)
+            {
+                _updateDescriptorCacheCbIndex = false;
+                program.UpdateDescriptorCacheCommandBufferIndex(cbs.CommandBufferIndex);
+            }
+
+            var dsc = program.GetNewDescriptorSetCollection(setIndex, out var isNew).Get(cbs);
 
             if (!program.HasMinimalLayout)
             {
@@ -697,6 +706,7 @@ namespace Ryujinx.Graphics.Vulkan
 
         public void SignalCommandBufferChange()
         {
+            _updateDescriptorCacheCbIndex = true;
             _dirty = DirtyFlags.All;
 
             _uniformSet.Clear();
diff --git a/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs b/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs
index 749d5929c..662bb80f8 100644
--- a/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs
+++ b/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs
@@ -148,6 +148,16 @@ namespace Ryujinx.Graphics.Vulkan
             return _attachments[index];
         }
 
+        public Auto<DisposableImageView> GetDepthStencilAttachment()
+        {
+            if (!HasDepthStencil)
+            {
+                return null;
+            }
+
+            return _attachments[AttachmentsCount - 1];
+        }
+
         public ComponentType GetAttachmentComponentType(int index)
         {
             if (_colors != null && (uint)index < _colors.Length)
@@ -247,14 +257,22 @@ namespace Ryujinx.Graphics.Vulkan
 
                 if (realIndex != -1)
                 {
-                    _colors[realIndex].Storage?.InsertReadToWriteBarrier(cbs, AccessFlags.ColorAttachmentWriteBit, PipelineStageFlags.ColorAttachmentOutputBit);
+                    _colors[realIndex].Storage?.InsertReadToWriteBarrier(
+                        cbs,
+                        AccessFlags.ColorAttachmentWriteBit,
+                        PipelineStageFlags.ColorAttachmentOutputBit,
+                        insideRenderPass: true);
                 }
             }
         }
 
         public void InsertClearBarrierDS(CommandBufferScoped cbs)
         {
-            _depthStencil?.Storage?.InsertReadToWriteBarrier(cbs, AccessFlags.DepthStencilAttachmentWriteBit, PipelineStageFlags.LateFragmentTestsBit);
+            _depthStencil?.Storage?.InsertReadToWriteBarrier(
+                cbs,
+                AccessFlags.DepthStencilAttachmentWriteBit,
+                PipelineStageFlags.LateFragmentTestsBit,
+                insideRenderPass: true);
         }
     }
 }
diff --git a/src/Ryujinx.Graphics.Vulkan/HardwareCapabilities.cs b/src/Ryujinx.Graphics.Vulkan/HardwareCapabilities.cs
index e76a332f4..32e941dba 100644
--- a/src/Ryujinx.Graphics.Vulkan/HardwareCapabilities.cs
+++ b/src/Ryujinx.Graphics.Vulkan/HardwareCapabilities.cs
@@ -25,7 +25,6 @@ namespace Ryujinx.Graphics.Vulkan
         public readonly bool SupportsIndirectParameters;
         public readonly bool SupportsFragmentShaderInterlock;
         public readonly bool SupportsGeometryShaderPassthrough;
-        public readonly bool SupportsSubgroupSizeControl;
         public readonly bool SupportsShaderFloat64;
         public readonly bool SupportsShaderInt8;
         public readonly bool SupportsShaderStencilExport;
@@ -42,12 +41,11 @@ namespace Ryujinx.Graphics.Vulkan
         public readonly bool SupportsPreciseOcclusionQueries;
         public readonly bool SupportsPipelineStatisticsQuery;
         public readonly bool SupportsGeometryShader;
+        public readonly bool SupportsTessellationShader;
         public readonly bool SupportsViewportArray2;
         public readonly bool SupportsHostImportedMemory;
         public readonly bool SupportsDepthClipControl;
-        public readonly uint MinSubgroupSize;
-        public readonly uint MaxSubgroupSize;
-        public readonly ShaderStageFlags RequiredSubgroupSizeStages;
+        public readonly uint SubgroupSize;
         public readonly SampleCountFlags SupportedSampleCounts;
         public readonly PortabilitySubsetFlags PortabilitySubset;
         public readonly uint VertexBufferAlignment;
@@ -64,7 +62,6 @@ namespace Ryujinx.Graphics.Vulkan
             bool supportsIndirectParameters,
             bool supportsFragmentShaderInterlock,
             bool supportsGeometryShaderPassthrough,
-            bool supportsSubgroupSizeControl,
             bool supportsShaderFloat64,
             bool supportsShaderInt8,
             bool supportsShaderStencilExport,
@@ -81,12 +78,11 @@ namespace Ryujinx.Graphics.Vulkan
             bool supportsPreciseOcclusionQueries,
             bool supportsPipelineStatisticsQuery,
             bool supportsGeometryShader,
+            bool supportsTessellationShader,
             bool supportsViewportArray2,
             bool supportsHostImportedMemory,
             bool supportsDepthClipControl,
-            uint minSubgroupSize,
-            uint maxSubgroupSize,
-            ShaderStageFlags requiredSubgroupSizeStages,
+            uint subgroupSize,
             SampleCountFlags supportedSampleCounts,
             PortabilitySubsetFlags portabilitySubset,
             uint vertexBufferAlignment,
@@ -102,7 +98,6 @@ namespace Ryujinx.Graphics.Vulkan
             SupportsIndirectParameters = supportsIndirectParameters;
             SupportsFragmentShaderInterlock = supportsFragmentShaderInterlock;
             SupportsGeometryShaderPassthrough = supportsGeometryShaderPassthrough;
-            SupportsSubgroupSizeControl = supportsSubgroupSizeControl;
             SupportsShaderFloat64 = supportsShaderFloat64;
             SupportsShaderInt8 = supportsShaderInt8;
             SupportsShaderStencilExport = supportsShaderStencilExport;
@@ -119,12 +114,11 @@ namespace Ryujinx.Graphics.Vulkan
             SupportsPreciseOcclusionQueries = supportsPreciseOcclusionQueries;
             SupportsPipelineStatisticsQuery = supportsPipelineStatisticsQuery;
             SupportsGeometryShader = supportsGeometryShader;
+            SupportsTessellationShader = supportsTessellationShader;
             SupportsViewportArray2 = supportsViewportArray2;
             SupportsHostImportedMemory = supportsHostImportedMemory;
             SupportsDepthClipControl = supportsDepthClipControl;
-            MinSubgroupSize = minSubgroupSize;
-            MaxSubgroupSize = maxSubgroupSize;
-            RequiredSubgroupSizeStages = requiredSubgroupSizeStages;
+            SubgroupSize = subgroupSize;
             SupportedSampleCounts = supportedSampleCounts;
             PortabilitySubset = portabilitySubset;
             VertexBufferAlignment = vertexBufferAlignment;
diff --git a/src/Ryujinx.Graphics.Vulkan/HelperShader.cs b/src/Ryujinx.Graphics.Vulkan/HelperShader.cs
index 684ed068a..5be4a9329 100644
--- a/src/Ryujinx.Graphics.Vulkan/HelperShader.cs
+++ b/src/Ryujinx.Graphics.Vulkan/HelperShader.cs
@@ -5,7 +5,6 @@ using Ryujinx.Graphics.Shader.Translation;
 using Silk.NET.Vulkan;
 using System;
 using System.Collections.Generic;
-using System.IO;
 using System.Numerics;
 using CompareOp = Ryujinx.Graphics.GAL.CompareOp;
 using Format = Ryujinx.Graphics.GAL.Format;
@@ -27,6 +26,7 @@ namespace Ryujinx.Graphics.Vulkan
     class HelperShader : IDisposable
     {
         private const int UniformBufferAlignment = 256;
+        private const int ConvertElementsPerWorkgroup = 32 * 100; // Work group size of 32 times 100 elements.
         private const string ShaderBinariesPath = "Ryujinx.Graphics.Vulkan/Shaders/SpirvBinaries";
 
         private readonly PipelineHelperShader _pipeline;
@@ -38,6 +38,7 @@ namespace Ryujinx.Graphics.Vulkan
         private readonly IProgram _programColorClearF;
         private readonly IProgram _programColorClearSI;
         private readonly IProgram _programColorClearUI;
+        private readonly IProgram _programDepthStencilClear;
         private readonly IProgram _programStrideChange;
         private readonly IProgram _programConvertD32S8ToD24S8;
         private readonly IProgram _programConvertIndexBuffer;
@@ -105,6 +106,12 @@ namespace Ryujinx.Graphics.Vulkan
                 new ShaderSource(ReadSpirv("ColorClearUIFragment.spv"), ShaderStage.Fragment, TargetLanguage.Spirv),
             }, colorClearResourceLayout);
 
+            _programDepthStencilClear = gd.CreateProgramWithMinimalLayout(new[]
+            {
+                new ShaderSource(ReadSpirv("ColorClearVertex.spv"), ShaderStage.Vertex, TargetLanguage.Spirv),
+                new ShaderSource(ReadSpirv("DepthStencilClearFragment.spv"), ShaderStage.Fragment, TargetLanguage.Spirv),
+            }, colorClearResourceLayout);
+
             var strideChangeResourceLayout = new ResourceLayoutBuilder()
                 .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 0)
                 .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 1)
@@ -446,10 +453,6 @@ namespace Ryujinx.Graphics.Vulkan
                 0f,
                 1f);
 
-            Span<Rectangle<int>> scissors = stackalloc Rectangle<int>[1];
-
-            scissors[0] = new Rectangle<int>(0, 0, dstWidth, dstHeight);
-
             if (dstIsDepthOrStencil)
             {
                 _pipeline.SetProgram(src.Info.Target.IsMultisample() ? _programDepthBlitMs : _programDepthBlit);
@@ -470,7 +473,7 @@ namespace Ryujinx.Graphics.Vulkan
 
             _pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight, (uint)dstSamples, dstIsDepthOrStencil, dstFormat);
             _pipeline.SetRenderTargetColorMasks(new uint[] { 0xf });
-            _pipeline.SetScissors(scissors);
+            _pipeline.SetScissors(stackalloc Rectangle<int>[] { new Rectangle<int>(0, 0, dstWidth, dstHeight) });
 
             if (clearAlpha)
             {
@@ -547,12 +550,8 @@ namespace Ryujinx.Graphics.Vulkan
                 0f,
                 1f);
 
-            Span<Rectangle<int>> scissors = stackalloc Rectangle<int>[1];
-
-            scissors[0] = new Rectangle<int>(0, 0, dstWidth, dstHeight);
-
             _pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight, (uint)dstSamples, true, dstFormat);
-            _pipeline.SetScissors(scissors);
+            _pipeline.SetScissors(stackalloc Rectangle<int>[] { new Rectangle<int>(0, 0, dstWidth, dstHeight) });
             _pipeline.SetViewports(viewports);
             _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip);
 
@@ -639,7 +638,11 @@ namespace Ryujinx.Graphics.Vulkan
             }
         }
 
-        private static StencilTestDescriptor CreateStencilTestDescriptor(bool enabled)
+        private static StencilTestDescriptor CreateStencilTestDescriptor(
+            bool enabled,
+            int refValue = 0,
+            int compareMask = 0xff,
+            int writeMask = 0xff)
         {
             return new StencilTestDescriptor(
                 enabled,
@@ -647,16 +650,16 @@ namespace Ryujinx.Graphics.Vulkan
                 StencilOp.Replace,
                 StencilOp.Replace,
                 StencilOp.Replace,
-                0,
-                0xff,
-                0xff,
+                refValue,
+                compareMask,
+                writeMask,
                 CompareOp.Always,
                 StencilOp.Replace,
                 StencilOp.Replace,
                 StencilOp.Replace,
-                0,
-                0xff,
-                0xff);
+                refValue,
+                compareMask,
+                writeMask);
         }
 
         public void Clear(
@@ -695,10 +698,6 @@ namespace Ryujinx.Graphics.Vulkan
                 0f,
                 1f);
 
-            Span<Rectangle<int>> scissors = stackalloc Rectangle<int>[1];
-
-            scissors[0] = scissor;
-
             IProgram program;
 
             if (type == ComponentType.SignedInteger)
@@ -718,7 +717,7 @@ namespace Ryujinx.Graphics.Vulkan
             _pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight, false, dstFormat);
             _pipeline.SetRenderTargetColorMasks(new[] { componentMask });
             _pipeline.SetViewports(viewports);
-            _pipeline.SetScissors(scissors);
+            _pipeline.SetScissors(stackalloc Rectangle<int>[] { scissor });
             _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip);
             _pipeline.Draw(4, 1, 0, 0);
             _pipeline.Finish();
@@ -726,6 +725,56 @@ namespace Ryujinx.Graphics.Vulkan
             gd.BufferManager.Delete(bufferHandle);
         }
 
+        public void Clear(
+            VulkanRenderer gd,
+            Auto<DisposableImageView> dst,
+            float depthValue,
+            bool depthMask,
+            int stencilValue,
+            int stencilMask,
+            int dstWidth,
+            int dstHeight,
+            VkFormat dstFormat,
+            Rectangle<int> scissor)
+        {
+            const int ClearColorBufferSize = 16;
+
+            gd.FlushAllCommands();
+
+            using var cbs = gd.CommandBufferPool.Rent();
+
+            _pipeline.SetCommandBuffer(cbs);
+
+            var bufferHandle = gd.BufferManager.CreateWithHandle(gd, ClearColorBufferSize);
+
+            gd.BufferManager.SetData<float>(bufferHandle, 0, stackalloc float[] { depthValue });
+
+            _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(1, new BufferRange(bufferHandle, 0, ClearColorBufferSize)) });
+
+            Span<Viewport> viewports = stackalloc Viewport[1];
+
+            viewports[0] = new Viewport(
+                new Rectangle<float>(0, 0, dstWidth, dstHeight),
+                ViewportSwizzle.PositiveX,
+                ViewportSwizzle.PositiveY,
+                ViewportSwizzle.PositiveZ,
+                ViewportSwizzle.PositiveW,
+                0f,
+                1f);
+
+            _pipeline.SetProgram(_programDepthStencilClear);
+            _pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight, true, dstFormat);
+            _pipeline.SetViewports(viewports);
+            _pipeline.SetScissors(stackalloc Rectangle<int>[] { scissor });
+            _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip);
+            _pipeline.SetDepthTest(new DepthTestDescriptor(true, depthMask, CompareOp.Always));
+            _pipeline.SetStencilTest(CreateStencilTestDescriptor(stencilMask != 0, stencilValue, 0xff, stencilMask));
+            _pipeline.Draw(4, 1, 0, 0);
+            _pipeline.Finish();
+
+            gd.BufferManager.Delete(bufferHandle);
+        }
+
         public void DrawTexture(
             VulkanRenderer gd,
             PipelineBase pipeline,
@@ -778,8 +827,6 @@ namespace Ryujinx.Graphics.Vulkan
                 0f,
                 1f);
 
-            Span<Rectangle<int>> scissors = stackalloc Rectangle<int>[1];
-
             pipeline.SetProgram(_programColorBlit);
             pipeline.SetViewports(viewports);
             pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip);
@@ -847,7 +894,7 @@ namespace Ryujinx.Graphics.Vulkan
                 _pipeline.SetStorageBuffers(1, sbRanges);
 
                 _pipeline.SetProgram(_programStrideChange);
-                _pipeline.DispatchCompute(1, 1, 1);
+                _pipeline.DispatchCompute(1 + elems / ConvertElementsPerWorkgroup, 1, 1);
 
                 gd.BufferManager.Delete(bufferHandle);
 
@@ -1119,11 +1166,7 @@ namespace Ryujinx.Graphics.Vulkan
                     0f,
                     1f);
 
-                Span<Rectangle<int>> scissors = stackalloc Rectangle<int>[1];
-
-                scissors[0] = new Rectangle<int>(0, 0, dst.Width, dst.Height);
-
-                _pipeline.SetScissors(scissors);
+                _pipeline.SetScissors(stackalloc Rectangle<int>[] { new Rectangle<int>(0, 0, dst.Width, dst.Height) });
                 _pipeline.SetViewports(viewports);
                 _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip);
 
@@ -1251,12 +1294,8 @@ namespace Ryujinx.Graphics.Vulkan
                 0f,
                 1f);
 
-            Span<Rectangle<int>> scissors = stackalloc Rectangle<int>[1];
-
-            scissors[0] = new Rectangle<int>(0, 0, dst.Width, dst.Height);
-
             _pipeline.SetRenderTargetColorMasks(new uint[] { 0xf });
-            _pipeline.SetScissors(scissors);
+            _pipeline.SetScissors(stackalloc Rectangle<int>[] { new Rectangle<int>(0, 0, dst.Width, dst.Height) });
             _pipeline.SetViewports(viewports);
             _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip);
 
@@ -1703,7 +1742,7 @@ namespace Ryujinx.Graphics.Vulkan
             _pipeline.SetStorageBuffers(1, sbRanges);
 
             _pipeline.SetProgram(_programConvertD32S8ToD24S8);
-            _pipeline.DispatchCompute(1, 1, 1);
+            _pipeline.DispatchCompute(1 + inSize / ConvertElementsPerWorkgroup, 1, 1);
 
             gd.BufferManager.Delete(bufferHandle);
 
@@ -1731,6 +1770,7 @@ namespace Ryujinx.Graphics.Vulkan
                 _programColorClearF.Dispose();
                 _programColorClearSI.Dispose();
                 _programColorClearUI.Dispose();
+                _programDepthStencilClear.Dispose();
                 _programStrideChange.Dispose();
                 _programConvertIndexBuffer.Dispose();
                 _programConvertIndirectData.Dispose();
diff --git a/src/Ryujinx.Graphics.Vulkan/MemoryAllocator.cs b/src/Ryujinx.Graphics.Vulkan/MemoryAllocator.cs
index ab7006270..aa0b410c4 100644
--- a/src/Ryujinx.Graphics.Vulkan/MemoryAllocator.cs
+++ b/src/Ryujinx.Graphics.Vulkan/MemoryAllocator.cs
@@ -1,6 +1,7 @@
 using Silk.NET.Vulkan;
 using System;
 using System.Collections.Generic;
+using System.Threading;
 
 namespace Ryujinx.Graphics.Vulkan
 {
@@ -13,6 +14,7 @@ namespace Ryujinx.Graphics.Vulkan
         private readonly Device _device;
         private readonly List<MemoryAllocatorBlockList> _blockLists;
         private readonly int _blockAlignment;
+        private readonly ReaderWriterLockSlim _lock;
 
         public MemoryAllocator(Vk api, VulkanPhysicalDevice physicalDevice, Device device)
         {
@@ -21,6 +23,7 @@ namespace Ryujinx.Graphics.Vulkan
             _device = device;
             _blockLists = new List<MemoryAllocatorBlockList>();
             _blockAlignment = (int)Math.Min(int.MaxValue, MaxDeviceMemoryUsageEstimate / _physicalDevice.PhysicalDeviceProperties.Limits.MaxMemoryAllocationCount);
+            _lock = new(LockRecursionPolicy.NoRecursion);
         }
 
         public MemoryAllocation AllocateDeviceMemory(
@@ -40,21 +43,37 @@ namespace Ryujinx.Graphics.Vulkan
 
         private MemoryAllocation Allocate(int memoryTypeIndex, ulong size, ulong alignment, bool map, bool isBuffer)
         {
-            for (int i = 0; i < _blockLists.Count; i++)
+            _lock.EnterReadLock();
+
+            try
             {
-                var bl = _blockLists[i];
-                if (bl.MemoryTypeIndex == memoryTypeIndex && bl.ForBuffer == isBuffer)
+                for (int i = 0; i < _blockLists.Count; i++)
                 {
-                    lock (bl)
+                    var bl = _blockLists[i];
+                    if (bl.MemoryTypeIndex == memoryTypeIndex && bl.ForBuffer == isBuffer)
                     {
                         return bl.Allocate(size, alignment, map);
                     }
                 }
             }
+            finally
+            {
+                _lock.ExitReadLock();
+            }
 
-            var newBl = new MemoryAllocatorBlockList(_api, _device, memoryTypeIndex, _blockAlignment, isBuffer);
-            _blockLists.Add(newBl);
-            return newBl.Allocate(size, alignment, map);
+            _lock.EnterWriteLock();
+
+            try
+            {
+                var newBl = new MemoryAllocatorBlockList(_api, _device, memoryTypeIndex, _blockAlignment, isBuffer);
+                _blockLists.Add(newBl);
+
+                return newBl.Allocate(size, alignment, map);
+            }
+            finally
+            {
+                _lock.ExitWriteLock();
+            }
         }
 
         internal int FindSuitableMemoryTypeIndex(
diff --git a/src/Ryujinx.Graphics.Vulkan/MemoryAllocatorBlockList.cs b/src/Ryujinx.Graphics.Vulkan/MemoryAllocatorBlockList.cs
index bd57e778b..6a40b16e3 100644
--- a/src/Ryujinx.Graphics.Vulkan/MemoryAllocatorBlockList.cs
+++ b/src/Ryujinx.Graphics.Vulkan/MemoryAllocatorBlockList.cs
@@ -3,6 +3,7 @@ using Silk.NET.Vulkan;
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.Threading;
 
 namespace Ryujinx.Graphics.Vulkan
 {
@@ -166,6 +167,8 @@ namespace Ryujinx.Graphics.Vulkan
 
         private readonly int _blockAlignment;
 
+        private readonly ReaderWriterLockSlim _lock;
+
         public MemoryAllocatorBlockList(Vk api, Device device, int memoryTypeIndex, int blockAlignment, bool forBuffer)
         {
             _blocks = new List<Block>();
@@ -174,6 +177,7 @@ namespace Ryujinx.Graphics.Vulkan
             MemoryTypeIndex = memoryTypeIndex;
             ForBuffer = forBuffer;
             _blockAlignment = blockAlignment;
+            _lock = new(LockRecursionPolicy.NoRecursion);
         }
 
         public unsafe MemoryAllocation Allocate(ulong size, ulong alignment, bool map)
@@ -184,19 +188,28 @@ namespace Ryujinx.Graphics.Vulkan
                 throw new ArgumentOutOfRangeException(nameof(alignment), $"Invalid alignment 0x{alignment:X}.");
             }
 
-            for (int i = 0; i < _blocks.Count; i++)
-            {
-                var block = _blocks[i];
+            _lock.EnterReadLock();
 
-                if (block.Mapped == map && block.Size >= size)
+            try
+            {
+                for (int i = 0; i < _blocks.Count; i++)
                 {
-                    ulong offset = block.Allocate(size, alignment);
-                    if (offset != InvalidOffset)
+                    var block = _blocks[i];
+
+                    if (block.Mapped == map && block.Size >= size)
                     {
-                        return new MemoryAllocation(this, block, block.Memory, GetHostPointer(block, offset), offset, size);
+                        ulong offset = block.Allocate(size, alignment);
+                        if (offset != InvalidOffset)
+                        {
+                            return new MemoryAllocation(this, block, block.Memory, GetHostPointer(block, offset), offset, size);
+                        }
                     }
                 }
             }
+            finally
+            {
+                _lock.ExitReadLock();
+            }
 
             ulong blockAlignedSize = BitUtils.AlignUp(size, (ulong)_blockAlignment);
 
@@ -244,14 +257,23 @@ namespace Ryujinx.Graphics.Vulkan
 
             if (block.IsTotallyFree())
             {
-                for (int i = 0; i < _blocks.Count; i++)
+                _lock.EnterWriteLock();
+
+                try
                 {
-                    if (_blocks[i] == block)
+                    for (int i = 0; i < _blocks.Count; i++)
                     {
-                        _blocks.RemoveAt(i);
-                        break;
+                        if (_blocks[i] == block)
+                        {
+                            _blocks.RemoveAt(i);
+                            break;
+                        }
                     }
                 }
+                finally
+                {
+                    _lock.ExitWriteLock();
+                }
 
                 block.Destroy(_api, _device);
             }
@@ -259,13 +281,22 @@ namespace Ryujinx.Graphics.Vulkan
 
         private void InsertBlock(Block block)
         {
-            int index = _blocks.BinarySearch(block);
-            if (index < 0)
-            {
-                index = ~index;
-            }
+            _lock.EnterWriteLock();
 
-            _blocks.Insert(index, block);
+            try
+            {
+                int index = _blocks.BinarySearch(block);
+                if (index < 0)
+                {
+                    index = ~index;
+                }
+
+                _blocks.Insert(index, block);
+            }
+            finally
+            {
+                _lock.ExitWriteLock();
+            }
         }
 
         public void Dispose()
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
index 67b16ec96..156b3db16 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
@@ -149,10 +149,22 @@ namespace Ryujinx.Graphics.Vulkan
                 DstAccessMask = AccessFlags.MemoryReadBit | AccessFlags.MemoryWriteBit,
             };
 
+            PipelineStageFlags pipelineStageFlags = PipelineStageFlags.VertexShaderBit | PipelineStageFlags.FragmentShaderBit;
+
+            if (Gd.Capabilities.SupportsGeometryShader)
+            {
+                pipelineStageFlags |= PipelineStageFlags.GeometryShaderBit;
+            }
+
+            if (Gd.Capabilities.SupportsTessellationShader)
+            {
+                pipelineStageFlags |= PipelineStageFlags.TessellationControlShaderBit | PipelineStageFlags.TessellationEvaluationShaderBit;
+            }
+
             Gd.Api.CmdPipelineBarrier(
                 CommandBuffer,
-                PipelineStageFlags.FragmentShaderBit,
-                PipelineStageFlags.FragmentShaderBit,
+                pipelineStageFlags,
+                pipelineStageFlags,
                 0,
                 1,
                 memoryBarrier,
@@ -243,10 +255,8 @@ namespace Ryujinx.Graphics.Vulkan
             Gd.Api.CmdClearAttachments(CommandBuffer, 1, &attachment, 1, &clearRect);
         }
 
-        public unsafe void ClearRenderTargetDepthStencil(int layer, int layerCount, float depthValue, bool depthMask, int stencilValue, int stencilMask)
+        public unsafe void ClearRenderTargetDepthStencil(int layer, int layerCount, float depthValue, bool depthMask, int stencilValue, bool stencilMask)
         {
-            // TODO: Use stencilMask (fully).
-
             if (FramebufferParams == null || !FramebufferParams.HasDepthStencil)
             {
                 return;
@@ -255,7 +265,7 @@ namespace Ryujinx.Graphics.Vulkan
             var clearValue = new ClearValue(null, new ClearDepthStencilValue(depthValue, (uint)stencilValue));
             var flags = depthMask ? ImageAspectFlags.DepthBit : 0;
 
-            if (stencilMask != 0)
+            if (stencilMask)
             {
                 flags |= ImageAspectFlags.StencilBit;
             }
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineConverter.cs b/src/Ryujinx.Graphics.Vulkan/PipelineConverter.cs
index 7c1ddef8d..b2da61031 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineConverter.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineConverter.cs
@@ -9,8 +9,12 @@ namespace Ryujinx.Graphics.Vulkan
 {
     static class PipelineConverter
     {
-        private const AccessFlags SubpassSrcAccessMask = AccessFlags.MemoryReadBit | AccessFlags.MemoryWriteBit | AccessFlags.ColorAttachmentWriteBit;
-        private const AccessFlags SubpassDstAccessMask = AccessFlags.MemoryReadBit | AccessFlags.MemoryWriteBit | AccessFlags.ShaderReadBit;
+        private const AccessFlags SubpassAccessMask =
+            AccessFlags.MemoryReadBit |
+            AccessFlags.MemoryWriteBit |
+            AccessFlags.ShaderReadBit |
+            AccessFlags.ColorAttachmentWriteBit |
+            AccessFlags.DepthStencilAttachmentWriteBit;
 
         public static unsafe DisposableRenderPass ToRenderPass(this ProgramPipelineState state, VulkanRenderer gd, Device device)
         {
@@ -132,8 +136,8 @@ namespace Ryujinx.Graphics.Vulkan
                 0,
                 PipelineStageFlags.AllGraphicsBit,
                 PipelineStageFlags.AllGraphicsBit,
-                SubpassSrcAccessMask,
-                SubpassDstAccessMask,
+                SubpassAccessMask,
+                SubpassAccessMask,
                 0);
         }
 
@@ -146,8 +150,8 @@ namespace Ryujinx.Graphics.Vulkan
                 0,
                 PipelineStageFlags.AllGraphicsBit,
                 PipelineStageFlags.AllGraphicsBit,
-                SubpassSrcAccessMask,
-                SubpassDstAccessMask,
+                SubpassAccessMask,
+                SubpassAccessMask,
                 0);
         }
 
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineDynamicState.cs b/src/Ryujinx.Graphics.Vulkan/PipelineDynamicState.cs
index ff88e4cf4..a5c218ac2 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineDynamicState.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineDynamicState.cs
@@ -146,7 +146,10 @@ namespace Ryujinx.Graphics.Vulkan
 
         private void RecordScissor(Vk api, CommandBuffer commandBuffer)
         {
-            api.CmdSetScissor(commandBuffer, 0, (uint)ScissorsCount, _scissors.AsSpan());
+            if (ScissorsCount != 0)
+            {
+                api.CmdSetScissor(commandBuffer, 0, (uint)ScissorsCount, _scissors.AsSpan());
+            }
         }
 
         private readonly void RecordStencilMasks(Vk api, CommandBuffer commandBuffer)
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs b/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs
index dcc6c5300..c3e6f37c3 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs
@@ -81,6 +81,42 @@ namespace Ryujinx.Graphics.Vulkan
             }
         }
 
+        public void ClearRenderTargetDepthStencil(int layer, int layerCount, float depthValue, bool depthMask, int stencilValue, int stencilMask)
+        {
+            if (FramebufferParams == null)
+            {
+                return;
+            }
+
+            if (stencilMask != 0 && stencilMask != 0xff)
+            {
+                // We can't use CmdClearAttachments if not clearing all (mask is all ones, 0xFF) or none (mask is 0) of the stencil bits,
+                // because on Vulkan, the pipeline state does not affect clears.
+                var dstTexture = FramebufferParams.GetDepthStencilAttachment();
+                if (dstTexture == null)
+                {
+                    return;
+                }
+
+                // TODO: Clear only the specified layer.
+                Gd.HelperShader.Clear(
+                    Gd,
+                    dstTexture,
+                    depthValue,
+                    depthMask,
+                    stencilValue,
+                    stencilMask,
+                    (int)FramebufferParams.Width,
+                    (int)FramebufferParams.Height,
+                    FramebufferParams.AttachmentFormats[FramebufferParams.AttachmentsCount - 1],
+                    ClearScissor);
+            }
+            else
+            {
+                ClearRenderTargetDepthStencil(layer, layerCount, depthValue, depthMask, stencilValue, stencilMask != 0);
+            }
+        }
+
         public void EndHostConditionalRendering()
         {
             if (Gd.Capabilities.SupportsConditionalRendering)
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs
index eeb25dc0f..2840dda0f 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs
@@ -1,5 +1,6 @@
 using Ryujinx.Graphics.GAL;
 using Silk.NET.Vulkan;
+using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 
@@ -7,15 +8,28 @@ namespace Ryujinx.Graphics.Vulkan
 {
     class PipelineLayoutCacheEntry
     {
+        // Those were adjusted based on current descriptor usage and the descriptor counts usually used on pipeline layouts.
+        // It might be a good idea to tweak them again if those change, or maybe find a way to calculate an optimal value dynamically.
+        private const uint DefaultUniformBufferPoolCapacity = 19 * DescriptorSetManager.MaxSets;
+        private const uint DefaultStorageBufferPoolCapacity = 16 * DescriptorSetManager.MaxSets;
+        private const uint DefaultTexturePoolCapacity = 128 * DescriptorSetManager.MaxSets;
+        private const uint DefaultImagePoolCapacity = 8 * DescriptorSetManager.MaxSets;
+
+        private const int MaxPoolSizesPerSet = 2;
+
         private readonly VulkanRenderer _gd;
         private readonly Device _device;
 
         public DescriptorSetLayout[] DescriptorSetLayouts { get; }
         public PipelineLayout PipelineLayout { get; }
 
+        private readonly int[] _consumedDescriptorsPerSet;
+
         private readonly List<Auto<DescriptorSetCollection>>[][] _dsCache;
+        private List<Auto<DescriptorSetCollection>>[] _currentDsCache;
         private readonly int[] _dsCacheCursor;
         private int _dsLastCbIndex;
+        private int _dsLastSubmissionCount;
 
         private PipelineLayoutCacheEntry(VulkanRenderer gd, Device device, int setsCount)
         {
@@ -44,29 +58,55 @@ namespace Ryujinx.Graphics.Vulkan
             bool usePushDescriptors) : this(gd, device, setDescriptors.Count)
         {
             (DescriptorSetLayouts, PipelineLayout) = PipelineLayoutFactory.Create(gd, device, setDescriptors, usePushDescriptors);
+
+            _consumedDescriptorsPerSet = new int[setDescriptors.Count];
+
+            for (int setIndex = 0; setIndex < setDescriptors.Count; setIndex++)
+            {
+                int count = 0;
+
+                foreach (var descriptor in setDescriptors[setIndex].Descriptors)
+                {
+                    count += descriptor.Count;
+                }
+
+                _consumedDescriptorsPerSet[setIndex] = count;
+            }
         }
 
-        public Auto<DescriptorSetCollection> GetNewDescriptorSetCollection(
-            VulkanRenderer gd,
-            int commandBufferIndex,
-            int setIndex,
-            out bool isNew)
+        public void UpdateCommandBufferIndex(int commandBufferIndex)
         {
-            if (_dsLastCbIndex != commandBufferIndex)
+            int submissionCount = _gd.CommandBufferPool.GetSubmissionCount(commandBufferIndex);
+
+            if (_dsLastCbIndex != commandBufferIndex || _dsLastSubmissionCount != submissionCount)
             {
                 _dsLastCbIndex = commandBufferIndex;
-
-                for (int i = 0; i < _dsCacheCursor.Length; i++)
-                {
-                    _dsCacheCursor[i] = 0;
-                }
+                _dsLastSubmissionCount = submissionCount;
+                Array.Clear(_dsCacheCursor);
             }
 
-            var list = _dsCache[commandBufferIndex][setIndex];
+            _currentDsCache = _dsCache[commandBufferIndex];
+        }
+
+        public Auto<DescriptorSetCollection> GetNewDescriptorSetCollection(int setIndex, out bool isNew)
+        {
+            var list = _currentDsCache[setIndex];
             int index = _dsCacheCursor[setIndex]++;
             if (index == list.Count)
             {
-                var dsc = gd.DescriptorSetManager.AllocateDescriptorSet(gd.Api, DescriptorSetLayouts[setIndex]);
+                Span<DescriptorPoolSize> poolSizes = stackalloc DescriptorPoolSize[MaxPoolSizesPerSet];
+                poolSizes = GetDescriptorPoolSizes(poolSizes, setIndex);
+
+                int consumedDescriptors = _consumedDescriptorsPerSet[setIndex];
+
+                var dsc = _gd.DescriptorSetManager.AllocateDescriptorSet(
+                    _gd.Api,
+                    DescriptorSetLayouts[setIndex],
+                    poolSizes,
+                    setIndex,
+                    consumedDescriptors,
+                    false);
+
                 list.Add(dsc);
                 isNew = true;
                 return dsc;
@@ -76,6 +116,33 @@ namespace Ryujinx.Graphics.Vulkan
             return list[index];
         }
 
+        private static Span<DescriptorPoolSize> GetDescriptorPoolSizes(Span<DescriptorPoolSize> output, int setIndex)
+        {
+            int count = 1;
+
+            switch (setIndex)
+            {
+                case PipelineBase.UniformSetIndex:
+                    output[0] = new(DescriptorType.UniformBuffer, DefaultUniformBufferPoolCapacity);
+                    break;
+                case PipelineBase.StorageSetIndex:
+                    output[0] = new(DescriptorType.StorageBuffer, DefaultStorageBufferPoolCapacity);
+                    break;
+                case PipelineBase.TextureSetIndex:
+                    output[0] = new(DescriptorType.CombinedImageSampler, DefaultTexturePoolCapacity);
+                    output[1] = new(DescriptorType.UniformTexelBuffer, DefaultTexturePoolCapacity);
+                    count = 2;
+                    break;
+                case PipelineBase.ImageSetIndex:
+                    output[0] = new(DescriptorType.StorageImage, DefaultImagePoolCapacity);
+                    output[1] = new(DescriptorType.StorageTexelBuffer, DefaultImagePoolCapacity);
+                    count = 2;
+                    break;
+            }
+
+            return output[..count];
+        }
+
         protected virtual unsafe void Dispose(bool disposing)
         {
             if (disposing)
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineState.cs b/src/Ryujinx.Graphics.Vulkan/PipelineState.cs
index cc9af5b6d..5a30cff8e 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineState.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineState.cs
@@ -352,11 +352,6 @@ namespace Ryujinx.Graphics.Vulkan
                 return pipeline;
             }
 
-            if (gd.Capabilities.SupportsSubgroupSizeControl)
-            {
-                UpdateStageRequiredSubgroupSizes(gd, 1);
-            }
-
             var pipelineCreateInfo = new ComputePipelineCreateInfo
             {
                 SType = StructureType.ComputePipelineCreateInfo,
@@ -616,11 +611,6 @@ namespace Ryujinx.Graphics.Vulkan
                     PDynamicStates = dynamicStates,
                 };
 
-                if (gd.Capabilities.SupportsSubgroupSizeControl)
-                {
-                    UpdateStageRequiredSubgroupSizes(gd, (int)StagesCount);
-                }
-
                 var pipelineCreateInfo = new GraphicsPipelineCreateInfo
                 {
                     SType = StructureType.GraphicsPipelineCreateInfo,
@@ -659,19 +649,6 @@ namespace Ryujinx.Graphics.Vulkan
             return pipeline;
         }
 
-        private readonly unsafe void UpdateStageRequiredSubgroupSizes(VulkanRenderer gd, int count)
-        {
-            for (int index = 0; index < count; index++)
-            {
-                bool canUseExplicitSubgroupSize =
-                    (gd.Capabilities.RequiredSubgroupSizeStages & Stages[index].Stage) != 0 &&
-                    gd.Capabilities.MinSubgroupSize <= RequiredSubgroupSize &&
-                    gd.Capabilities.MaxSubgroupSize >= RequiredSubgroupSize;
-
-                Stages[index].PNext = canUseExplicitSubgroupSize ? StageRequiredSubgroupSizes.Pointer + index : null;
-            }
-        }
-
         private void UpdateVertexAttributeDescriptions(VulkanRenderer gd)
         {
             // Vertex attributes exceeding the stride are invalid.
diff --git a/src/Ryujinx.Graphics.Vulkan/ResourceBindingSegment.cs b/src/Ryujinx.Graphics.Vulkan/ResourceBindingSegment.cs
index 9de46e614..8902f13e6 100644
--- a/src/Ryujinx.Graphics.Vulkan/ResourceBindingSegment.cs
+++ b/src/Ryujinx.Graphics.Vulkan/ResourceBindingSegment.cs
@@ -8,15 +8,13 @@ namespace Ryujinx.Graphics.Vulkan
         public readonly int Count;
         public readonly ResourceType Type;
         public readonly ResourceStages Stages;
-        public readonly ResourceAccess Access;
 
-        public ResourceBindingSegment(int binding, int count, ResourceType type, ResourceStages stages, ResourceAccess access)
+        public ResourceBindingSegment(int binding, int count, ResourceType type, ResourceStages stages)
         {
             Binding = binding;
             Count = count;
             Type = type;
             Stages = stages;
-            Access = access;
         }
     }
 }
diff --git a/src/Ryujinx.Graphics.Vulkan/ResourceLayoutBuilder.cs b/src/Ryujinx.Graphics.Vulkan/ResourceLayoutBuilder.cs
index 0b87d8000..f5ac39684 100644
--- a/src/Ryujinx.Graphics.Vulkan/ResourceLayoutBuilder.cs
+++ b/src/Ryujinx.Graphics.Vulkan/ResourceLayoutBuilder.cs
@@ -34,22 +34,12 @@ namespace Ryujinx.Graphics.Vulkan
                 _ => throw new ArgumentException($"Invalid resource type \"{type}\"."),
             };
 
-            ResourceAccess access = IsReadOnlyType(type) ? ResourceAccess.Read : ResourceAccess.ReadWrite;
-
             _resourceDescriptors[setIndex].Add(new ResourceDescriptor(binding, 1, type, stages));
-            _resourceUsages[setIndex].Add(new ResourceUsage(binding, type, stages, access));
+            _resourceUsages[setIndex].Add(new ResourceUsage(binding, type, stages));
 
             return this;
         }
 
-        private static bool IsReadOnlyType(ResourceType type)
-        {
-            return type == ResourceType.UniformBuffer ||
-                   type == ResourceType.Sampler ||
-                   type == ResourceType.TextureAndSampler ||
-                   type == ResourceType.BufferTexture;
-        }
-
         public ResourceLayout Build()
         {
             var descriptors = new ResourceDescriptorCollection[TotalSets];
diff --git a/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj b/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj
index 343951186..8d30457e2 100644
--- a/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj
+++ b/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj
@@ -42,6 +42,7 @@
     <EmbeddedResource Include="Shaders\SpirvBinaries\DepthBlitMsFragment.spv" />
     <EmbeddedResource Include="Shaders\SpirvBinaries\DepthDrawToMsFragment.spv" />
     <EmbeddedResource Include="Shaders\SpirvBinaries\DepthDrawToNonMsFragment.spv" />
+    <EmbeddedResource Include="Shaders\SpirvBinaries\DepthStencilClearFragment.spv" />
     <EmbeddedResource Include="Shaders\SpirvBinaries\StencilBlitFragment.spv" />
     <EmbeddedResource Include="Shaders\SpirvBinaries\StencilBlitMsFragment.spv" />
     <EmbeddedResource Include="Shaders\SpirvBinaries\StencilDrawToMsFragment.spv" />
diff --git a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs
index 1e56d1e89..0cb80ac71 100644
--- a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs
+++ b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs
@@ -162,8 +162,7 @@ namespace Ryujinx.Graphics.Vulkan
                                 currentDescriptor.Binding,
                                 currentCount,
                                 currentDescriptor.Type,
-                                currentDescriptor.Stages,
-                                ResourceAccess.ReadWrite));
+                                currentDescriptor.Stages));
                         }
 
                         currentDescriptor = descriptor;
@@ -181,8 +180,7 @@ namespace Ryujinx.Graphics.Vulkan
                         currentDescriptor.Binding,
                         currentCount,
                         currentDescriptor.Type,
-                        currentDescriptor.Stages,
-                        ResourceAccess.ReadWrite));
+                        currentDescriptor.Stages));
                 }
 
                 segments[setIndex] = currentSegments.ToArray();
@@ -206,16 +204,9 @@ namespace Ryujinx.Graphics.Vulkan
                 {
                     ResourceUsage usage = setUsages[setIndex].Usages[index];
 
-                    // If the resource is not accessed, we don't need to update it.
-                    if (usage.Access == ResourceAccess.None)
-                    {
-                        continue;
-                    }
-
                     if (currentUsage.Binding + currentCount != usage.Binding ||
                         currentUsage.Type != usage.Type ||
-                        currentUsage.Stages != usage.Stages ||
-                        currentUsage.Access != usage.Access)
+                        currentUsage.Stages != usage.Stages)
                     {
                         if (currentCount != 0)
                         {
@@ -223,8 +214,7 @@ namespace Ryujinx.Graphics.Vulkan
                                 currentUsage.Binding,
                                 currentCount,
                                 currentUsage.Type,
-                                currentUsage.Stages,
-                                currentUsage.Access));
+                                currentUsage.Stages));
                         }
 
                         currentUsage = usage;
@@ -242,8 +232,7 @@ namespace Ryujinx.Graphics.Vulkan
                         currentUsage.Binding,
                         currentCount,
                         currentUsage.Type,
-                        currentUsage.Stages,
-                        currentUsage.Access));
+                        currentUsage.Stages));
                 }
 
                 segments[setIndex] = currentSegments.ToArray();
@@ -475,13 +464,14 @@ namespace Ryujinx.Graphics.Vulkan
             return true;
         }
 
-        public Auto<DescriptorSetCollection> GetNewDescriptorSetCollection(
-            VulkanRenderer gd,
-            int commandBufferIndex,
-            int setIndex,
-            out bool isNew)
+        public void UpdateDescriptorCacheCommandBufferIndex(int commandBufferIndex)
         {
-            return _plce.GetNewDescriptorSetCollection(gd, commandBufferIndex, setIndex, out isNew);
+            _plce.UpdateCommandBufferIndex(commandBufferIndex);
+        }
+
+        public Auto<DescriptorSetCollection> GetNewDescriptorSetCollection(int setIndex, out bool isNew)
+        {
+            return _plce.GetNewDescriptorSetCollection(setIndex, out isNew);
         }
 
         protected virtual void Dispose(bool disposing)
diff --git a/src/Ryujinx.Graphics.Vulkan/Shaders/ChangeBufferStrideShaderSource.comp b/src/Ryujinx.Graphics.Vulkan/Shaders/ChangeBufferStrideShaderSource.comp
index 081fc119f..4deba30d9 100644
--- a/src/Ryujinx.Graphics.Vulkan/Shaders/ChangeBufferStrideShaderSource.comp
+++ b/src/Ryujinx.Graphics.Vulkan/Shaders/ChangeBufferStrideShaderSource.comp
@@ -29,7 +29,7 @@ void main()
     int sourceOffset = stride_arguments_data.w;
 
     int strideRemainder = targetStride - sourceStride;
-    int invocations = int(gl_WorkGroupSize.x);
+    int invocations = int(gl_WorkGroupSize.x * gl_NumWorkGroups.x);
 
     int copiesRequired = bufferSize / sourceStride;
 
@@ -39,7 +39,7 @@ void main()
     int allInvocationCopies = copiesRequired / invocations;
 
     // - Extra remainder copy that this invocation performs.
-    int index = int(gl_LocalInvocationID.x);
+    int index = int(gl_GlobalInvocationID.x);
     int extra = (index < (copiesRequired % invocations)) ? 1 : 0;
 
     int copyCount = allInvocationCopies + extra;
diff --git a/src/Ryujinx.Graphics.Vulkan/Shaders/ConvertD32S8ToD24S8ShaderSource.comp b/src/Ryujinx.Graphics.Vulkan/Shaders/ConvertD32S8ToD24S8ShaderSource.comp
index d3a74b1c8..96cbdebbb 100644
--- a/src/Ryujinx.Graphics.Vulkan/Shaders/ConvertD32S8ToD24S8ShaderSource.comp
+++ b/src/Ryujinx.Graphics.Vulkan/Shaders/ConvertD32S8ToD24S8ShaderSource.comp
@@ -23,7 +23,7 @@ layout (std430, set = 1, binding = 2) buffer out_s
 void main()
 {
     // Determine what slice of the stride copies this invocation will perform.
-    int invocations = int(gl_WorkGroupSize.x);
+    int invocations = int(gl_WorkGroupSize.x * gl_NumWorkGroups.x);
 
     int copiesRequired = pixelCount;
 
@@ -33,7 +33,7 @@ void main()
     int allInvocationCopies = copiesRequired / invocations;
 
     // - Extra remainder copy that this invocation performs.
-    int index = int(gl_LocalInvocationID.x);
+    int index = int(gl_GlobalInvocationID.x);
     int extra = (index < (copiesRequired % invocations)) ? 1 : 0;
 
     int copyCount = allInvocationCopies + extra;
diff --git a/src/Ryujinx.Graphics.Vulkan/Shaders/DepthStencilClearFragmentShaderSource.frag b/src/Ryujinx.Graphics.Vulkan/Shaders/DepthStencilClearFragmentShaderSource.frag
new file mode 100644
index 000000000..689a0fffb
--- /dev/null
+++ b/src/Ryujinx.Graphics.Vulkan/Shaders/DepthStencilClearFragmentShaderSource.frag
@@ -0,0 +1,8 @@
+#version 450 core
+
+layout (location = 0) in vec4 clear_colour;
+
+void main()
+{
+    gl_FragDepth = clear_colour.x;
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Vulkan/Shaders/SpirvBinaries/ChangeBufferStride.spv b/src/Ryujinx.Graphics.Vulkan/Shaders/SpirvBinaries/ChangeBufferStride.spv
index 49e7d23f9..1e0d8810f 100644
Binary files a/src/Ryujinx.Graphics.Vulkan/Shaders/SpirvBinaries/ChangeBufferStride.spv and b/src/Ryujinx.Graphics.Vulkan/Shaders/SpirvBinaries/ChangeBufferStride.spv differ
diff --git a/src/Ryujinx.Graphics.Vulkan/Shaders/SpirvBinaries/ConvertD32S8ToD24S8.spv b/src/Ryujinx.Graphics.Vulkan/Shaders/SpirvBinaries/ConvertD32S8ToD24S8.spv
index fb543e443..d8e19ff68 100644
Binary files a/src/Ryujinx.Graphics.Vulkan/Shaders/SpirvBinaries/ConvertD32S8ToD24S8.spv and b/src/Ryujinx.Graphics.Vulkan/Shaders/SpirvBinaries/ConvertD32S8ToD24S8.spv differ
diff --git a/src/Ryujinx.Graphics.Vulkan/Shaders/SpirvBinaries/DepthStencilClearFragment.spv b/src/Ryujinx.Graphics.Vulkan/Shaders/SpirvBinaries/DepthStencilClearFragment.spv
new file mode 100644
index 000000000..dcd3235b6
Binary files /dev/null and b/src/Ryujinx.Graphics.Vulkan/Shaders/SpirvBinaries/DepthStencilClearFragment.spv differ
diff --git a/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs b/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs
index 090f69dca..5a6216c22 100644
--- a/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs
+++ b/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs
@@ -78,7 +78,7 @@ namespace Ryujinx.Graphics.Vulkan
 
             var sampleCountFlags = ConvertToSampleCountFlags(gd.Capabilities.SupportedSampleCounts, (uint)info.Samples);
 
-            var usage = GetImageUsage(info.Format, info.Target, gd.Capabilities.SupportsShaderStorageImageMultisample, forceStorage: true);
+            var usage = GetImageUsage(info.Format, info.Target, gd.Capabilities.SupportsShaderStorageImageMultisample);
 
             var flags = ImageCreateFlags.CreateMutableFormatBit;
 
@@ -291,7 +291,7 @@ namespace Ryujinx.Graphics.Vulkan
             }
         }
 
-        public static ImageUsageFlags GetImageUsage(Format format, Target target, bool supportsMsStorage, bool forceStorage = false)
+        public static ImageUsageFlags GetImageUsage(Format format, Target target, bool supportsMsStorage)
         {
             var usage = DefaultUsageFlags;
 
@@ -304,7 +304,7 @@ namespace Ryujinx.Graphics.Vulkan
                 usage |= ImageUsageFlags.ColorAttachmentBit;
             }
 
-            if (((forceStorage && !format.IsDepthOrStencil()) || format.IsImageCompatible()) && (supportsMsStorage || !target.IsMultisample()))
+            if (format.IsImageCompatible() && (supportsMsStorage || !target.IsMultisample()))
             {
                 usage |= ImageUsageFlags.StorageBit;
             }
@@ -440,25 +440,50 @@ namespace Ryujinx.Graphics.Vulkan
             _lastModificationStage = stage;
         }
 
-        public void InsertReadToWriteBarrier(CommandBufferScoped cbs, AccessFlags dstAccessFlags, PipelineStageFlags dstStageFlags)
+        public void InsertReadToWriteBarrier(CommandBufferScoped cbs, AccessFlags dstAccessFlags, PipelineStageFlags dstStageFlags, bool insideRenderPass)
         {
-            if (_lastReadAccess != AccessFlags.None)
-            {
-                ImageAspectFlags aspectFlags = Info.Format.ConvertAspectFlags();
+            var lastReadStage = _lastReadStage;
 
-                TextureView.InsertImageBarrier(
-                    _gd.Api,
-                    cbs.CommandBuffer,
-                    _imageAuto.Get(cbs).Value,
-                    _lastReadAccess,
-                    dstAccessFlags,
-                    _lastReadStage,
-                    dstStageFlags,
-                    aspectFlags,
-                    0,
-                    0,
-                    _info.GetLayers(),
-                    _info.Levels);
+            if (insideRenderPass)
+            {
+                // We can't have barrier from compute inside a render pass,
+                // as it is invalid to specify compute in the subpass dependency stage mask.
+
+                lastReadStage &= ~PipelineStageFlags.ComputeShaderBit;
+            }
+
+            if (lastReadStage != PipelineStageFlags.None)
+            {
+                // This would result in a validation error, but is
+                // required on MoltenVK as the generic barrier results in
+                // severe texture flickering in some scenarios.
+                if (_gd.IsMoltenVk)
+                {
+                    ImageAspectFlags aspectFlags = Info.Format.ConvertAspectFlags();
+                    TextureView.InsertImageBarrier(
+                        _gd.Api,
+                        cbs.CommandBuffer,
+                        _imageAuto.Get(cbs).Value,
+                        _lastReadAccess,
+                        dstAccessFlags,
+                        _lastReadStage,
+                        dstStageFlags,
+                        aspectFlags,
+                        0,
+                        0,
+                        _info.GetLayers(),
+                        _info.Levels);
+                }
+                else
+                {
+                    TextureView.InsertMemoryBarrier(
+                        _gd.Api,
+                        cbs.CommandBuffer,
+                        _lastReadAccess,
+                        dstAccessFlags,
+                        lastReadStage,
+                        dstStageFlags);
+                }
 
                 _lastReadAccess = AccessFlags.None;
                 _lastReadStage = PipelineStageFlags.None;
@@ -472,21 +497,36 @@ namespace Ryujinx.Graphics.Vulkan
 
             if (_lastModificationAccess != AccessFlags.None)
             {
-                ImageAspectFlags aspectFlags = Info.Format.ConvertAspectFlags();
-
-                TextureView.InsertImageBarrier(
-                    _gd.Api,
-                    cbs.CommandBuffer,
-                    _imageAuto.Get(cbs).Value,
-                    _lastModificationAccess,
-                    dstAccessFlags,
-                    _lastModificationStage,
-                    dstStageFlags,
-                    aspectFlags,
-                    0,
-                    0,
-                    _info.GetLayers(),
-                    _info.Levels);
+                // This would result in a validation error, but is
+                // required on MoltenVK as the generic barrier results in
+                // severe texture flickering in some scenarios.
+                if (_gd.IsMoltenVk)
+                {
+                    ImageAspectFlags aspectFlags = Info.Format.ConvertAspectFlags();
+                    TextureView.InsertImageBarrier(
+                        _gd.Api,
+                        cbs.CommandBuffer,
+                        _imageAuto.Get(cbs).Value,
+                        _lastModificationAccess,
+                        dstAccessFlags,
+                        _lastModificationStage,
+                        dstStageFlags,
+                        aspectFlags,
+                        0,
+                        0,
+                        _info.GetLayers(),
+                        _info.Levels);
+                }
+                else
+                {
+                    TextureView.InsertMemoryBarrier(
+                        _gd.Api,
+                        cbs.CommandBuffer,
+                        _lastModificationAccess,
+                        dstAccessFlags,
+                        _lastModificationStage,
+                        dstStageFlags);
+                }
 
                 _lastModificationAccess = AccessFlags.None;
             }
diff --git a/src/Ryujinx.Graphics.Vulkan/TextureView.cs b/src/Ryujinx.Graphics.Vulkan/TextureView.cs
index 9fc50f67a..09128f007 100644
--- a/src/Ryujinx.Graphics.Vulkan/TextureView.cs
+++ b/src/Ryujinx.Graphics.Vulkan/TextureView.cs
@@ -435,6 +435,34 @@ namespace Ryujinx.Graphics.Vulkan
                 ImageAspectFlags.ColorBit);
         }
 
+        public static unsafe void InsertMemoryBarrier(
+            Vk api,
+            CommandBuffer commandBuffer,
+            AccessFlags srcAccessMask,
+            AccessFlags dstAccessMask,
+            PipelineStageFlags srcStageMask,
+            PipelineStageFlags dstStageMask)
+        {
+            MemoryBarrier memoryBarrier = new()
+            {
+                SType = StructureType.MemoryBarrier,
+                SrcAccessMask = srcAccessMask,
+                DstAccessMask = dstAccessMask,
+            };
+
+            api.CmdPipelineBarrier(
+                commandBuffer,
+                srcStageMask,
+                dstStageMask,
+                DependencyFlags.None,
+                1,
+                memoryBarrier,
+                0,
+                null,
+                0,
+                null);
+        }
+
         public static unsafe void InsertImageBarrier(
             Vk api,
             CommandBuffer commandBuffer,
diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs
index 6f73397b8..973c6d396 100644
--- a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs
+++ b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs
@@ -37,7 +37,6 @@ namespace Ryujinx.Graphics.Vulkan
             "VK_EXT_shader_stencil_export",
             "VK_KHR_shader_float16_int8",
             "VK_EXT_shader_subgroup_ballot",
-            "VK_EXT_subgroup_size_control",
             "VK_NV_geometry_shader_passthrough",
             "VK_NV_viewport_array2",
             "VK_EXT_depth_clip_control",
diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
index a3ce05e4d..0b824d7c3 100644
--- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
+++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
@@ -151,6 +151,14 @@ namespace Ryujinx.Graphics.Vulkan
                 SType = StructureType.PhysicalDeviceProperties2,
             };
 
+            PhysicalDeviceSubgroupProperties propertiesSubgroup = new()
+            {
+                SType = StructureType.PhysicalDeviceSubgroupProperties,
+                PNext = properties2.PNext,
+            };
+
+            properties2.PNext = &propertiesSubgroup;
+
             PhysicalDeviceBlendOperationAdvancedPropertiesEXT propertiesBlendOperationAdvanced = new()
             {
                 SType = StructureType.PhysicalDeviceBlendOperationAdvancedPropertiesExt,
@@ -164,18 +172,6 @@ namespace Ryujinx.Graphics.Vulkan
                 properties2.PNext = &propertiesBlendOperationAdvanced;
             }
 
-            PhysicalDeviceSubgroupSizeControlPropertiesEXT propertiesSubgroupSizeControl = new()
-            {
-                SType = StructureType.PhysicalDeviceSubgroupSizeControlPropertiesExt,
-            };
-
-            bool supportsSubgroupSizeControl = _physicalDevice.IsDeviceExtensionPresent("VK_EXT_subgroup_size_control");
-
-            if (supportsSubgroupSizeControl)
-            {
-                properties2.PNext = &propertiesSubgroupSizeControl;
-            }
-
             bool supportsTransformFeedback = _physicalDevice.IsDeviceExtensionPresent(ExtTransformFeedback.ExtensionName);
 
             PhysicalDeviceTransformFeedbackPropertiesEXT propertiesTransformFeedback = new()
@@ -315,7 +311,6 @@ namespace Ryujinx.Graphics.Vulkan
                 _physicalDevice.IsDeviceExtensionPresent(KhrDrawIndirectCount.ExtensionName),
                 _physicalDevice.IsDeviceExtensionPresent("VK_EXT_fragment_shader_interlock"),
                 _physicalDevice.IsDeviceExtensionPresent("VK_NV_geometry_shader_passthrough"),
-                supportsSubgroupSizeControl,
                 features2.Features.ShaderFloat64,
                 featuresShaderInt8.ShaderInt8,
                 _physicalDevice.IsDeviceExtensionPresent("VK_EXT_shader_stencil_export"),
@@ -332,12 +327,11 @@ namespace Ryujinx.Graphics.Vulkan
                 features2.Features.OcclusionQueryPrecise,
                 _physicalDevice.PhysicalDeviceFeatures.PipelineStatisticsQuery,
                 _physicalDevice.PhysicalDeviceFeatures.GeometryShader,
+                _physicalDevice.PhysicalDeviceFeatures.TessellationShader,
                 _physicalDevice.IsDeviceExtensionPresent("VK_NV_viewport_array2"),
                 _physicalDevice.IsDeviceExtensionPresent(ExtExternalMemoryHost.ExtensionName),
                 supportsDepthClipControl && featuresDepthClipControl.DepthClipControl,
-                propertiesSubgroupSizeControl.MinSubgroupSize,
-                propertiesSubgroupSizeControl.MaxSubgroupSize,
-                propertiesSubgroupSizeControl.RequiredSubgroupSizeStages,
+                propertiesSubgroup.SubgroupSize,
                 supportedSampleCounts,
                 portabilityFlags,
                 vertexBufferAlignment,
@@ -353,7 +347,7 @@ namespace Ryujinx.Graphics.Vulkan
 
             CommandBufferPool = new CommandBufferPool(Api, _device, Queue, QueueLock, queueFamilyIndex);
 
-            DescriptorSetManager = new DescriptorSetManager(_device);
+            DescriptorSetManager = new DescriptorSetManager(_device, PipelineBase.DescriptorSetLayouts);
 
             PipelineLayoutCache = new PipelineLayoutCache();
 
@@ -612,6 +606,7 @@ namespace Ryujinx.Graphics.Vulkan
                 supportsShaderBarrierDivergence: Vendor != Vendor.Intel,
                 supportsShaderFloat64: Capabilities.SupportsShaderFloat64,
                 supportsTextureShadowLod: false,
+                supportsVertexStoreAndAtomics: features2.Features.VertexPipelineStoresAndAtomics,
                 supportsViewportIndexVertexTessellation: featuresVk12.ShaderOutputViewportIndex,
                 supportsViewportMask: Capabilities.SupportsViewportArray2,
                 supportsViewportSwizzle: false,
@@ -624,7 +619,9 @@ namespace Ryujinx.Graphics.Vulkan
                 maximumImagesPerStage: Constants.MaxImagesPerStage,
                 maximumComputeSharedMemorySize: (int)limits.MaxComputeSharedMemorySize,
                 maximumSupportedAnisotropy: (int)limits.MaxSamplerAnisotropy,
+                shaderSubgroupSize: (int)Capabilities.SubgroupSize,
                 storageBufferOffsetAlignment: (int)limits.MinStorageBufferOffsetAlignment,
+                textureBufferOffsetAlignment: (int)limits.MinTexelBufferOffsetAlignment,
                 gatherBiasPrecision: IsIntelWindows || IsAmdWindows ? (int)Capabilities.SubTexelPrecisionBits : 0);
         }
 
diff --git a/src/Ryujinx.Graphics.Vulkan/Window.cs b/src/Ryujinx.Graphics.Vulkan/Window.cs
index 2d0ad664c..afaa7defe 100644
--- a/src/Ryujinx.Graphics.Vulkan/Window.cs
+++ b/src/Ryujinx.Graphics.Vulkan/Window.cs
@@ -22,8 +22,10 @@ namespace Ryujinx.Graphics.Vulkan
         private Image[] _swapchainImages;
         private Auto<DisposableImageView>[] _swapchainImageViews;
 
-        private Semaphore _imageAvailableSemaphore;
-        private Semaphore _renderFinishedSemaphore;
+        private Semaphore[] _imageAvailableSemaphores;
+        private Semaphore[] _renderFinishedSemaphores;
+
+        private int _frameIndex;
 
         private int _width;
         private int _height;
@@ -48,14 +50,6 @@ namespace Ryujinx.Graphics.Vulkan
             _surface = surface;
 
             CreateSwapchain();
-
-            var semaphoreCreateInfo = new SemaphoreCreateInfo
-            {
-                SType = StructureType.SemaphoreCreateInfo,
-            };
-
-            gd.Api.CreateSemaphore(device, semaphoreCreateInfo, null, out _imageAvailableSemaphore).ThrowOnError();
-            gd.Api.CreateSemaphore(device, semaphoreCreateInfo, null, out _renderFinishedSemaphore).ThrowOnError();
         }
 
         private void RecreateSwapchain()
@@ -69,7 +63,22 @@ namespace Ryujinx.Graphics.Vulkan
             }
 
             // Destroy old Swapchain.
+
             _gd.Api.DeviceWaitIdle(_device);
+
+            unsafe
+            {
+                for (int i = 0; i < _imageAvailableSemaphores.Length; i++)
+                {
+                    _gd.Api.DestroySemaphore(_device, _imageAvailableSemaphores[i], null);
+                }
+
+                for (int i = 0; i < _renderFinishedSemaphores.Length; i++)
+                {
+                    _gd.Api.DestroySemaphore(_device, _renderFinishedSemaphores[i], null);
+                }
+            }
+
             _gd.SwapchainApi.DestroySwapchain(_device, oldSwapchain, Span<AllocationCallbacks>.Empty);
 
             CreateSwapchain();
@@ -151,6 +160,25 @@ namespace Ryujinx.Graphics.Vulkan
             {
                 _swapchainImageViews[i] = CreateSwapchainImageView(_swapchainImages[i], surfaceFormat.Format);
             }
+
+            var semaphoreCreateInfo = new SemaphoreCreateInfo
+            {
+                SType = StructureType.SemaphoreCreateInfo,
+            };
+
+            _imageAvailableSemaphores = new Semaphore[imageCount];
+
+            for (int i = 0; i < _imageAvailableSemaphores.Length; i++)
+            {
+                _gd.Api.CreateSemaphore(_device, semaphoreCreateInfo, null, out _imageAvailableSemaphores[i]).ThrowOnError();
+            }
+
+            _renderFinishedSemaphores = new Semaphore[imageCount];
+
+            for (int i = 0; i < _renderFinishedSemaphores.Length; i++)
+            {
+                _gd.Api.CreateSemaphore(_device, semaphoreCreateInfo, null, out _renderFinishedSemaphores[i]).ThrowOnError();
+            }
         }
 
         private unsafe Auto<DisposableImageView> CreateSwapchainImageView(Image swapchainImage, VkFormat format)
@@ -185,6 +213,7 @@ namespace Ryujinx.Graphics.Vulkan
             {
                 return new SurfaceFormatKHR(VkFormat.B8G8R8A8Unorm, ColorSpaceKHR.PaceSrgbNonlinearKhr);
             }
+
             var formatToReturn = availableFormats[0];
             if (colorSpacePassthroughEnabled)
             {
@@ -212,6 +241,7 @@ namespace Ryujinx.Graphics.Vulkan
                     }
                 }
             }
+
             return formatToReturn;
         }
 
@@ -265,6 +295,7 @@ namespace Ryujinx.Graphics.Vulkan
             _gd.PipelineInternal.AutoFlush.Present();
 
             uint nextImage = 0;
+            int semaphoreIndex = _frameIndex++ % _imageAvailableSemaphores.Length;
 
             while (true)
             {
@@ -272,7 +303,7 @@ namespace Ryujinx.Graphics.Vulkan
                     _device,
                     _swapchain,
                     ulong.MaxValue,
-                    _imageAvailableSemaphore,
+                    _imageAvailableSemaphores[semaphoreIndex],
                     new Fence(),
                     ref nextImage);
 
@@ -411,12 +442,12 @@ namespace Ryujinx.Graphics.Vulkan
 
             _gd.CommandBufferPool.Return(
                 cbs,
-                stackalloc[] { _imageAvailableSemaphore },
+                stackalloc[] { _imageAvailableSemaphores[semaphoreIndex] },
                 stackalloc[] { PipelineStageFlags.ColorAttachmentOutputBit },
-                stackalloc[] { _renderFinishedSemaphore });
+                stackalloc[] { _renderFinishedSemaphores[semaphoreIndex] });
 
             // TODO: Present queue.
-            var semaphore = _renderFinishedSemaphore;
+            var semaphore = _renderFinishedSemaphores[semaphoreIndex];
             var swapchain = _swapchain;
 
             Result result;
@@ -593,14 +624,21 @@ namespace Ryujinx.Graphics.Vulkan
             {
                 unsafe
                 {
-                    _gd.Api.DestroySemaphore(_device, _renderFinishedSemaphore, null);
-                    _gd.Api.DestroySemaphore(_device, _imageAvailableSemaphore, null);
-
                     for (int i = 0; i < _swapchainImageViews.Length; i++)
                     {
                         _swapchainImageViews[i].Dispose();
                     }
 
+                    for (int i = 0; i < _imageAvailableSemaphores.Length; i++)
+                    {
+                        _gd.Api.DestroySemaphore(_device, _imageAvailableSemaphores[i], null);
+                    }
+
+                    for (int i = 0; i < _renderFinishedSemaphores.Length; i++)
+                    {
+                        _gd.Api.DestroySemaphore(_device, _renderFinishedSemaphores[i], null);
+                    }
+
                     _gd.SwapchainApi.DestroySwapchain(_device, _swapchain, null);
                 }
 
diff --git a/src/Ryujinx.HLE/FileSystem/ContentManager.cs b/src/Ryujinx.HLE/FileSystem/ContentManager.cs
index 13f987638..646808e78 100644
--- a/src/Ryujinx.HLE/FileSystem/ContentManager.cs
+++ b/src/Ryujinx.HLE/FileSystem/ContentManager.cs
@@ -19,6 +19,7 @@ using System.Collections.Generic;
 using System.IO;
 using System.IO.Compression;
 using System.Linq;
+using System.Text;
 using Path = System.IO.Path;
 
 namespace Ryujinx.HLE.FileSystem
@@ -817,13 +818,13 @@ namespace Ryujinx.HLE.FileSystem
 
                     if (updateNcas.Count > 0)
                     {
-                        string extraNcas = string.Empty;
+                        StringBuilder extraNcas = new();
 
                         foreach (var entry in updateNcas)
                         {
                             foreach (var (type, path) in entry.Value)
                             {
-                                extraNcas += path + Environment.NewLine;
+                                extraNcas.AppendLine(path);
                             }
                         }
 
@@ -954,13 +955,13 @@ namespace Ryujinx.HLE.FileSystem
 
                 if (updateNcas.Count > 0)
                 {
-                    string extraNcas = string.Empty;
+                    StringBuilder extraNcas = new();
 
                     foreach (var entry in updateNcas)
                     {
                         foreach (var (type, path) in entry.Value)
                         {
-                            extraNcas += path + Environment.NewLine;
+                            extraNcas.AppendLine(path);
                         }
                     }
 
diff --git a/src/Ryujinx.HLE/HLEConfiguration.cs b/src/Ryujinx.HLE/HLEConfiguration.cs
index 22477a8c0..b1ba11b59 100644
--- a/src/Ryujinx.HLE/HLEConfiguration.cs
+++ b/src/Ryujinx.HLE/HLEConfiguration.cs
@@ -1,6 +1,7 @@
 using LibHac.Tools.FsSystem;
 using Ryujinx.Audio.Integration;
 using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Configuration.Multiplayer;
 using Ryujinx.Graphics.GAL;
 using Ryujinx.HLE.FileSystem;
 using Ryujinx.HLE.HOS;
@@ -158,6 +159,11 @@ namespace Ryujinx.HLE
         /// </summary>
         public string MultiplayerLanInterfaceId { internal get; set; }
 
+        /// <summary>
+        /// Multiplayer Mode
+        /// </summary>
+        public MultiplayerMode MultiplayerMode { internal get; set; }
+
         /// <summary>
         /// An action called when HLE force a refresh of output after docked mode changed.
         /// </summary>
@@ -187,7 +193,8 @@ namespace Ryujinx.HLE
                                 AspectRatio aspectRatio,
                                 float audioVolume,
                                 bool useHypervisor,
-                                string multiplayerLanInterfaceId)
+                                string multiplayerLanInterfaceId,
+                                MultiplayerMode multiplayerMode)
         {
             VirtualFileSystem = virtualFileSystem;
             LibHacHorizonManager = libHacHorizonManager;
@@ -214,6 +221,7 @@ namespace Ryujinx.HLE
             AudioVolume = audioVolume;
             UseHypervisor = useHypervisor;
             MultiplayerLanInterfaceId = multiplayerLanInterfaceId;
+            MultiplayerMode = multiplayerMode;
         }
     }
 }
diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/KeyboardMode.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/KeyboardMode.cs
index 917e1f9e1..c2dfc31a3 100644
--- a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/KeyboardMode.cs
+++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/KeyboardMode.cs
@@ -11,9 +11,9 @@
         Default = 0,
 
         /// <summary>
-        /// Only numbers allowed.
+        /// Only 0-9 or '.' allowed.
         /// </summary>
-        NumbersOnly = 1,
+        Numeric = 1,
 
         /// <summary>
         /// Only ASCII characters allowed.
diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/NumericCharacterValidation.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/NumericCharacterValidation.cs
new file mode 100644
index 000000000..d72b68eae
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/NumericCharacterValidation.cs
@@ -0,0 +1,17 @@
+using System.Text.RegularExpressions;
+
+namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
+{
+    public static partial class NumericCharacterValidation
+    {
+        public static bool IsNumeric(char value)
+        {
+            Regex regex = NumericRegex();
+
+            return regex.IsMatch(value.ToString());
+        }
+
+        [GeneratedRegex("[0-9]|.")]
+        private static partial Regex NumericRegex();
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs
index 770bf6e5a..2f4fd2a89 100644
--- a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs
+++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs
@@ -68,15 +68,15 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
             int ryujinxLogoSize = 32;
 
             string ryujinxIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Logo_Ryujinx.png";
-            _ryujinxLogo = LoadResource(Assembly.GetExecutingAssembly(), ryujinxIconPath, ryujinxLogoSize, ryujinxLogoSize);
+            _ryujinxLogo = LoadResource(typeof(SoftwareKeyboardRendererBase).Assembly, ryujinxIconPath, ryujinxLogoSize, ryujinxLogoSize);
 
             string padAcceptIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_BtnA.png";
             string padCancelIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_BtnB.png";
             string keyModeIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_KeyF6.png";
 
-            _padAcceptIcon = LoadResource(Assembly.GetExecutingAssembly(), padAcceptIconPath, 0, 0);
-            _padCancelIcon = LoadResource(Assembly.GetExecutingAssembly(), padCancelIconPath, 0, 0);
-            _keyModeIcon = LoadResource(Assembly.GetExecutingAssembly(), keyModeIconPath, 0, 0);
+            _padAcceptIcon = LoadResource(typeof(SoftwareKeyboardRendererBase).Assembly, padAcceptIconPath, 0, 0);
+            _padCancelIcon = LoadResource(typeof(SoftwareKeyboardRendererBase).Assembly, padCancelIconPath, 0, 0);
+            _keyModeIcon = LoadResource(typeof(SoftwareKeyboardRendererBase).Assembly, keyModeIconPath, 0, 0);
 
             Color panelColor = ToColor(uiTheme.DefaultBackgroundColor, 255);
             Color panelTransparentColor = ToColor(uiTheme.DefaultBackgroundColor, 150);
diff --git a/src/Ryujinx.HLE/HOS/Horizon.cs b/src/Ryujinx.HLE/HOS/Horizon.cs
index f65d357e7..1a402240f 100644
--- a/src/Ryujinx.HLE/HOS/Horizon.cs
+++ b/src/Ryujinx.HLE/HOS/Horizon.cs
@@ -88,6 +88,7 @@ namespace Ryujinx.HLE.HOS
         internal ServerBase ViServer { get; private set; }
         internal ServerBase ViServerM { get; private set; }
         internal ServerBase ViServerS { get; private set; }
+        internal ServerBase LdnServer { get; private set; }
 
         internal KSharedMemory HidSharedMem { get; private set; }
         internal KSharedMemory FontSharedMem { get; private set; }
@@ -319,14 +320,17 @@ namespace Ryujinx.HLE.HOS
             ViServer = new ServerBase(KernelContext, "ViServerU");
             ViServerM = new ServerBase(KernelContext, "ViServerM");
             ViServerS = new ServerBase(KernelContext, "ViServerS");
+            LdnServer = new ServerBase(KernelContext, "LdnServer");
 
             StartNewServices();
         }
 
         private void StartNewServices()
         {
+            HorizonFsClient fsClient = new(this);
+
             ServiceTable = new ServiceTable();
-            var services = ServiceTable.GetServices(new HorizonOptions(Device.Configuration.IgnoreMissingServices, LibHacHorizonManager.BcatClient));
+            var services = ServiceTable.GetServices(new HorizonOptions(Device.Configuration.IgnoreMissingServices, LibHacHorizonManager.BcatClient, fsClient));
 
             foreach (var service in services)
             {
diff --git a/src/Ryujinx.HLE/HOS/HorizonFsClient.cs b/src/Ryujinx.HLE/HOS/HorizonFsClient.cs
new file mode 100644
index 000000000..3dbafa88b
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/HorizonFsClient.cs
@@ -0,0 +1,119 @@
+using LibHac.Common;
+using LibHac.Fs.Fsa;
+using LibHac.FsSystem;
+using LibHac.Ncm;
+using LibHac.Tools.FsSystem.NcaUtils;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.Horizon;
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Fs;
+using System;
+using System.Collections.Concurrent;
+using System.IO;
+
+namespace Ryujinx.HLE.HOS
+{
+    class HorizonFsClient : IFsClient
+    {
+        private readonly Horizon _system;
+        private readonly LibHac.Fs.FileSystemClient _fsClient;
+        private readonly ConcurrentDictionary<string, LocalStorage> _mountedStorages;
+
+        public HorizonFsClient(Horizon system)
+        {
+            _system = system;
+            _fsClient = _system.LibHacHorizonManager.FsClient.Fs;
+            _mountedStorages = new();
+        }
+
+        public void CloseFile(FileHandle handle)
+        {
+            _fsClient.CloseFile((LibHac.Fs.FileHandle)handle.Value);
+        }
+
+        public Result GetFileSize(out long size, FileHandle handle)
+        {
+            return _fsClient.GetFileSize(out size, (LibHac.Fs.FileHandle)handle.Value).ToHorizonResult();
+        }
+
+        public Result MountSystemData(string mountName, ulong dataId)
+        {
+            string contentPath = _system.ContentManager.GetInstalledContentPath(dataId, StorageId.BuiltInSystem, NcaContentType.PublicData);
+            string installPath = VirtualFileSystem.SwitchPathToSystemPath(contentPath);
+
+            if (!string.IsNullOrWhiteSpace(installPath))
+            {
+                string ncaPath = installPath;
+
+                if (File.Exists(ncaPath))
+                {
+                    LocalStorage ncaStorage = null;
+
+                    try
+                    {
+                        ncaStorage = new LocalStorage(ncaPath, FileAccess.Read, FileMode.Open);
+
+                        Nca nca = new(_system.KeySet, ncaStorage);
+
+                        using var ncaFileSystem = nca.OpenFileSystem(NcaSectionType.Data, _system.FsIntegrityCheckLevel);
+                        using var ncaFsRef = new UniqueRef<IFileSystem>(ncaFileSystem);
+
+                        Result result = _fsClient.Register(mountName.ToU8Span(), ref ncaFsRef.Ref).ToHorizonResult();
+                        if (result.IsFailure)
+                        {
+                            ncaStorage.Dispose();
+                        }
+                        else
+                        {
+                            _mountedStorages.TryAdd(mountName, ncaStorage);
+                        }
+
+                        return result;
+                    }
+                    catch (HorizonResultException ex)
+                    {
+                        ncaStorage?.Dispose();
+
+                        return ex.ResultValue.ToHorizonResult();
+                    }
+                }
+            }
+
+            // TODO: Return correct result here, this is likely wrong.
+
+            return LibHac.Fs.ResultFs.TargetNotFound.Handle().ToHorizonResult();
+        }
+
+        public Result OpenFile(out FileHandle handle, string path, OpenMode openMode)
+        {
+            var result = _fsClient.OpenFile(out var libhacHandle, path.ToU8Span(), (LibHac.Fs.OpenMode)openMode);
+            handle = new(libhacHandle);
+
+            return result.ToHorizonResult();
+        }
+
+        public Result QueryMountSystemDataCacheSize(out long size, ulong dataId)
+        {
+            // TODO.
+
+            size = 0;
+
+            return Result.Success;
+        }
+
+        public Result ReadFile(FileHandle handle, long offset, Span<byte> destination)
+        {
+            return _fsClient.ReadFile((LibHac.Fs.FileHandle)handle.Value, offset, destination).ToHorizonResult();
+        }
+
+        public void Unmount(string mountName)
+        {
+            if (_mountedStorages.TryRemove(mountName, out LocalStorage ncaStorage))
+            {
+                ncaStorage.Dispose();
+            }
+
+            _fsClient.Unmount(mountName.ToU8Span());
+        }
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/HleProcessDebugger.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/HleProcessDebugger.cs
index cbfef588c..7578f1d2f 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Process/HleProcessDebugger.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Process/HleProcessDebugger.cs
@@ -436,14 +436,14 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
 
             uint nameIndex = sym.NameOffset;
 
-            string name = string.Empty;
+            StringBuilder nameBuilder = new();
 
             for (int chr; (chr = memory.Read<byte>(strTblAddr + nameIndex++)) != 0;)
             {
-                name += (char)chr;
+                nameBuilder.Append((char)chr);
             }
 
-            return new ElfSymbol(name, sym.Info, sym.Other, sym.SectionIndex, sym.ValueAddress, sym.Size);
+            return new ElfSymbol(nameBuilder.ToString(), sym.Info, sym.Other, sym.SectionIndex, sym.ValueAddress, sym.Size);
         }
 
         private static ElfSymbol GetSymbol32(IVirtualMemoryManager memory, ulong address, ulong strTblAddr)
@@ -452,14 +452,14 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
 
             uint nameIndex = sym.NameOffset;
 
-            string name = string.Empty;
+            StringBuilder nameBuilder = new();
 
             for (int chr; (chr = memory.Read<byte>(strTblAddr + nameIndex++)) != 0;)
             {
-                name += (char)chr;
+                nameBuilder.Append((char)chr);
             }
 
-            return new ElfSymbol(name, sym.Info, sym.Other, sym.SectionIndex, sym.ValueAddress, sym.Size);
+            return new ElfSymbol(nameBuilder.ToString(), sym.Info, sym.Other, sym.SectionIndex, sym.ValueAddress, sym.Size);
         }
     }
 }
diff --git a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/ICommonStateGetter.cs b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/ICommonStateGetter.cs
index 0d2ec8bc4..602fc2c4d 100644
--- a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/ICommonStateGetter.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/ICommonStateGetter.cs
@@ -5,6 +5,7 @@ using Ryujinx.HLE.HOS.Services.Settings.Types;
 using Ryujinx.HLE.HOS.Services.Vi.RootService.ApplicationDisplayService;
 using Ryujinx.HLE.HOS.SystemState;
 using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Lbl;
 using System;
 
 namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.SystemAppletProxy
@@ -15,7 +16,6 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
 
         private readonly Apm.ManagerServer _apmManagerServer;
         private readonly Apm.SystemManagerServer _apmSystemManagerServer;
-        private readonly Lbl.LblControllerServer _lblControllerServer;
 
         private bool _vrModeEnabled;
 #pragma warning disable CS0414, IDE0052 // Remove unread private member
@@ -34,7 +34,6 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
 
             _apmManagerServer = new Apm.ManagerServer(context);
             _apmSystemManagerServer = new Apm.SystemManagerServer(context);
-            _lblControllerServer = new Lbl.LblControllerServer(context);
 
             _acquiredSleepLockEvent = new KEvent(context.Device.System.KernelContext);
         }
@@ -215,13 +214,15 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
 
             _vrModeEnabled = vrModeEnabled;
 
+            using var lblApi = new LblApi();
+
             if (vrModeEnabled)
             {
-                _lblControllerServer.EnableVrMode();
+                lblApi.EnableVrMode().AbortOnFailure();
             }
             else
             {
-                _lblControllerServer.DisableVrMode();
+                lblApi.DisableVrMode().AbortOnFailure();
             }
 
             // TODO: It signals an internal event of ICommonStateGetter. We have to determine where this event is used.
diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/IHardwareOpusDecoderManager.cs b/src/Ryujinx.HLE/HOS/Services/Audio/IHardwareOpusDecoderManager.cs
index b69a23921..514b51a51 100644
--- a/src/Ryujinx.HLE/HOS/Services/Audio/IHardwareOpusDecoderManager.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Audio/IHardwareOpusDecoderManager.cs
@@ -154,6 +154,28 @@ namespace Ryujinx.HLE.HOS.Services.Audio
             return ResultCode.Success;
         }
 
+        [CommandCmif(8)] // 16.0.0+
+        // GetWorkBufferSizeExEx(OpusParametersEx) -> u32
+        public ResultCode GetWorkBufferSizeExEx(ServiceCtx context)
+        {
+            // NOTE: GetWorkBufferSizeEx use hardcoded values to compute the returned size.
+            //       GetWorkBufferSizeExEx fixes that by using dynamic values.
+            //       Since we're already doing that, it's fine to call it directly.
+
+            return GetWorkBufferSizeEx(context);
+        }
+
+        [CommandCmif(9)] // 16.0.0+
+        // GetWorkBufferSizeForMultiStreamExEx(buffer<unknown<0x118>, 0x19>) -> u32
+        public ResultCode GetWorkBufferSizeForMultiStreamExEx(ServiceCtx context)
+        {
+            // NOTE: GetWorkBufferSizeForMultiStreamEx use hardcoded values to compute the returned size.
+            //       GetWorkBufferSizeForMultiStreamExEx fixes that by using dynamic values.
+            //       Since we're already doing that, it's fine to call it directly.
+
+            return GetWorkBufferSizeForMultiStreamEx(context);
+        }
+
         private static int GetOpusMultistreamDecoderSize(int streams, int coupledStreams)
         {
             if (streams < 1 || coupledStreams > streams || coupledStreams < 0)
diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IFriendService.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IFriendService.cs
index f084fad11..54d23e88c 100644
--- a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IFriendService.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IFriendService.cs
@@ -36,6 +36,8 @@ namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator
                 throw new InvalidOperationException("Out of handles!");
             }
 
+            _completionEvent.WritableEvent.Signal();
+
             context.Response.HandleDesc = IpcHandleDesc.MakeCopy(completionEventHandle);
 
             return ResultCode.Success;
@@ -187,6 +189,20 @@ namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator
             return ResultCode.Success;
         }
 
+        [CommandCmif(10420)]
+        // nn::friends::CheckBlockedUserListAvailability(nn::account::Uid userId) -> bool
+        public ResultCode CheckBlockedUserListAvailability(ServiceCtx context)
+        {
+            UserId userId = context.RequestData.ReadStruct<UserId>();
+
+            // Yes, it is available.
+            context.ResponseData.Write(true);
+
+            Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { UserId = userId.ToString() });
+
+            return ResultCode.Success;
+        }
+
         [CommandCmif(10600)]
         // nn::friends::DeclareOpenOnlinePlaySession(nn::account::Uid userId)
         public ResultCode DeclareOpenOnlinePlaySession(ServiceCtx context)
diff --git a/src/Ryujinx.HLE/HOS/Services/Ins/IReceiverManager.cs b/src/Ryujinx.HLE/HOS/Services/Ins/IReceiverManager.cs
deleted file mode 100644
index 8ee00d0e9..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Ins/IReceiverManager.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Ins
-{
-    [Service("ins:r")]
-    class IReceiverManager : IpcService
-    {
-        public IReceiverManager(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ins/ISenderManager.cs b/src/Ryujinx.HLE/HOS/Services/Ins/ISenderManager.cs
deleted file mode 100644
index 239c4cc83..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Ins/ISenderManager.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Ins
-{
-    [Service("ins:s")]
-    class ISenderManager : IpcService
-    {
-        public ISenderManager(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/IpcService.cs b/src/Ryujinx.HLE/HOS/Services/IpcService.cs
index eb9f6b2fd..808f21c0e 100644
--- a/src/Ryujinx.HLE/HOS/Services/IpcService.cs
+++ b/src/Ryujinx.HLE/HOS/Services/IpcService.cs
@@ -23,14 +23,14 @@ namespace Ryujinx.HLE.HOS.Services
 
         public IpcService(ServerBase server = null)
         {
-            CmifCommands = Assembly.GetExecutingAssembly().GetTypes()
+            CmifCommands = typeof(IpcService).Assembly.GetTypes()
                 .Where(type => type == GetType())
                 .SelectMany(type => type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public))
                 .SelectMany(methodInfo => methodInfo.GetCustomAttributes(typeof(CommandCmifAttribute))
                 .Select(command => (((CommandCmifAttribute)command).Id, methodInfo)))
                 .ToDictionary(command => command.Id, command => command.methodInfo);
 
-            TipcCommands = Assembly.GetExecutingAssembly().GetTypes()
+            TipcCommands = typeof(IpcService).Assembly.GetTypes()
                 .Where(type => type == GetType())
                 .SelectMany(type => type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public))
                 .SelectMany(methodInfo => methodInfo.GetCustomAttributes(typeof(CommandTipcAttribute))
diff --git a/src/Ryujinx.HLE/HOS/Services/Lbl/ILblController.cs b/src/Ryujinx.HLE/HOS/Services/Lbl/ILblController.cs
deleted file mode 100644
index 75d787432..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Lbl/ILblController.cs
+++ /dev/null
@@ -1,92 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Lbl
-{
-    abstract class ILblController : IpcService
-    {
-        public ILblController(ServiceCtx context) { }
-
-        protected abstract void SetCurrentBrightnessSettingForVrMode(float currentBrightnessSettingForVrMode);
-        protected abstract float GetCurrentBrightnessSettingForVrMode();
-        internal abstract void EnableVrMode();
-        internal abstract void DisableVrMode();
-        protected abstract bool IsVrModeEnabled();
-
-        [CommandCmif(17)]
-        // SetBrightnessReflectionDelayLevel(float, float)
-        public ResultCode SetBrightnessReflectionDelayLevel(ServiceCtx context)
-        {
-            return ResultCode.Success;
-        }
-
-        [CommandCmif(18)]
-        // GetBrightnessReflectionDelayLevel(float) -> float
-        public ResultCode GetBrightnessReflectionDelayLevel(ServiceCtx context)
-        {
-            context.ResponseData.Write(0.0f);
-
-            return ResultCode.Success;
-        }
-
-        [CommandCmif(21)]
-        // SetCurrentAmbientLightSensorMapping(unknown<0xC>)
-        public ResultCode SetCurrentAmbientLightSensorMapping(ServiceCtx context)
-        {
-            return ResultCode.Success;
-        }
-
-        [CommandCmif(22)]
-        // GetCurrentAmbientLightSensorMapping() -> unknown<0xC>
-        public ResultCode GetCurrentAmbientLightSensorMapping(ServiceCtx context)
-        {
-            return ResultCode.Success;
-        }
-
-        [CommandCmif(24)] // 3.0.0+
-        // SetCurrentBrightnessSettingForVrMode(float)
-        public ResultCode SetCurrentBrightnessSettingForVrMode(ServiceCtx context)
-        {
-            float currentBrightnessSettingForVrMode = context.RequestData.ReadSingle();
-
-            SetCurrentBrightnessSettingForVrMode(currentBrightnessSettingForVrMode);
-
-            return ResultCode.Success;
-        }
-
-        [CommandCmif(25)] // 3.0.0+
-        // GetCurrentBrightnessSettingForVrMode() -> float
-        public ResultCode GetCurrentBrightnessSettingForVrMode(ServiceCtx context)
-        {
-            float currentBrightnessSettingForVrMode = GetCurrentBrightnessSettingForVrMode();
-
-            context.ResponseData.Write(currentBrightnessSettingForVrMode);
-
-            return ResultCode.Success;
-        }
-
-        [CommandCmif(26)] // 3.0.0+
-        // EnableVrMode()
-        public ResultCode EnableVrMode(ServiceCtx context)
-        {
-            EnableVrMode();
-
-            return ResultCode.Success;
-        }
-
-        [CommandCmif(27)] // 3.0.0+
-        // DisableVrMode()
-        public ResultCode DisableVrMode(ServiceCtx context)
-        {
-            DisableVrMode();
-
-            return ResultCode.Success;
-        }
-
-        [CommandCmif(28)] // 3.0.0+
-        // IsVrModeEnabled() -> bool
-        public ResultCode IsVrModeEnabled(ServiceCtx context)
-        {
-            context.ResponseData.Write(IsVrModeEnabled());
-
-            return ResultCode.Success;
-        }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Lbl/LblControllerServer.cs b/src/Ryujinx.HLE/HOS/Services/Lbl/LblControllerServer.cs
deleted file mode 100644
index 899e882e5..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Lbl/LblControllerServer.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Lbl
-{
-    [Service("lbl")]
-    class LblControllerServer : ILblController
-    {
-        private bool _vrModeEnabled;
-        private float _currentBrightnessSettingForVrMode;
-
-        public LblControllerServer(ServiceCtx context) : base(context) { }
-
-        protected override void SetCurrentBrightnessSettingForVrMode(float currentBrightnessSettingForVrMode)
-        {
-            if (float.IsNaN(currentBrightnessSettingForVrMode) || float.IsInfinity(currentBrightnessSettingForVrMode))
-            {
-                _currentBrightnessSettingForVrMode = 0.0f;
-
-                return;
-            }
-
-            _currentBrightnessSettingForVrMode = currentBrightnessSettingForVrMode;
-        }
-
-        protected override float GetCurrentBrightnessSettingForVrMode()
-        {
-            if (float.IsNaN(_currentBrightnessSettingForVrMode) || float.IsInfinity(_currentBrightnessSettingForVrMode))
-            {
-                return 0.0f;
-            }
-
-            return _currentBrightnessSettingForVrMode;
-        }
-
-        internal override void EnableVrMode()
-        {
-            _vrModeEnabled = true;
-
-            // NOTE: Service check _vrModeEnabled field value in a thread and then change the screen brightness.
-            //       Since we don't support that. It's fine to do nothing.
-        }
-
-        internal override void DisableVrMode()
-        {
-            _vrModeEnabled = false;
-
-            // NOTE: Service check _vrModeEnabled field value in a thread and then change the screen brightness.
-            //       Since we don't support that. It's fine to do nothing.
-        }
-
-        protected override bool IsVrModeEnabled()
-        {
-            return _vrModeEnabled;
-        }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/IUserServiceCreator.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/IUserServiceCreator.cs
index 317b1dbe8..7bcb7785f 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/IUserServiceCreator.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/IUserServiceCreator.cs
@@ -5,7 +5,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn
     [Service("ldn:u")]
     class IUserServiceCreator : IpcService
     {
-        public IUserServiceCreator(ServiceCtx context) { }
+        public IUserServiceCreator(ServiceCtx context) : base(context.Device.System.LdnServer) { }
 
         [CommandCmif(0)]
         // CreateUserLocalCommunicationService() -> object<nn::ldn::detail::IUserLocalCommunicationService>
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/ResultCode.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/ResultCode.cs
index a9f2cbc33..d550ea57e 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/ResultCode.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/ResultCode.cs
@@ -7,10 +7,18 @@ namespace Ryujinx.HLE.HOS.Services.Ldn
 
         Success = 0,
 
+        DeviceNotAvailable = (16 << ErrorCodeShift) | ModuleId,
         DeviceDisabled = (22 << ErrorCodeShift) | ModuleId,
         InvalidState = (32 << ErrorCodeShift) | ModuleId,
-        Unknown1 = (48 << ErrorCodeShift) | ModuleId,
+        NodeNotFound = (48 << ErrorCodeShift) | ModuleId,
+        ConnectFailure = (64 << ErrorCodeShift) | ModuleId,
+        ConnectNotFound = (65 << ErrorCodeShift) | ModuleId,
+        ConnectTimeout = (66 << ErrorCodeShift) | ModuleId,
+        ConnectRejected = (67 << ErrorCodeShift) | ModuleId,
         InvalidArgument = (96 << ErrorCodeShift) | ModuleId,
         InvalidObject = (97 << ErrorCodeShift) | ModuleId,
+        VersionTooLow = (113 << ErrorCodeShift) | ModuleId,
+        VersionTooHigh = (114 << ErrorCodeShift) | ModuleId,
+        TooManyPlayers = (144 << ErrorCodeShift) | ModuleId,
     }
 }
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/AcceptPolicy.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/AcceptPolicy.cs
new file mode 100644
index 000000000..272a2fd8b
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/AcceptPolicy.cs
@@ -0,0 +1,10 @@
+namespace Ryujinx.HLE.HOS.Services.Ldn.Types
+{
+    enum AcceptPolicy : byte
+    {
+        AcceptAll,
+        RejectAll,
+        BlackList,
+        WhiteList,
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/AddressEntry.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/AddressEntry.cs
new file mode 100644
index 000000000..a458c521b
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/AddressEntry.cs
@@ -0,0 +1,13 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.Types
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0xC)]
+    struct AddressEntry
+    {
+        public uint Ipv4Address;
+        public Array6<byte> MacAddress;
+        public ushort Reserved;
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/AddressList.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/AddressList.cs
new file mode 100644
index 000000000..cf4d0e202
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/AddressList.cs
@@ -0,0 +1,11 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.Types
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x60)]
+    struct AddressList
+    {
+        public Array8<AddressEntry> Addresses;
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/CommonNetworkInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/CommonNetworkInfo.cs
new file mode 100644
index 000000000..cd3a5716c
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/CommonNetworkInfo.cs
@@ -0,0 +1,16 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.Types
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x30)]
+    struct CommonNetworkInfo
+    {
+        public Array6<byte> MacAddress;
+        public Ssid Ssid;
+        public ushort Channel;
+        public byte LinkLevel;
+        public byte NetworkType;
+        public uint Reserved;
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/DisconnectReason.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/DisconnectReason.cs
new file mode 100644
index 000000000..e3fd0ed43
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/DisconnectReason.cs
@@ -0,0 +1,13 @@
+namespace Ryujinx.HLE.HOS.Services.Ldn.Types
+{
+    enum DisconnectReason : uint
+    {
+        None,
+        DisconnectedByUser,
+        DisconnectedBySystem,
+        DestroyedByUser,
+        DestroyedBySystem,
+        Rejected,
+        SignalLost,
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/IntentId.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/IntentId.cs
new file mode 100644
index 000000000..e1ffec5e4
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/IntentId.cs
@@ -0,0 +1,13 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.Types
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x10)]
+    struct IntentId
+    {
+        public long LocalCommunicationId;
+        public ushort Reserved1;
+        public ushort SceneId;
+        public uint Reserved2;
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs
new file mode 100644
index 000000000..4b7241c43
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs
@@ -0,0 +1,23 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.Types
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x430)]
+    struct LdnNetworkInfo
+    {
+        public Array16<byte> SecurityParameter;
+        public ushort SecurityMode;
+        public AcceptPolicy StationAcceptPolicy;
+        public byte Reserved1;
+        public ushort Reserved2;
+        public byte NodeCountMax;
+        public byte NodeCount;
+        public Array8<NodeInfo> Nodes;
+        public ushort Reserved3;
+        public ushort AdvertiseDataSize;
+        public Array384<byte> AdvertiseData;
+        public Array140<byte> Reserved4;
+        public ulong AuthenticationId;
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs
new file mode 100644
index 000000000..aea9a4a7e
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs
@@ -0,0 +1,16 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.Types
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x20)]
+    struct NetworkConfig
+    {
+        public IntentId IntentId;
+        public ushort Channel;
+        public byte NodeCountMax;
+        public byte Reserved1;
+        public ushort LocalCommunicationVersion;
+        public Array10<byte> Reserved2;
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkId.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkId.cs
new file mode 100644
index 000000000..9579647b3
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkId.cs
@@ -0,0 +1,12 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.Types
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x20)]
+    struct NetworkId
+    {
+        public IntentId IntentId;
+        public Array16<byte> SessionId;
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkInfo.cs
new file mode 100644
index 000000000..c1fb87d4a
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkInfo.cs
@@ -0,0 +1,12 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.Types
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x480)]
+    struct NetworkInfo
+    {
+        public NetworkId NetworkId;
+        public CommonNetworkInfo Common;
+        public LdnNetworkInfo Ldn;
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkType.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkType.cs
new file mode 100644
index 000000000..a9ca153fb
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkType.cs
@@ -0,0 +1,10 @@
+namespace Ryujinx.HLE.HOS.Services.Ldn.Types
+{
+    enum NetworkType : uint
+    {
+        None,
+        General,
+        Ldn,
+        All,
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs
new file mode 100644
index 000000000..c57a7dc45
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs
@@ -0,0 +1,18 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.Types
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x40)]
+    struct NodeInfo
+    {
+        public uint Ipv4Address;
+        public Array6<byte> MacAddress;
+        public byte NodeId;
+        public byte IsConnected;
+        public Array33<byte> UserName;
+        public byte Reserved1;
+        public ushort LocalCommunicationVersion;
+        public Array16<byte> Reserved2;
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs
new file mode 100644
index 000000000..f33ceaebe
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs
@@ -0,0 +1,62 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.Types
+{
+    [StructLayout(LayoutKind.Sequential, Size = 8)]
+    struct NodeLatestUpdate
+    {
+        public NodeLatestUpdateFlags State;
+        public Array7<byte> Reserved;
+    }
+
+    static class NodeLatestUpdateHelper
+    {
+        private static readonly object _lock = new();
+
+        public static void CalculateLatestUpdate(this Array8<NodeLatestUpdate> array, Array8<NodeInfo> beforeNodes, Array8<NodeInfo> afterNodes)
+        {
+            lock (_lock)
+            {
+                for (int i = 0; i < 8; i++)
+                {
+                    if (beforeNodes[i].IsConnected == 0)
+                    {
+                        if (afterNodes[i].IsConnected != 0)
+                        {
+                            array[i].State |= NodeLatestUpdateFlags.Connect;
+                        }
+                    }
+                    else
+                    {
+                        if (afterNodes[i].IsConnected == 0)
+                        {
+                            array[i].State |= NodeLatestUpdateFlags.Disconnect;
+                        }
+                    }
+                }
+            }
+        }
+
+        public static NodeLatestUpdate[] ConsumeLatestUpdate(this Array8<NodeLatestUpdate> array, int number)
+        {
+            NodeLatestUpdate[] result = new NodeLatestUpdate[number];
+
+            lock (_lock)
+            {
+                for (int i = 0; i < number; i++)
+                {
+                    result[i].Reserved = new Array7<byte>();
+
+                    if (i < 8)
+                    {
+                        result[i].State = array[i].State;
+                        array[i].State = NodeLatestUpdateFlags.None;
+                    }
+                }
+            }
+
+            return result;
+        }
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdateFlags.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdateFlags.cs
new file mode 100644
index 000000000..3b69b2798
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdateFlags.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.Types
+{
+    [Flags]
+    enum NodeLatestUpdateFlags : byte
+    {
+        None = 0,
+        Connect = 1 << 0,
+        Disconnect = 1 << 1,
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs
new file mode 100644
index 000000000..a5991074a
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs
@@ -0,0 +1,16 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.Types
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x60)]
+    struct ScanFilter
+    {
+        public NetworkId NetworkId;
+        public NetworkType NetworkType;
+        public Array6<byte> MacAddress;
+        public Ssid Ssid;
+        public Array16<byte> Reserved;
+        public ScanFilterFlag Flag;
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilterFlag.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilterFlag.cs
new file mode 100644
index 000000000..f27b52c35
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilterFlag.cs
@@ -0,0 +1,18 @@
+using System;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.Types
+{
+    [Flags]
+    enum ScanFilterFlag : byte
+    {
+        LocalCommunicationId = 1 << 0,
+        SessionId = 1 << 1,
+        NetworkType = 1 << 2,
+        MacAddress = 1 << 3,
+        Ssid = 1 << 4,
+        SceneId = 1 << 5,
+        IntentId = LocalCommunicationId | SceneId,
+        NetworkId = IntentId | SessionId,
+        All = NetworkType | IntentId | SessionId | MacAddress | Ssid,
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs
new file mode 100644
index 000000000..85a19a875
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs
@@ -0,0 +1,13 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.Types
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x44)]
+    struct SecurityConfig
+    {
+        public SecurityMode SecurityMode;
+        public ushort PassphraseSize;
+        public Array64<byte> Passphrase;
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityMode.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityMode.cs
new file mode 100644
index 000000000..a621d20fc
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityMode.cs
@@ -0,0 +1,9 @@
+namespace Ryujinx.HLE.HOS.Services.Ldn.Types
+{
+    enum SecurityMode : ushort
+    {
+        All,
+        Retail,
+        Debug,
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs
new file mode 100644
index 000000000..534dbc7ae
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs
@@ -0,0 +1,12 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.Types
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x20)]
+    struct SecurityParameter
+    {
+        public Array16<byte> Data;
+        public Array16<byte> SessionId;
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs
new file mode 100644
index 000000000..72db4d41a
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs
@@ -0,0 +1,12 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.Types
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x22)]
+    struct Ssid
+    {
+        public byte Length;
+        public Array33<byte> Name;
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs
new file mode 100644
index 000000000..1401f5214
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs
@@ -0,0 +1,12 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.Types
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x30)]
+    struct UserConfig
+    {
+        public Array33<byte> UserName;
+        public Array15<byte> Unknown1;
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs
new file mode 100644
index 000000000..07bbbeda3
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs
@@ -0,0 +1,104 @@
+using Ryujinx.Common.Memory;
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
+using System;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
+{
+    class AccessPoint : IDisposable
+    {
+        private byte[] _advertiseData;
+
+        private readonly IUserLocalCommunicationService _parent;
+
+        public NetworkInfo NetworkInfo;
+        public Array8<NodeLatestUpdate> LatestUpdates = new();
+        public bool Connected { get; private set; }
+
+        public AccessPoint(IUserLocalCommunicationService parent)
+        {
+            _parent = parent;
+
+            _parent.NetworkClient.NetworkChange += NetworkChanged;
+        }
+
+        public void Dispose()
+        {
+            _parent.NetworkClient.DisconnectNetwork();
+
+            _parent.NetworkClient.NetworkChange -= NetworkChanged;
+        }
+
+        private void NetworkChanged(object sender, RyuLdn.NetworkChangeEventArgs e)
+        {
+            LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes);
+
+            NetworkInfo = e.Info;
+
+            if (Connected != e.Connected)
+            {
+                Connected = e.Connected;
+
+                if (Connected)
+                {
+                    _parent.SetState(NetworkState.AccessPointCreated);
+                }
+                else
+                {
+                    _parent.SetDisconnectReason(e.DisconnectReasonOrDefault(DisconnectReason.DestroyedBySystem));
+                }
+            }
+            else
+            {
+                _parent.SetState();
+            }
+        }
+
+        public ResultCode SetAdvertiseData(byte[] advertiseData)
+        {
+            _advertiseData = advertiseData;
+
+            _parent.NetworkClient.SetAdvertiseData(_advertiseData);
+
+            return ResultCode.Success;
+        }
+
+        public ResultCode SetStationAcceptPolicy(AcceptPolicy acceptPolicy)
+        {
+            _parent.NetworkClient.SetStationAcceptPolicy(acceptPolicy);
+
+            return ResultCode.Success;
+        }
+
+        public ResultCode CreateNetwork(SecurityConfig securityConfig, UserConfig userConfig, NetworkConfig networkConfig)
+        {
+            CreateAccessPointRequest request = new()
+            {
+                SecurityConfig = securityConfig,
+                UserConfig = userConfig,
+                NetworkConfig = networkConfig,
+            };
+
+            bool success = _parent.NetworkClient.CreateNetwork(request, _advertiseData ?? Array.Empty<byte>());
+
+            return success ? ResultCode.Success : ResultCode.InvalidState;
+        }
+
+        public ResultCode CreateNetworkPrivate(SecurityConfig securityConfig, SecurityParameter securityParameter, UserConfig userConfig, NetworkConfig networkConfig, AddressList addressList)
+        {
+            CreateAccessPointPrivateRequest request = new()
+            {
+                SecurityConfig = securityConfig,
+                SecurityParameter = securityParameter,
+                UserConfig = userConfig,
+                NetworkConfig = networkConfig,
+                AddressList = addressList,
+            };
+
+            bool success = _parent.NetworkClient.CreateNetworkPrivate(request, _advertiseData);
+
+            return success ? ResultCode.Success : ResultCode.InvalidState;
+        }
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs
index d390a3e68..29cc0e1b9 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs
@@ -1,88 +1,1104 @@
-using Ryujinx.HLE.HOS.Ipc;
+using LibHac.Ns;
+using Ryujinx.Common;
+using Ryujinx.Common.Configuration.Multiplayer;
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.Memory;
+using Ryujinx.Common.Utilities;
+using Ryujinx.Cpu;
+using Ryujinx.HLE.HOS.Ipc;
+using Ryujinx.HLE.HOS.Kernel.Threading;
 using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn;
 using Ryujinx.Horizon.Common;
+using Ryujinx.Memory;
 using System;
+using System.IO;
 using System.Net;
+using System.Net.NetworkInformation;
+using System.Runtime.InteropServices;
 
 namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
 {
-    class IUserLocalCommunicationService : IpcService
+    class IUserLocalCommunicationService : IpcService, IDisposable
     {
-        // TODO(Ac_K): Determine what the hardcoded unknown value is.
-        private const int UnknownValue = 90;
+        public INetworkClient NetworkClient { get; private set; }
 
-        private readonly NetworkInterface _networkInterface;
+        private const int NifmRequestID = 90;
+        private const string DefaultIPAddress = "127.0.0.1";
+        private const string DefaultSubnetMask = "255.255.255.0";
+        private const bool IsDevelopment = false;
 
-        private int _stateChangeEventHandle = 0;
+        private readonly KEvent _stateChangeEvent;
+        private int _stateChangeEventHandle;
+
+        private NetworkState _state;
+        private DisconnectReason _disconnectReason;
+        private ResultCode _nifmResultCode;
+
+        private AccessPoint _accessPoint;
+        private Station _station;
 
         public IUserLocalCommunicationService(ServiceCtx context)
         {
-            _networkInterface = new NetworkInterface(context.Device.System);
+            _stateChangeEvent = new KEvent(context.Device.System.KernelContext);
+            _state = NetworkState.None;
+            _disconnectReason = DisconnectReason.None;
+        }
+
+        private ushort CheckDevelopmentChannel(ushort channel)
+        {
+            return (ushort)(!IsDevelopment ? 0 : channel);
+        }
+
+        private SecurityMode CheckDevelopmentSecurityMode(SecurityMode securityMode)
+        {
+            return !IsDevelopment ? SecurityMode.Retail : securityMode;
+        }
+
+        private bool CheckLocalCommunicationIdPermission(ServiceCtx context, ulong localCommunicationIdChecked)
+        {
+            // TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
+            ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
+
+            foreach (var localCommunicationId in controlProperty.LocalCommunicationId.ItemsRo)
+            {
+                if (localCommunicationId == localCommunicationIdChecked)
+                {
+                    return true;
+                }
+            }
+
+            return false;
         }
 
         [CommandCmif(0)]
         // GetState() -> s32 state
         public ResultCode GetState(ServiceCtx context)
         {
-            if (_networkInterface.NifmState != ResultCode.Success)
+            if (_nifmResultCode != ResultCode.Success)
             {
                 context.ResponseData.Write((int)NetworkState.Error);
 
                 return ResultCode.Success;
             }
 
-            ResultCode result = _networkInterface.GetState(out NetworkState state);
+            // NOTE: Returns ResultCode.InvalidArgument if _state is null, doesn't occur in our case.
+            context.ResponseData.Write((int)_state);
 
-            if (result == ResultCode.Success)
+            return ResultCode.Success;
+        }
+
+        public void SetState()
+        {
+            _stateChangeEvent.WritableEvent.Signal();
+        }
+
+        public void SetState(NetworkState state)
+        {
+            _state = state;
+
+            SetState();
+        }
+
+        [CommandCmif(1)]
+        // GetNetworkInfo() -> buffer<network_info<0x480>, 0x1a>
+        public ResultCode GetNetworkInfo(ServiceCtx context)
+        {
+            ulong bufferPosition = context.Request.RecvListBuff[0].Position;
+
+            MemoryHelper.FillWithZeros(context.Memory, bufferPosition, 0x480);
+
+            if (_nifmResultCode != ResultCode.Success)
             {
-                context.ResponseData.Write((int)state);
+                return _nifmResultCode;
             }
 
-            return result;
+            ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo);
+            if (resultCode != ResultCode.Success)
+            {
+                return resultCode;
+            }
+
+            ulong infoSize = MemoryHelper.Write(context.Memory, bufferPosition, networkInfo);
+
+            context.Response.PtrBuff[0] = context.Response.PtrBuff[0].WithSize(infoSize);
+
+            return ResultCode.Success;
+        }
+
+        private ResultCode GetNetworkInfoImpl(out NetworkInfo networkInfo)
+        {
+            if (_state == NetworkState.StationConnected)
+            {
+                networkInfo = _station.NetworkInfo;
+            }
+            else if (_state == NetworkState.AccessPointCreated)
+            {
+                networkInfo = _accessPoint.NetworkInfo;
+            }
+            else
+            {
+                networkInfo = new NetworkInfo();
+
+                return ResultCode.InvalidState;
+            }
+
+            return ResultCode.Success;
+        }
+
+        private NodeLatestUpdate[] GetNodeLatestUpdateImpl(int count)
+        {
+            if (_state == NetworkState.StationConnected)
+            {
+                return _station.LatestUpdates.ConsumeLatestUpdate(count);
+            }
+            else if (_state == NetworkState.AccessPointCreated)
+            {
+                return _accessPoint.LatestUpdates.ConsumeLatestUpdate(count);
+            }
+            else
+            {
+                return Array.Empty<NodeLatestUpdate>();
+            }
+        }
+
+        [CommandCmif(2)]
+        // GetIpv4Address() -> (u32 ip_address, u32 subnet_mask)
+        public ResultCode GetIpv4Address(ServiceCtx context)
+        {
+            if (_nifmResultCode != ResultCode.Success)
+            {
+                return _nifmResultCode;
+            }
+
+            // NOTE: Return ResultCode.InvalidArgument if ip_address and subnet_mask are null, doesn't occur in our case.
+
+            if (_state == NetworkState.AccessPointCreated || _state == NetworkState.StationConnected)
+            {
+                (_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(context.Device.Configuration.MultiplayerLanInterfaceId);
+
+                if (unicastAddress == null)
+                {
+                    context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultIPAddress));
+                    context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultSubnetMask));
+                }
+                else
+                {
+                    Logger.Info?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\".");
+
+                    context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.Address));
+                    context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.IPv4Mask));
+                }
+            }
+            else
+            {
+                return ResultCode.InvalidArgument;
+            }
+
+            return ResultCode.Success;
+        }
+
+        [CommandCmif(3)]
+        // GetDisconnectReason() -> u16 disconnect_reason
+        public ResultCode GetDisconnectReason(ServiceCtx context)
+        {
+            // NOTE: Returns ResultCode.InvalidArgument if _disconnectReason is null, doesn't occur in our case.
+
+            context.ResponseData.Write((short)_disconnectReason);
+
+            return ResultCode.Success;
+        }
+
+        public void SetDisconnectReason(DisconnectReason reason)
+        {
+            if (_state != NetworkState.Initialized)
+            {
+                _disconnectReason = reason;
+
+                SetState(NetworkState.Initialized);
+            }
+        }
+
+        [CommandCmif(4)]
+        // GetSecurityParameter() -> bytes<0x20, 1> security_parameter
+        public ResultCode GetSecurityParameter(ServiceCtx context)
+        {
+            if (_nifmResultCode != ResultCode.Success)
+            {
+                return _nifmResultCode;
+            }
+
+            ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo);
+            if (resultCode != ResultCode.Success)
+            {
+                return resultCode;
+            }
+
+            SecurityParameter securityParameter = new()
+            {
+                Data = new Array16<byte>(),
+                SessionId = networkInfo.NetworkId.SessionId,
+            };
+
+            context.ResponseData.WriteStruct(securityParameter);
+
+            return ResultCode.Success;
+        }
+
+        [CommandCmif(5)]
+        // GetNetworkConfig() -> bytes<0x20, 8> network_config
+        public ResultCode GetNetworkConfig(ServiceCtx context)
+        {
+            if (_nifmResultCode != ResultCode.Success)
+            {
+                return _nifmResultCode;
+            }
+
+            ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo);
+            if (resultCode != ResultCode.Success)
+            {
+                return resultCode;
+            }
+
+            NetworkConfig networkConfig = new()
+            {
+                IntentId = networkInfo.NetworkId.IntentId,
+                Channel = networkInfo.Common.Channel,
+                NodeCountMax = networkInfo.Ldn.NodeCountMax,
+                LocalCommunicationVersion = networkInfo.Ldn.Nodes[0].LocalCommunicationVersion,
+                Reserved2 = new Array10<byte>(),
+            };
+
+            context.ResponseData.WriteStruct(networkConfig);
+
+            return ResultCode.Success;
         }
 
         [CommandCmif(100)]
         // AttachStateChangeEvent() -> handle<copy>
         public ResultCode AttachStateChangeEvent(ServiceCtx context)
         {
-            if (_stateChangeEventHandle == 0)
+            if (_stateChangeEventHandle == 0 && context.Process.HandleTable.GenerateHandle(_stateChangeEvent.ReadableEvent, out _stateChangeEventHandle) != Result.Success)
             {
-                if (context.Process.HandleTable.GenerateHandle(_networkInterface.StateChangeEvent.ReadableEvent, out _stateChangeEventHandle) != Result.Success)
-                {
-                    throw new InvalidOperationException("Out of handles!");
-                }
+                throw new InvalidOperationException("Out of handles!");
             }
 
             context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_stateChangeEventHandle);
 
-            // Return ResultCode.InvalidArgument if handle is null, doesn't occur in our case since we already throw an Exception.
+            // Returns ResultCode.InvalidArgument if handle is null, doesn't occur in our case since we already throw an Exception.
 
             return ResultCode.Success;
         }
 
+        [CommandCmif(101)]
+        // GetNetworkInfoLatestUpdate() -> (buffer<network_info<0x480>, 0x1a>, buffer<node_latest_update, 0xa>)
+        public ResultCode GetNetworkInfoLatestUpdate(ServiceCtx context)
+        {
+            ulong bufferPosition = context.Request.RecvListBuff[0].Position;
+
+            MemoryHelper.FillWithZeros(context.Memory, bufferPosition, 0x480);
+
+            if (_nifmResultCode != ResultCode.Success)
+            {
+                return _nifmResultCode;
+            }
+
+            ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo);
+            if (resultCode != ResultCode.Success)
+            {
+                return resultCode;
+            }
+
+            ulong outputPosition = context.Request.RecvListBuff[0].Position;
+            ulong outputSize = context.Request.RecvListBuff[0].Size;
+
+            ulong latestUpdateSize = (ulong)Marshal.SizeOf<NodeLatestUpdate>();
+            int count = (int)(outputSize / latestUpdateSize);
+
+            NodeLatestUpdate[] latestUpdate = GetNodeLatestUpdateImpl(count);
+
+            MemoryHelper.FillWithZeros(context.Memory, outputPosition, (int)outputSize);
+
+            foreach (NodeLatestUpdate node in latestUpdate)
+            {
+                MemoryHelper.Write(context.Memory, outputPosition, node);
+
+                outputPosition += latestUpdateSize;
+            }
+
+            ulong infoSize = MemoryHelper.Write(context.Memory, bufferPosition, networkInfo);
+
+            context.Response.PtrBuff[0] = context.Response.PtrBuff[0].WithSize(infoSize);
+
+            return ResultCode.Success;
+        }
+
+        [CommandCmif(102)]
+        // Scan(u16 channel, bytes<0x60, 8> scan_filter) -> (u16 count, buffer<network_info, 0x22>)
+        public ResultCode Scan(ServiceCtx context)
+        {
+            return ScanImpl(context);
+        }
+
+        [CommandCmif(103)]
+        // ScanPrivate(u16 channel, bytes<0x60, 8> scan_filter) -> (u16 count, buffer<network_info, 0x22>)
+        public ResultCode ScanPrivate(ServiceCtx context)
+        {
+            return ScanImpl(context, true);
+        }
+
+        private ResultCode ScanImpl(ServiceCtx context, bool isPrivate = false)
+        {
+            ushort channel = (ushort)context.RequestData.ReadUInt64();
+            ScanFilter scanFilter = context.RequestData.ReadStruct<ScanFilter>();
+
+            (ulong bufferPosition, ulong bufferSize) = context.Request.GetBufferType0x22(0);
+
+            if (_nifmResultCode != ResultCode.Success)
+            {
+                return _nifmResultCode;
+            }
+
+            if (!isPrivate)
+            {
+                channel = CheckDevelopmentChannel(channel);
+            }
+
+            ResultCode resultCode = ResultCode.InvalidArgument;
+
+            if (bufferSize != 0)
+            {
+                if (bufferPosition != 0)
+                {
+                    ScanFilterFlag scanFilterFlag = scanFilter.Flag;
+
+                    if (!scanFilterFlag.HasFlag(ScanFilterFlag.NetworkType) || scanFilter.NetworkType <= NetworkType.All)
+                    {
+                        if (scanFilterFlag.HasFlag(ScanFilterFlag.Ssid))
+                        {
+                            if (scanFilter.Ssid.Length <= 31)
+                            {
+                                return resultCode;
+                            }
+                        }
+
+                        if (!scanFilterFlag.HasFlag(ScanFilterFlag.MacAddress))
+                        {
+                            if (scanFilterFlag > ScanFilterFlag.All)
+                            {
+                                return resultCode;
+                            }
+
+                            if (_state - 3 >= NetworkState.AccessPoint)
+                            {
+                                resultCode = ResultCode.InvalidState;
+                            }
+                            else
+                            {
+                                if (scanFilter.NetworkId.IntentId.LocalCommunicationId == -1)
+                                {
+                                    // TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
+                                    ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
+
+                                    scanFilter.NetworkId.IntentId.LocalCommunicationId = (long)controlProperty.LocalCommunicationId[0];
+                                }
+
+                                resultCode = ScanInternal(context.Memory, channel, scanFilter, bufferPosition, bufferSize, out ulong counter);
+
+                                context.ResponseData.Write(counter);
+                            }
+                        }
+                        else
+                        {
+                            throw new NotSupportedException();
+                        }
+                    }
+                }
+            }
+
+            return resultCode;
+        }
+
+        private ResultCode ScanInternal(IVirtualMemoryManager memory, ushort channel, ScanFilter scanFilter, ulong bufferPosition, ulong bufferSize, out ulong counter)
+        {
+            ulong networkInfoSize = (ulong)Marshal.SizeOf(typeof(NetworkInfo));
+            ulong maxGames = bufferSize / networkInfoSize;
+
+            MemoryHelper.FillWithZeros(memory, bufferPosition, (int)bufferSize);
+
+            NetworkInfo[] availableGames = NetworkClient.Scan(channel, scanFilter);
+
+            counter = 0;
+
+            foreach (NetworkInfo networkInfo in availableGames)
+            {
+                MemoryHelper.Write(memory, bufferPosition + (networkInfoSize * counter), networkInfo);
+
+                if (++counter >= maxGames)
+                {
+                    break;
+                }
+            }
+
+            return ResultCode.Success;
+        }
+
+        [CommandCmif(104)] // 5.0.0+
+        // SetWirelessControllerRestriction(u32 wireless_controller_restriction)
+        public ResultCode SetWirelessControllerRestriction(ServiceCtx context)
+        {
+            // NOTE: Return ResultCode.InvalidArgument if an internal IPAddress is null, doesn't occur in our case.
+
+            uint wirelessControllerRestriction = context.RequestData.ReadUInt32();
+
+            if (wirelessControllerRestriction > 1)
+            {
+                return ResultCode.InvalidArgument;
+            }
+
+            if (_state != NetworkState.Initialized)
+            {
+                return ResultCode.InvalidState;
+            }
+
+            // NOTE: WirelessControllerRestriction value is used for the btm service in SetWlanMode call.
+            //       Since we use our own implementation we can do nothing here.
+
+            return ResultCode.Success;
+        }
+
+        [CommandCmif(200)]
+        // OpenAccessPoint()
+        public ResultCode OpenAccessPoint(ServiceCtx context)
+        {
+            if (_nifmResultCode != ResultCode.Success)
+            {
+                return _nifmResultCode;
+            }
+
+            if (_state != NetworkState.Initialized)
+            {
+                return ResultCode.InvalidState;
+            }
+
+            CloseStation();
+
+            SetState(NetworkState.AccessPoint);
+
+            _accessPoint = new AccessPoint(this);
+
+            // NOTE: Calls nifm service and return related result codes.
+            //       Since we use our own implementation we can return ResultCode.Success.
+
+            return ResultCode.Success;
+        }
+
+        [CommandCmif(201)]
+        // CloseAccessPoint()
+        public ResultCode CloseAccessPoint(ServiceCtx context)
+        {
+            if (_nifmResultCode != ResultCode.Success)
+            {
+                return _nifmResultCode;
+            }
+
+            if (_state == NetworkState.AccessPoint || _state == NetworkState.AccessPointCreated)
+            {
+                DestroyNetworkImpl(DisconnectReason.DestroyedByUser);
+            }
+            else
+            {
+                return ResultCode.InvalidState;
+            }
+
+            SetState(NetworkState.Initialized);
+
+            return ResultCode.Success;
+        }
+
+        private void CloseAccessPoint()
+        {
+            _accessPoint?.Dispose();
+            _accessPoint = null;
+        }
+
+        [CommandCmif(202)]
+        // CreateNetwork(bytes<0x44, 2> security_config, bytes<0x30, 1> user_config, bytes<0x20, 8> network_config)
+        public ResultCode CreateNetwork(ServiceCtx context)
+        {
+            return CreateNetworkImpl(context);
+        }
+
+        [CommandCmif(203)]
+        // CreateNetworkPrivate(bytes<0x44, 2> security_config, bytes<0x20, 1> security_parameter, bytes<0x30, 1>, bytes<0x20, 8> network_config, buffer<unknown, 9> address_entry, int count)
+        public ResultCode CreateNetworkPrivate(ServiceCtx context)
+        {
+            return CreateNetworkImpl(context, true);
+        }
+
+        public ResultCode CreateNetworkImpl(ServiceCtx context, bool isPrivate = false)
+        {
+            SecurityConfig securityConfig = context.RequestData.ReadStruct<SecurityConfig>();
+            SecurityParameter securityParameter = isPrivate ? context.RequestData.ReadStruct<SecurityParameter>() : new SecurityParameter();
+
+            UserConfig userConfig = context.RequestData.ReadStruct<UserConfig>();
+
+            context.RequestData.BaseStream.Seek(4, SeekOrigin.Current); // Alignment?
+            NetworkConfig networkConfig = context.RequestData.ReadStruct<NetworkConfig>();
+
+            if (networkConfig.IntentId.LocalCommunicationId == -1)
+            {
+                // TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
+                ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
+
+                networkConfig.IntentId.LocalCommunicationId = (long)controlProperty.LocalCommunicationId[0];
+            }
+
+            bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkConfig.IntentId.LocalCommunicationId);
+            if (!isLocalCommunicationIdValid)
+            {
+                return ResultCode.InvalidObject;
+            }
+
+            if (_nifmResultCode != ResultCode.Success)
+            {
+                return _nifmResultCode;
+            }
+
+            networkConfig.Channel = CheckDevelopmentChannel(networkConfig.Channel);
+            securityConfig.SecurityMode = CheckDevelopmentSecurityMode(securityConfig.SecurityMode);
+
+            if (networkConfig.NodeCountMax <= 8)
+            {
+                if ((((ulong)networkConfig.LocalCommunicationVersion) & 0x80000000) == 0)
+                {
+                    if (securityConfig.SecurityMode <= SecurityMode.Retail)
+                    {
+                        if (securityConfig.Passphrase.Length <= 0x40)
+                        {
+                            if (_state == NetworkState.AccessPoint)
+                            {
+                                if (isPrivate)
+                                {
+                                    ulong bufferPosition = context.Request.PtrBuff[0].Position;
+                                    ulong bufferSize = context.Request.PtrBuff[0].Size;
+
+                                    byte[] addressListBytes = new byte[bufferSize];
+
+                                    context.Memory.Read(bufferPosition, addressListBytes);
+
+                                    AddressList addressList = MemoryMarshal.Cast<byte, AddressList>(addressListBytes)[0];
+
+                                    _accessPoint.CreateNetworkPrivate(securityConfig, securityParameter, userConfig, networkConfig, addressList);
+                                }
+                                else
+                                {
+                                    _accessPoint.CreateNetwork(securityConfig, userConfig, networkConfig);
+                                }
+
+                                return ResultCode.Success;
+                            }
+                            else
+                            {
+                                return ResultCode.InvalidState;
+                            }
+                        }
+                    }
+                }
+            }
+
+            return ResultCode.InvalidArgument;
+        }
+
+        [CommandCmif(204)]
+        // DestroyNetwork()
+        public ResultCode DestroyNetwork(ServiceCtx context)
+        {
+            return DestroyNetworkImpl(DisconnectReason.DestroyedByUser);
+        }
+
+        private ResultCode DestroyNetworkImpl(DisconnectReason disconnectReason)
+        {
+            if (_nifmResultCode != ResultCode.Success)
+            {
+                return _nifmResultCode;
+            }
+
+            if (disconnectReason - 3 <= DisconnectReason.DisconnectedByUser)
+            {
+                if (_state == NetworkState.AccessPointCreated)
+                {
+                    CloseAccessPoint();
+
+                    SetState(NetworkState.AccessPoint);
+
+                    return ResultCode.Success;
+                }
+
+                CloseAccessPoint();
+
+                return ResultCode.InvalidState;
+            }
+
+            return ResultCode.InvalidArgument;
+        }
+
+        [CommandCmif(205)]
+        // Reject(u32 node_id)
+        public ResultCode Reject(ServiceCtx context)
+        {
+            uint nodeId = context.RequestData.ReadUInt32();
+
+            return RejectImpl(DisconnectReason.Rejected, nodeId);
+        }
+
+        private ResultCode RejectImpl(DisconnectReason disconnectReason, uint nodeId)
+        {
+            if (_nifmResultCode != ResultCode.Success)
+            {
+                return _nifmResultCode;
+            }
+
+            if (_state != NetworkState.AccessPointCreated)
+            {
+                return ResultCode.InvalidState; // Must be network host to reject nodes.
+            }
+
+            return NetworkClient.Reject(disconnectReason, nodeId);
+        }
+
+        [CommandCmif(206)]
+        // SetAdvertiseData(buffer<advertise_data, 0x21>)
+        public ResultCode SetAdvertiseData(ServiceCtx context)
+        {
+            (ulong bufferPosition, ulong bufferSize) = context.Request.GetBufferType0x21(0);
+
+            if (_nifmResultCode != ResultCode.Success)
+            {
+                return _nifmResultCode;
+            }
+
+            if (bufferSize == 0 || bufferSize > 0x180)
+            {
+                return ResultCode.InvalidArgument;
+            }
+
+            if (_state == NetworkState.AccessPoint || _state == NetworkState.AccessPointCreated)
+            {
+                byte[] advertiseData = new byte[bufferSize];
+
+                context.Memory.Read(bufferPosition, advertiseData);
+
+                return _accessPoint.SetAdvertiseData(advertiseData);
+            }
+            else
+            {
+                return ResultCode.InvalidState;
+            }
+        }
+
+        [CommandCmif(207)]
+        // SetStationAcceptPolicy(u8 accept_policy)
+        public ResultCode SetStationAcceptPolicy(ServiceCtx context)
+        {
+            AcceptPolicy acceptPolicy = (AcceptPolicy)context.RequestData.ReadByte();
+
+            if (_nifmResultCode != ResultCode.Success)
+            {
+                return _nifmResultCode;
+            }
+
+            if (acceptPolicy > AcceptPolicy.WhiteList)
+            {
+                return ResultCode.InvalidArgument;
+            }
+
+            if (_state == NetworkState.AccessPoint || _state == NetworkState.AccessPointCreated)
+            {
+                return _accessPoint.SetStationAcceptPolicy(acceptPolicy);
+            }
+            else
+            {
+                return ResultCode.InvalidState;
+            }
+        }
+
+        [CommandCmif(208)]
+        // AddAcceptFilterEntry(bytes<6, 1> mac_address)
+        public ResultCode AddAcceptFilterEntry(ServiceCtx context)
+        {
+            if (_nifmResultCode != ResultCode.Success)
+            {
+                return _nifmResultCode;
+            }
+
+            // TODO
+
+            return ResultCode.Success;
+        }
+
+        [CommandCmif(209)]
+        // ClearAcceptFilter()
+        public ResultCode ClearAcceptFilter(ServiceCtx context)
+        {
+            if (_nifmResultCode != ResultCode.Success)
+            {
+                return _nifmResultCode;
+            }
+
+            // TODO
+
+            return ResultCode.Success;
+        }
+
+        [CommandCmif(300)]
+        // OpenStation()
+        public ResultCode OpenStation(ServiceCtx context)
+        {
+            if (_nifmResultCode != ResultCode.Success)
+            {
+                return _nifmResultCode;
+            }
+
+            if (_state != NetworkState.Initialized)
+            {
+                return ResultCode.InvalidState;
+            }
+
+            CloseAccessPoint();
+
+            SetState(NetworkState.Station);
+
+            _station?.Dispose();
+            _station = new Station(this);
+
+            // NOTE: Calls nifm service and returns related result codes.
+            //       Since we use our own implementation we can return ResultCode.Success.
+
+            return ResultCode.Success;
+        }
+
+        [CommandCmif(301)]
+        // CloseStation()
+        public ResultCode CloseStation(ServiceCtx context)
+        {
+            if (_nifmResultCode != ResultCode.Success)
+            {
+                return _nifmResultCode;
+            }
+
+            if (_state == NetworkState.Station || _state == NetworkState.StationConnected)
+            {
+                DisconnectImpl(DisconnectReason.DisconnectedByUser);
+            }
+            else
+            {
+                return ResultCode.InvalidState;
+            }
+
+            SetState(NetworkState.Initialized);
+
+            return ResultCode.Success;
+        }
+
+        private void CloseStation()
+        {
+            _station?.Dispose();
+            _station = null;
+        }
+
+        [CommandCmif(302)]
+        // Connect(bytes<0x44, 2> security_config, bytes<0x30, 1> user_config, u32 local_communication_version, u32 option_unknown, buffer<network_info<0x480>, 0x19>)
+        public ResultCode Connect(ServiceCtx context)
+        {
+            return ConnectImpl(context);
+        }
+
+        [CommandCmif(303)]
+        // ConnectPrivate(bytes<0x44, 2> security_config, bytes<0x20, 1> security_parameter, bytes<0x30, 1> user_config, u32 local_communication_version, u32 option_unknown, bytes<0x20, 8> network_config)
+        public ResultCode ConnectPrivate(ServiceCtx context)
+        {
+            return ConnectImpl(context, true);
+        }
+
+        private ResultCode ConnectImpl(ServiceCtx context, bool isPrivate = false)
+        {
+            SecurityConfig securityConfig = context.RequestData.ReadStruct<SecurityConfig>();
+            SecurityParameter securityParameter = isPrivate ? context.RequestData.ReadStruct<SecurityParameter>() : new SecurityParameter();
+
+            UserConfig userConfig = context.RequestData.ReadStruct<UserConfig>();
+            uint localCommunicationVersion = context.RequestData.ReadUInt32();
+            uint optionUnknown = context.RequestData.ReadUInt32();
+
+            NetworkConfig networkConfig = new();
+            NetworkInfo networkInfo = new();
+
+            if (isPrivate)
+            {
+                context.RequestData.ReadUInt32(); // Padding.
+
+                networkConfig = context.RequestData.ReadStruct<NetworkConfig>();
+            }
+            else
+            {
+                ulong bufferPosition = context.Request.PtrBuff[0].Position;
+                ulong bufferSize = context.Request.PtrBuff[0].Size;
+
+                byte[] networkInfoBytes = new byte[bufferSize];
+
+                context.Memory.Read(bufferPosition, networkInfoBytes);
+
+                networkInfo = MemoryMarshal.Cast<byte, NetworkInfo>(networkInfoBytes)[0];
+            }
+
+            if (networkInfo.NetworkId.IntentId.LocalCommunicationId == -1)
+            {
+                // TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
+                ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
+
+                networkInfo.NetworkId.IntentId.LocalCommunicationId = (long)controlProperty.LocalCommunicationId[0];
+            }
+
+            bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkInfo.NetworkId.IntentId.LocalCommunicationId);
+            if (!isLocalCommunicationIdValid)
+            {
+                return ResultCode.InvalidObject;
+            }
+
+            if (_nifmResultCode != ResultCode.Success)
+            {
+                return _nifmResultCode;
+            }
+
+            securityConfig.SecurityMode = CheckDevelopmentSecurityMode(securityConfig.SecurityMode);
+
+            ResultCode resultCode = ResultCode.InvalidArgument;
+
+            if (securityConfig.SecurityMode - 1 <= SecurityMode.Debug)
+            {
+                if (optionUnknown <= 1 && (localCommunicationVersion >> 15) == 0 && securityConfig.PassphraseSize <= 64)
+                {
+                    resultCode = ResultCode.VersionTooLow;
+                    if (localCommunicationVersion >= 0)
+                    {
+                        resultCode = ResultCode.VersionTooHigh;
+                        if (localCommunicationVersion <= short.MaxValue)
+                        {
+                            if (_state != NetworkState.Station)
+                            {
+                                resultCode = ResultCode.InvalidState;
+                            }
+                            else
+                            {
+                                if (isPrivate)
+                                {
+                                    resultCode = _station.ConnectPrivate(securityConfig, securityParameter, userConfig, localCommunicationVersion, optionUnknown, networkConfig);
+                                }
+                                else
+                                {
+                                    resultCode = _station.Connect(securityConfig, userConfig, localCommunicationVersion, optionUnknown, networkInfo);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            return resultCode;
+        }
+
+        [CommandCmif(304)]
+        // Disconnect()
+        public ResultCode Disconnect(ServiceCtx context)
+        {
+            return DisconnectImpl(DisconnectReason.DisconnectedByUser);
+        }
+
+        private ResultCode DisconnectImpl(DisconnectReason disconnectReason)
+        {
+            if (_nifmResultCode != ResultCode.Success)
+            {
+                return _nifmResultCode;
+            }
+
+            if (disconnectReason <= DisconnectReason.DisconnectedBySystem)
+            {
+                if (_state == NetworkState.StationConnected)
+                {
+                    SetState(NetworkState.Station);
+
+                    CloseStation();
+
+                    _disconnectReason = disconnectReason;
+
+                    return ResultCode.Success;
+                }
+
+                CloseStation();
+
+                return ResultCode.InvalidState;
+            }
+
+            return ResultCode.InvalidArgument;
+        }
+
         [CommandCmif(400)]
-        // InitializeOld(u64, pid)
+        // InitializeOld(pid)
         public ResultCode InitializeOld(ServiceCtx context)
         {
-            return _networkInterface.Initialize(UnknownValue, 0, null, null);
+            return InitializeImpl(context, context.Process.Pid, NifmRequestID);
         }
 
         [CommandCmif(401)]
         // Finalize()
         public ResultCode Finalize(ServiceCtx context)
         {
-            return _networkInterface.Finalize();
+            if (_nifmResultCode != ResultCode.Success)
+            {
+                return _nifmResultCode;
+            }
+
+            // NOTE: Use true when its called in nn::ldn::detail::ISystemLocalCommunicationService
+            ResultCode resultCode = FinalizeImpl(false);
+            if (resultCode == ResultCode.Success)
+            {
+                SetDisconnectReason(DisconnectReason.None);
+            }
+
+            if (_stateChangeEventHandle != 0)
+            {
+                context.Process.HandleTable.CloseHandle(_stateChangeEventHandle);
+                _stateChangeEventHandle = 0;
+            }
+
+            return resultCode;
+        }
+
+        private ResultCode FinalizeImpl(bool isCausedBySystem)
+        {
+            DisconnectReason disconnectReason;
+
+            switch (_state)
+            {
+                case NetworkState.None:
+                    return ResultCode.Success;
+                case NetworkState.AccessPoint:
+                    {
+                        CloseAccessPoint();
+
+                        break;
+                    }
+                case NetworkState.AccessPointCreated:
+                    {
+                        if (isCausedBySystem)
+                        {
+                            disconnectReason = DisconnectReason.DestroyedBySystem;
+                        }
+                        else
+                        {
+                            disconnectReason = DisconnectReason.DestroyedByUser;
+                        }
+
+                        DestroyNetworkImpl(disconnectReason);
+
+                        break;
+                    }
+                case NetworkState.Station:
+                    {
+                        CloseStation();
+
+                        break;
+                    }
+                case NetworkState.StationConnected:
+                    {
+                        if (isCausedBySystem)
+                        {
+                            disconnectReason = DisconnectReason.DisconnectedBySystem;
+                        }
+                        else
+                        {
+                            disconnectReason = DisconnectReason.DisconnectedByUser;
+                        }
+
+                        DisconnectImpl(disconnectReason);
+
+                        break;
+                    }
+            }
+
+            SetState(NetworkState.None);
+
+            NetworkClient?.Dispose();
+            NetworkClient = null;
+
+            return ResultCode.Success;
         }
 
         [CommandCmif(402)] // 7.0.0+
-        // Initialize(u64 ip_addresses, u64, pid)
+        // Initialize(u64 ip_addresses, pid)
         public ResultCode Initialize(ServiceCtx context)
         {
-            // TODO(Ac_K): Determine what addresses are.
-            IPAddress unknownAddress1 = new(context.RequestData.ReadUInt32());
-            IPAddress unknownAddress2 = new(context.RequestData.ReadUInt32());
+            _ = new IPAddress(context.RequestData.ReadUInt32());
+            _ = new IPAddress(context.RequestData.ReadUInt32());
 
-            return _networkInterface.Initialize(UnknownValue, version: 1, unknownAddress1, unknownAddress2);
+            // NOTE: It seems the guest can get ip_address and subnet_mask from nifm service and pass it through the initialize.
+            //       This calls InitializeImpl() twice: The first time with NIFM_REQUEST_ID, and if it fails, a second time with nifm_request_id = 1.
+
+            return InitializeImpl(context, context.Process.Pid, NifmRequestID);
+        }
+
+        public ResultCode InitializeImpl(ServiceCtx context, ulong pid, int nifmRequestId)
+        {
+            ResultCode resultCode = ResultCode.InvalidArgument;
+
+            if (nifmRequestId <= 255)
+            {
+                if (_state != NetworkState.Initialized)
+                {
+                    // NOTE: Service calls nn::ldn::detail::NetworkInterfaceManager::NetworkInterfaceMonitor::Initialize() with nifmRequestId as argument,
+                    //       then it stores the result code of it in a global variable. Since we use our own implementation, we can just check the connection
+                    //       and return related error codes.
+                    if (System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable())
+                    {
+                        MultiplayerMode mode = context.Device.Configuration.MultiplayerMode;
+                        switch (mode)
+                        {
+                            case MultiplayerMode.Disabled:
+                                NetworkClient = new DisabledLdnClient();
+                                break;
+                        }
+
+                        // TODO: Call nn::arp::GetApplicationLaunchProperty here when implemented.
+                        NetworkClient.SetGameVersion(context.Device.Processes.ActiveApplication.ApplicationControlProperties.DisplayVersion.Items.ToArray());
+
+                        resultCode = ResultCode.Success;
+
+                        _nifmResultCode = resultCode;
+
+                        SetState(NetworkState.Initialized);
+                    }
+                    else
+                    {
+                        // NOTE: Service returns different ResultCode here related to the nifm ResultCode.
+                        resultCode = ResultCode.DeviceDisabled;
+                        _nifmResultCode = resultCode;
+                    }
+                }
+            }
+
+            return resultCode;
+        }
+
+        public void Dispose()
+        {
+            _station?.Dispose();
+            _station = null;
+
+            _accessPoint?.Dispose();
+            _accessPoint = null;
+
+            NetworkClient?.Dispose();
+            NetworkClient = null;
         }
     }
 }
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs
new file mode 100644
index 000000000..9ff46cccb
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs
@@ -0,0 +1,15 @@
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x4FC)]
+    struct ConnectRequest
+    {
+        public SecurityConfig SecurityConfig;
+        public UserConfig UserConfig;
+        public uint LocalCommunicationVersion;
+        public uint OptionUnknown;
+        public NetworkInfo NetworkInfo;
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs
new file mode 100644
index 000000000..4efe9165a
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs
@@ -0,0 +1,16 @@
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types
+{
+    /// <remarks>
+    /// Advertise data is appended separately (remaining data in the buffer).
+    /// </remarks>
+    [StructLayout(LayoutKind.Sequential, Size = 0x94, CharSet = CharSet.Ansi)]
+    struct CreateAccessPointRequest
+    {
+        public SecurityConfig SecurityConfig;
+        public UserConfig UserConfig;
+        public NetworkConfig NetworkConfig;
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs
new file mode 100644
index 000000000..75a1e35ff
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs
@@ -0,0 +1,62 @@
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
+using System;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn
+{
+    class DisabledLdnClient : INetworkClient
+    {
+        public event EventHandler<NetworkChangeEventArgs> NetworkChange;
+
+        public NetworkError Connect(ConnectRequest request)
+        {
+            NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
+
+            return NetworkError.None;
+        }
+
+        public NetworkError ConnectPrivate(ConnectPrivateRequest request)
+        {
+            NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
+
+            return NetworkError.None;
+        }
+
+        public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData)
+        {
+            NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
+
+            return true;
+        }
+
+        public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData)
+        {
+            NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
+
+            return true;
+        }
+
+        public void DisconnectAndStop() { }
+
+        public void DisconnectNetwork() { }
+
+        public ResultCode Reject(DisconnectReason disconnectReason, uint nodeId)
+        {
+            return ResultCode.Success;
+        }
+
+        public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter)
+        {
+            return Array.Empty<NetworkInfo>();
+        }
+
+        public void SetAdvertiseData(byte[] data) { }
+
+        public void SetGameVersion(byte[] versionString) { }
+
+        public void SetStationAcceptPolicy(AcceptPolicy acceptPolicy) { }
+
+        public void Dispose() { }
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs
new file mode 100644
index 000000000..ff342d27c
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs
@@ -0,0 +1,24 @@
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
+using System;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn
+{
+    interface INetworkClient : IDisposable
+    {
+        event EventHandler<NetworkChangeEventArgs> NetworkChange;
+
+        void DisconnectNetwork();
+        void DisconnectAndStop();
+        NetworkError Connect(ConnectRequest request);
+        NetworkError ConnectPrivate(ConnectPrivateRequest request);
+        ResultCode Reject(DisconnectReason disconnectReason, uint nodeId);
+        NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter);
+        void SetGameVersion(byte[] versionString);
+        void SetStationAcceptPolicy(AcceptPolicy acceptPolicy);
+        void SetAdvertiseData(byte[] data);
+        bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData);
+        bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData);
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs
new file mode 100644
index 000000000..1cc09c00d
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs
@@ -0,0 +1,24 @@
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using System;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn
+{
+    class NetworkChangeEventArgs : EventArgs
+    {
+        public NetworkInfo Info;
+        public bool Connected;
+        public DisconnectReason DisconnectReason;
+
+        public NetworkChangeEventArgs(NetworkInfo info, bool connected, DisconnectReason disconnectReason = DisconnectReason.None)
+        {
+            Info = info;
+            Connected = connected;
+            DisconnectReason = disconnectReason;
+        }
+
+        public DisconnectReason DisconnectReasonOrDefault(DisconnectReason defaultReason)
+        {
+            return DisconnectReason == DisconnectReason.None ? defaultReason : DisconnectReason;
+        }
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs
new file mode 100644
index 000000000..47e48d0a1
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs
@@ -0,0 +1,16 @@
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0xBC)]
+    struct ConnectPrivateRequest
+    {
+        public SecurityConfig SecurityConfig;
+        public SecurityParameter SecurityParameter;
+        public UserConfig UserConfig;
+        public uint LocalCommunicationVersion;
+        public uint OptionUnknown;
+        public NetworkConfig NetworkConfig;
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs
new file mode 100644
index 000000000..6e890618c
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs
@@ -0,0 +1,18 @@
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
+{
+    /// <remarks>
+    /// Advertise data is appended separately (remaining data in the buffer).
+    /// </remarks>
+    [StructLayout(LayoutKind.Sequential, Size = 0x13C, Pack = 1)]
+    struct CreateAccessPointPrivateRequest
+    {
+        public SecurityConfig SecurityConfig;
+        public SecurityParameter SecurityParameter;
+        public UserConfig UserConfig;
+        public NetworkConfig NetworkConfig;
+        public AddressList AddressList;
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs
new file mode 100644
index 000000000..70ebf7e38
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs
@@ -0,0 +1,22 @@
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
+{
+    enum NetworkError : int
+    {
+        None,
+
+        PortUnreachable,
+
+        TooManyPlayers,
+        VersionTooLow,
+        VersionTooHigh,
+
+        ConnectFailure,
+        ConnectNotFound,
+        ConnectTimeout,
+        ConnectRejected,
+
+        RejectFailed,
+
+        Unknown = -1,
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs
new file mode 100644
index 000000000..acb0b36ac
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs
@@ -0,0 +1,10 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x4)]
+    struct NetworkErrorMessage
+    {
+        public NetworkError Error;
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs
new file mode 100644
index 000000000..c190d6ed1
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs
@@ -0,0 +1,115 @@
+using Ryujinx.Common.Memory;
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
+using System;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
+{
+    class Station : IDisposable
+    {
+        public NetworkInfo NetworkInfo;
+        public Array8<NodeLatestUpdate> LatestUpdates = new();
+
+        private readonly IUserLocalCommunicationService _parent;
+
+        public bool Connected { get; private set; }
+
+        public Station(IUserLocalCommunicationService parent)
+        {
+            _parent = parent;
+
+            _parent.NetworkClient.NetworkChange += NetworkChanged;
+        }
+
+        private void NetworkChanged(object sender, RyuLdn.NetworkChangeEventArgs e)
+        {
+            LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes);
+
+            NetworkInfo = e.Info;
+
+            if (Connected != e.Connected)
+            {
+                Connected = e.Connected;
+
+                if (Connected)
+                {
+                    _parent.SetState(NetworkState.StationConnected);
+                }
+                else
+                {
+                    _parent.SetDisconnectReason(e.DisconnectReasonOrDefault(DisconnectReason.DestroyedByUser));
+                }
+            }
+            else
+            {
+                _parent.SetState();
+            }
+        }
+
+        public void Dispose()
+        {
+            _parent.NetworkClient.DisconnectNetwork();
+
+            _parent.NetworkClient.NetworkChange -= NetworkChanged;
+        }
+
+        private ResultCode NetworkErrorToResult(NetworkError error)
+        {
+            return error switch
+            {
+                NetworkError.None => ResultCode.Success,
+                NetworkError.VersionTooLow => ResultCode.VersionTooLow,
+                NetworkError.VersionTooHigh => ResultCode.VersionTooHigh,
+                NetworkError.TooManyPlayers => ResultCode.TooManyPlayers,
+
+                NetworkError.ConnectFailure => ResultCode.ConnectFailure,
+                NetworkError.ConnectNotFound => ResultCode.ConnectNotFound,
+                NetworkError.ConnectTimeout => ResultCode.ConnectTimeout,
+                NetworkError.ConnectRejected => ResultCode.ConnectRejected,
+
+                _ => ResultCode.DeviceNotAvailable,
+            };
+        }
+
+        public ResultCode Connect(
+            SecurityConfig securityConfig,
+            UserConfig userConfig,
+            uint localCommunicationVersion,
+            uint optionUnknown,
+            NetworkInfo networkInfo)
+        {
+            ConnectRequest request = new()
+            {
+                SecurityConfig = securityConfig,
+                UserConfig = userConfig,
+                LocalCommunicationVersion = localCommunicationVersion,
+                OptionUnknown = optionUnknown,
+                NetworkInfo = networkInfo,
+            };
+
+            return NetworkErrorToResult(_parent.NetworkClient.Connect(request));
+        }
+
+        public ResultCode ConnectPrivate(
+            SecurityConfig securityConfig,
+            SecurityParameter securityParameter,
+            UserConfig userConfig,
+            uint localCommunicationVersion,
+            uint optionUnknown,
+            NetworkConfig networkConfig)
+        {
+            ConnectPrivateRequest request = new()
+            {
+                SecurityConfig = securityConfig,
+                SecurityParameter = securityParameter,
+                UserConfig = userConfig,
+                LocalCommunicationVersion = localCommunicationVersion,
+                OptionUnknown = optionUnknown,
+                NetworkConfig = networkConfig,
+            };
+
+            return NetworkErrorToResult(_parent.NetworkClient.ConnectPrivate(request));
+        }
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Mii/DatabaseImpl.cs b/src/Ryujinx.HLE/HOS/Services/Mii/DatabaseImpl.cs
index d9bcecee1..5041dc882 100644
--- a/src/Ryujinx.HLE/HOS/Services/Mii/DatabaseImpl.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Mii/DatabaseImpl.cs
@@ -290,7 +290,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii
         {
             coreData = new CoreData();
 
-            if (charInfo.IsValid())
+            if (!charInfo.IsValid())
             {
                 return ResultCode.InvalidCharInfo;
             }
diff --git a/src/Ryujinx.HLE/HOS/Services/Mii/Types/CoreData.cs b/src/Ryujinx.HLE/HOS/Services/Mii/Types/CoreData.cs
index 00e49ecb9..4f6e289e2 100644
--- a/src/Ryujinx.HLE/HOS/Services/Mii/Types/CoreData.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Mii/Types/CoreData.cs
@@ -1,4 +1,5 @@
-using System;
+using Ryujinx.Common.Memory;
+using System;
 using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
 using static Ryujinx.HLE.HOS.Services.Mii.Types.RandomMiiConstants;
@@ -10,9 +11,9 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
     {
         public const int Size = 0x30;
 
-        private byte _storage;
+        private Array48<byte> _storage;
 
-        public Span<byte> Storage => MemoryMarshal.CreateSpan(ref _storage, Size);
+        public Span<byte> Storage => _storage.AsSpan();
 
         [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 0x18)]
         public struct ElementInfo
diff --git a/src/Ryujinx.HLE/HOS/Services/Mii/Types/Nickname.cs b/src/Ryujinx.HLE/HOS/Services/Mii/Types/Nickname.cs
index dc21d8e56..6665ca6d4 100644
--- a/src/Ryujinx.HLE/HOS/Services/Mii/Types/Nickname.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Mii/Types/Nickname.cs
@@ -1,4 +1,5 @@
-using System;
+using Ryujinx.Common.Memory;
+using System;
 using System.Runtime.InteropServices;
 using System.Text;
 
@@ -10,12 +11,12 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
         public const int CharCount = 10;
         private const int SizeConst = (CharCount + 1) * 2;
 
-        private byte _storage;
+        private Array22<byte> _storage;
 
         public static Nickname Default => FromString("no name");
         public static Nickname Question => FromString("???");
 
-        public Span<byte> Raw => MemoryMarshal.CreateSpan(ref _storage, SizeConst);
+        public Span<byte> Raw => _storage.AsSpan();
 
         private ReadOnlySpan<ushort> Characters => MemoryMarshal.Cast<byte, ushort>(Raw);
 
diff --git a/src/Ryujinx.HLE/HOS/Services/Mii/Types/RandomMiiConstants.cs b/src/Ryujinx.HLE/HOS/Services/Mii/Types/RandomMiiConstants.cs
index 590f0f01e..ab08247b4 100644
--- a/src/Ryujinx.HLE/HOS/Services/Mii/Types/RandomMiiConstants.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Mii/Types/RandomMiiConstants.cs
@@ -320,7 +320,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
         };
 
         private static ReadOnlySpan<byte> RandomMiiFacelineColorRawArray => new byte[]
@@ -399,8 +399,8 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
-   ,     };
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        };
 
         private static ReadOnlySpan<byte> RandomMiiFacelineWrinkleRawArray => new byte[]
         {
@@ -633,8 +633,8 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
-    ,    };
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        };
 
         private static ReadOnlySpan<byte> RandomMiiFacelineMakeRawArray => new byte[]
         {
@@ -867,8 +867,8 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
-     ,   };
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        };
 
         private static ReadOnlySpan<byte> RandomMiiHairTypeRawArray => new byte[]
         {
@@ -1101,8 +1101,8 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
-      ,  };
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        };
 
         private static ReadOnlySpan<byte> RandomMiiHairColorRawArray => new byte[]
         {
@@ -1218,8 +1218,8 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
-       , };
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        };
 
         private static ReadOnlySpan<byte> RandomMiiEyeTypeRawArray => new byte[]
         {
@@ -1452,8 +1452,8 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
             0x2e, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
-        ,};
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        };
 
         private static ReadOnlySpan<byte> RandomMiiEyeColorRawArray => new byte[]
         {
@@ -1493,7 +1493,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
         };
 
         private static ReadOnlySpan<byte> RandomMiiEyebrowTypeRawArray => new byte[]
@@ -1727,7 +1727,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
         };
 
         private static ReadOnlySpan<byte> RandomMiiNoseTypeRawArray => new byte[]
@@ -1961,7 +1961,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
         };
 
         private static ReadOnlySpan<byte> RandomMiiMouthTypeRawArray => new byte[]
@@ -2195,7 +2195,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
         };
 
         private static ReadOnlySpan<byte> RandomMiiGlassTypeRawArray => new byte[]
@@ -2236,7 +2236,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
         };
         #endregion
     }
diff --git a/src/Ryujinx.HLE/HOS/Services/Mii/Types/StoreData.cs b/src/Ryujinx.HLE/HOS/Services/Mii/Types/StoreData.cs
index 994f6b7ce..178b48318 100644
--- a/src/Ryujinx.HLE/HOS/Services/Mii/Types/StoreData.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Mii/Types/StoreData.cs
@@ -62,7 +62,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
 
         private ushort CalculateDataCrc()
         {
-            return Helper.CalculateCrc16(AsSpanWithoutDeviceCrc(), 0, true);
+            return Helper.CalculateCrc16(AsSpanWithoutCrcs(), 0, true);
         }
 
         private ushort CalculateDeviceCrc()
@@ -71,7 +71,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
 
             ushort deviceIdCrc16 = Helper.CalculateCrc16(SpanHelpers.AsByteSpan(ref deviceId), 0, false);
 
-            return Helper.CalculateCrc16(AsSpan(), deviceIdCrc16, true);
+            return Helper.CalculateCrc16(AsSpanWithoutDeviceCrc(), deviceIdCrc16, true);
         }
 
         private ReadOnlySpan<byte> AsSpan()
@@ -84,6 +84,11 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
             return AsSpan()[..(Size - 2)];
         }
 
+        private ReadOnlySpan<byte> AsSpanWithoutCrcs()
+        {
+            return AsSpan()[..(Size - 4)];
+        }
+
         public static StoreData BuildDefault(UtilityImpl utilImpl, uint index)
         {
             StoreData result = new()
diff --git a/src/Ryujinx.HLE/HOS/Services/Mii/Types/Ver3StoreData.cs b/src/Ryujinx.HLE/HOS/Services/Mii/Types/Ver3StoreData.cs
index 70bb348b5..1c7db8e66 100644
--- a/src/Ryujinx.HLE/HOS/Services/Mii/Types/Ver3StoreData.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Mii/Types/Ver3StoreData.cs
@@ -1,4 +1,6 @@
-using System;
+using Ryujinx.Common.Memory;
+using Ryujinx.Common.Utilities;
+using System;
 using System.Runtime.InteropServices;
 
 namespace Ryujinx.HLE.HOS.Services.Mii.Types
@@ -8,9 +10,9 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
     {
         public const int Size = 0x60;
 
-        private byte _storage;
+        private Array96<byte> _storage;
 
-        public Span<byte> Storage => MemoryMarshal.CreateSpan(ref _storage, Size);
+        public Span<byte> Storage => _storage.AsSpan();
 
         // TODO: define all getters/setters
     }
diff --git a/src/Ryujinx.HLE/HOS/Services/Mm/IRequest.cs b/src/Ryujinx.HLE/HOS/Services/Mm/IRequest.cs
deleted file mode 100644
index 31d325a94..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Mm/IRequest.cs
+++ /dev/null
@@ -1,196 +0,0 @@
-using Ryujinx.Common.Logging;
-using Ryujinx.HLE.HOS.Services.Mm.Types;
-using System.Collections.Generic;
-
-namespace Ryujinx.HLE.HOS.Services.Mm
-{
-    [Service("mm:u")]
-    class IRequest : IpcService
-    {
-        private readonly object _sessionListLock = new();
-        private readonly List<MultiMediaSession> _sessionList = new();
-
-        private uint _uniqueId = 1;
-
-        public IRequest(ServiceCtx context) { }
-
-        [CommandCmif(0)]
-        // InitializeOld(u32, u32, u32)
-        public ResultCode InitializeOld(ServiceCtx context)
-        {
-            MultiMediaOperationType operationType = (MultiMediaOperationType)context.RequestData.ReadUInt32();
-            int fgmId = context.RequestData.ReadInt32();
-            bool isAutoClearEvent = context.RequestData.ReadInt32() != 0;
-
-            Logger.Stub?.PrintStub(LogClass.ServiceMm, new { operationType, fgmId, isAutoClearEvent });
-
-            Register(operationType, fgmId, isAutoClearEvent);
-
-            return ResultCode.Success;
-        }
-
-        [CommandCmif(1)]
-        // FinalizeOld(u32)
-        public ResultCode FinalizeOld(ServiceCtx context)
-        {
-            MultiMediaOperationType operationType = (MultiMediaOperationType)context.RequestData.ReadUInt32();
-
-            Logger.Stub?.PrintStub(LogClass.ServiceMm, new { operationType });
-
-            lock (_sessionListLock)
-            {
-                _sessionList.Remove(GetSessionByType(operationType));
-            }
-
-            return ResultCode.Success;
-        }
-
-        [CommandCmif(2)]
-        // SetAndWaitOld(u32, u32, u32)
-        public ResultCode SetAndWaitOld(ServiceCtx context)
-        {
-            MultiMediaOperationType operationType = (MultiMediaOperationType)context.RequestData.ReadUInt32();
-            uint frequenceHz = context.RequestData.ReadUInt32();
-            int timeout = context.RequestData.ReadInt32();
-
-            Logger.Stub?.PrintStub(LogClass.ServiceMm, new { operationType, frequenceHz, timeout });
-
-            lock (_sessionListLock)
-            {
-                GetSessionByType(operationType)?.SetAndWait(frequenceHz, timeout);
-            }
-
-            return ResultCode.Success;
-        }
-
-        [CommandCmif(3)]
-        // GetOld(u32) -> u32
-        public ResultCode GetOld(ServiceCtx context)
-        {
-            MultiMediaOperationType operationType = (MultiMediaOperationType)context.RequestData.ReadUInt32();
-
-            Logger.Stub?.PrintStub(LogClass.ServiceMm, new { operationType });
-
-            lock (_sessionListLock)
-            {
-                MultiMediaSession session = GetSessionByType(operationType);
-
-                uint currentValue = session == null ? 0 : session.CurrentValue;
-
-                context.ResponseData.Write(currentValue);
-            }
-
-            return ResultCode.Success;
-        }
-
-        [CommandCmif(4)]
-        // Initialize(u32, u32, u32) -> u32
-        public ResultCode Initialize(ServiceCtx context)
-        {
-            MultiMediaOperationType operationType = (MultiMediaOperationType)context.RequestData.ReadUInt32();
-            int fgmId = context.RequestData.ReadInt32();
-            bool isAutoClearEvent = context.RequestData.ReadInt32() != 0;
-
-            Logger.Stub?.PrintStub(LogClass.ServiceMm, new { operationType, fgmId, isAutoClearEvent });
-
-            uint id = Register(operationType, fgmId, isAutoClearEvent);
-
-            context.ResponseData.Write(id);
-
-            return ResultCode.Success;
-        }
-
-        [CommandCmif(5)]
-        // Finalize(u32)
-        public ResultCode Finalize(ServiceCtx context)
-        {
-            uint id = context.RequestData.ReadUInt32();
-
-            Logger.Stub?.PrintStub(LogClass.ServiceMm, new { id });
-
-            lock (_sessionListLock)
-            {
-                _sessionList.Remove(GetSessionById(id));
-            }
-
-            return ResultCode.Success;
-        }
-
-        [CommandCmif(6)]
-        // SetAndWait(u32, u32, u32)
-        public ResultCode SetAndWait(ServiceCtx context)
-        {
-            uint id = context.RequestData.ReadUInt32();
-            uint frequenceHz = context.RequestData.ReadUInt32();
-            int timeout = context.RequestData.ReadInt32();
-
-            Logger.Stub?.PrintStub(LogClass.ServiceMm, new { id, frequenceHz, timeout });
-
-            lock (_sessionListLock)
-            {
-                GetSessionById(id)?.SetAndWait(frequenceHz, timeout);
-            }
-
-            return ResultCode.Success;
-        }
-
-        [CommandCmif(7)]
-        // Get(u32) -> u32
-        public ResultCode Get(ServiceCtx context)
-        {
-            uint id = context.RequestData.ReadUInt32();
-
-            Logger.Stub?.PrintStub(LogClass.ServiceMm, new { id });
-
-            lock (_sessionListLock)
-            {
-                MultiMediaSession session = GetSessionById(id);
-
-                uint currentValue = session == null ? 0 : session.CurrentValue;
-
-                context.ResponseData.Write(currentValue);
-            }
-
-            return ResultCode.Success;
-        }
-
-        private MultiMediaSession GetSessionById(uint id)
-        {
-            foreach (MultiMediaSession session in _sessionList)
-            {
-                if (session.Id == id)
-                {
-                    return session;
-                }
-            }
-
-            return null;
-        }
-
-        private MultiMediaSession GetSessionByType(MultiMediaOperationType type)
-        {
-            foreach (MultiMediaSession session in _sessionList)
-            {
-                if (session.Type == type)
-                {
-                    return session;
-                }
-            }
-
-            return null;
-        }
-
-        private uint Register(MultiMediaOperationType type, int fgmId, bool isAutoClearEvent)
-        {
-            lock (_sessionListLock)
-            {
-                // Nintendo ignore the fgm id as the other interfaces were deprecated.
-                MultiMediaSession session = new(_uniqueId++, type, isAutoClearEvent);
-
-                _sessionList.Add(session);
-
-                return session.Id;
-            }
-        }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Mm/Types/MultiMediaOperationType.cs b/src/Ryujinx.HLE/HOS/Services/Mm/Types/MultiMediaOperationType.cs
deleted file mode 100644
index 095dbfc31..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Mm/Types/MultiMediaOperationType.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Mm.Types
-{
-    enum MultiMediaOperationType : uint
-    {
-        Ram = 2,
-        NvEnc = 5,
-        NvDec = 6,
-        NvJpg = 7,
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Mm/Types/MultiMediaSession.cs b/src/Ryujinx.HLE/HOS/Services/Mm/Types/MultiMediaSession.cs
deleted file mode 100644
index 32b52ca54..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Mm/Types/MultiMediaSession.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Mm.Types
-{
-    class MultiMediaSession
-    {
-        public MultiMediaOperationType Type { get; }
-
-        public bool IsAutoClearEvent { get; }
-        public uint Id { get; }
-        public uint CurrentValue { get; private set; }
-
-        public MultiMediaSession(uint id, MultiMediaOperationType type, bool isAutoClearEvent)
-        {
-            Type = type;
-            Id = id;
-            IsAutoClearEvent = isAutoClearEvent;
-            CurrentValue = 0;
-        }
-
-        public void SetAndWait(uint value, int timeout)
-        {
-            CurrentValue = value;
-        }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ovln/IReceiverService.cs b/src/Ryujinx.HLE/HOS/Services/Ovln/IReceiverService.cs
deleted file mode 100644
index 99e929a70..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Ovln/IReceiverService.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Ovln
-{
-    [Service("ovln:rcv")]
-    class IReceiverService : IpcService
-    {
-        public IReceiverService(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ovln/ISenderService.cs b/src/Ryujinx.HLE/HOS/Services/Ovln/ISenderService.cs
deleted file mode 100644
index e445c16cd..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Ovln/ISenderService.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Ovln
-{
-    [Service("ovln:snd")]
-    class ISenderService : IpcService
-    {
-        public ISenderService(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Psc/IPmControl.cs b/src/Ryujinx.HLE/HOS/Services/Psc/IPmControl.cs
deleted file mode 100644
index 6682a8481..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Psc/IPmControl.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Psc
-{
-    [Service("psc:c")]
-    class IPmControl : IpcService
-    {
-        public IPmControl(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Psc/IPmService.cs b/src/Ryujinx.HLE/HOS/Services/Psc/IPmService.cs
deleted file mode 100644
index 1be338660..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Psc/IPmService.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Psc
-{
-    [Service("psc:m")]
-    class IPmService : IpcService
-    {
-        public IPmService(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Psc/IPmUnknown.cs b/src/Ryujinx.HLE/HOS/Services/Psc/IPmUnknown.cs
deleted file mode 100644
index 95aff9ece..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Psc/IPmUnknown.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Psc
-{
-    [Service("psc:l")] // 9.0.0+
-    class IPmUnknown : IpcService
-    {
-        public IPmUnknown(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/ServerBase.cs b/src/Ryujinx.HLE/HOS/Services/ServerBase.cs
index f107f5026..9d7e4d4c5 100644
--- a/src/Ryujinx.HLE/HOS/Services/ServerBase.cs
+++ b/src/Ryujinx.HLE/HOS/Services/ServerBase.cs
@@ -6,6 +6,7 @@ using Ryujinx.HLE.HOS.Kernel;
 using Ryujinx.HLE.HOS.Kernel.Ipc;
 using Ryujinx.HLE.HOS.Kernel.Process;
 using Ryujinx.HLE.HOS.Kernel.Threading;
+using Ryujinx.Horizon;
 using Ryujinx.Horizon.Common;
 using System;
 using System.Buffers;
@@ -172,6 +173,13 @@ namespace Ryujinx.HLE.HOS.Services
             _selfProcess = KernelStatic.GetCurrentProcess();
             _selfThread = KernelStatic.GetCurrentThread();
 
+            HorizonStatic.Register(
+                default,
+                _context.Syscall,
+                _selfProcess.CpuMemory,
+                _selfThread.ThreadContext,
+                (int)_selfThread.ThreadContext.GetX(1));
+
             if (SmObjectFactory != null)
             {
                 _context.Syscall.ManageNamedPort(out int serverPortHandle, "sm:", 50);
diff --git a/src/Ryujinx.HLE/HOS/Services/Sm/IUserInterface.cs b/src/Ryujinx.HLE/HOS/Services/Sm/IUserInterface.cs
index 2d8502208..3dc82035f 100644
--- a/src/Ryujinx.HLE/HOS/Services/Sm/IUserInterface.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Sm/IUserInterface.cs
@@ -8,6 +8,7 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Reflection;
+using System.Text;
 
 namespace Ryujinx.HLE.HOS.Services.Sm
 {
@@ -28,7 +29,7 @@ namespace Ryujinx.HLE.HOS.Services.Sm
 
         static IUserInterface()
         {
-            _services = Assembly.GetExecutingAssembly().GetTypes()
+            _services = typeof(IUserInterface).Assembly.GetTypes()
                 .SelectMany(type => type.GetCustomAttributes(typeof(ServiceAttribute), true)
                 .Select(service => (((ServiceAttribute)service).Name, type)))
                 .ToDictionary(service => service.Name, service => service.type);
@@ -235,7 +236,7 @@ namespace Ryujinx.HLE.HOS.Services.Sm
 
         private static string ReadName(ServiceCtx context)
         {
-            string name = string.Empty;
+            StringBuilder nameBuilder = new();
 
             for (int index = 0; index < 8 &&
                 context.RequestData.BaseStream.Position <
@@ -245,11 +246,11 @@ namespace Ryujinx.HLE.HOS.Services.Sm
 
                 if (chr >= 0x20 && chr < 0x7f)
                 {
-                    name += (char)chr;
+                    nameBuilder.Append((char)chr);
                 }
             }
 
-            return name;
+            return nameBuilder.ToString();
         }
 
         public override void DestroyAtExit()
diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs
index dfc2a6723..2d8fce1df 100644
--- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs
@@ -299,11 +299,21 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
         {
             try
             {
+                LinuxError result = WinSockHelper.ValidateSocketOption(option, level, write: false);
+
+                if (result != LinuxError.SUCCESS)
+                {
+                    Logger.Warning?.Print(LogClass.ServiceBsd, $"Invalid GetSockOpt Option: {option} Level: {level}");
+
+                    return result;
+                }
+
                 if (!WinSockHelper.TryConvertSocketOption(option, level, out SocketOptionName optionName))
                 {
                     Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported GetSockOpt Option: {option} Level: {level}");
+                    optionValue.Clear();
 
-                    return LinuxError.EOPNOTSUPP;
+                    return LinuxError.SUCCESS;
                 }
 
                 byte[] tempOptionValue = new byte[optionValue.Length];
@@ -324,11 +334,20 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
         {
             try
             {
+                LinuxError result = WinSockHelper.ValidateSocketOption(option, level, write: true);
+
+                if (result != LinuxError.SUCCESS)
+                {
+                    Logger.Warning?.Print(LogClass.ServiceBsd, $"Invalid SetSockOpt Option: {option} Level: {level}");
+
+                    return result;
+                }
+
                 if (!WinSockHelper.TryConvertSocketOption(option, level, out SocketOptionName optionName))
                 {
                     Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported SetSockOpt Option: {option} Level: {level}");
 
-                    return LinuxError.EOPNOTSUPP;
+                    return LinuxError.SUCCESS;
                 }
 
                 int value = optionValue.Length >= 4 ? MemoryMarshal.Read<int>(optionValue) : MemoryMarshal.Read<byte>(optionValue);
diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/WinSockHelper.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/WinSockHelper.cs
index 9df180235..e2ef75f80 100644
--- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/WinSockHelper.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/WinSockHelper.cs
@@ -183,6 +183,104 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
             { BsdSocketOption.TcpKeepCnt,   SocketOptionName.TcpKeepAliveRetryCount },
         };
 
+        [Flags]
+        private enum OptionDir
+        {
+            Get = 1 << 0,
+            Set = 1 << 1,
+            GetSet = Get | Set,
+        }
+
+        private static readonly Dictionary<BsdSocketOption, OptionDir> _validSoSocketOptionMap = new()
+        {
+            { BsdSocketOption.SoDebug,         OptionDir.GetSet },
+            { BsdSocketOption.SoAcceptConn,    OptionDir.Get },
+            { BsdSocketOption.SoReuseAddr,     OptionDir.GetSet },
+            { BsdSocketOption.SoKeepAlive,     OptionDir.GetSet },
+            { BsdSocketOption.SoDontRoute,     OptionDir.GetSet },
+            { BsdSocketOption.SoBroadcast,     OptionDir.GetSet },
+            { BsdSocketOption.SoUseLoopBack,   OptionDir.GetSet },
+            { BsdSocketOption.SoLinger,        OptionDir.GetSet },
+            { BsdSocketOption.SoOobInline,     OptionDir.GetSet },
+            { BsdSocketOption.SoReusePort,     OptionDir.GetSet },
+            { BsdSocketOption.SoTimestamp,     OptionDir.GetSet },
+            { BsdSocketOption.SoNoSigpipe,     OptionDir.GetSet },
+            { BsdSocketOption.SoAcceptFilter,  OptionDir.GetSet },
+            { BsdSocketOption.SoSndBuf,        OptionDir.GetSet },
+            { BsdSocketOption.SoRcvBuf,        OptionDir.GetSet },
+            { BsdSocketOption.SoSndLoWat,      OptionDir.GetSet },
+            { BsdSocketOption.SoRcvLoWat,      OptionDir.GetSet },
+            { BsdSocketOption.SoSndTimeo,      OptionDir.GetSet },
+            { BsdSocketOption.SoRcvTimeo,      OptionDir.GetSet },
+            { BsdSocketOption.SoError,         OptionDir.Get },
+            { BsdSocketOption.SoType,          OptionDir.Get },
+            { BsdSocketOption.SoLabel,         OptionDir.Get },
+            { BsdSocketOption.SoPeerLabel,     OptionDir.Get },
+            { BsdSocketOption.SoListenQLimit,  OptionDir.Get },
+            { BsdSocketOption.SoListenQLen,    OptionDir.Get },
+            { BsdSocketOption.SoListenIncQLen, OptionDir.Get },
+            { BsdSocketOption.SoSetFib,        OptionDir.Set },
+            { BsdSocketOption.SoUserCookie,    OptionDir.Set },
+            { BsdSocketOption.SoProtocol,      OptionDir.Get },
+            { BsdSocketOption.SoBinTime,       OptionDir.GetSet },
+            { BsdSocketOption.SoNoOffload,     OptionDir.Set },
+            { BsdSocketOption.SoNoDdp,         OptionDir.Set },
+            { BsdSocketOption.SoReusePortLb,   OptionDir.GetSet },
+        };
+
+        private static readonly Dictionary<BsdSocketOption, OptionDir> _validIpSocketOptionMap = new()
+        {
+            { BsdSocketOption.IpOptions,              OptionDir.GetSet },
+            { BsdSocketOption.IpHdrIncl,              OptionDir.GetSet },
+            { BsdSocketOption.IpTos,                  OptionDir.GetSet },
+            { BsdSocketOption.IpTtl,                  OptionDir.GetSet },
+            { BsdSocketOption.IpRecvOpts,             OptionDir.GetSet },
+            { BsdSocketOption.IpRecvRetOpts,          OptionDir.GetSet },
+            { BsdSocketOption.IpRecvDstAddr,          OptionDir.GetSet },
+            { BsdSocketOption.IpRetOpts,              OptionDir.GetSet },
+            { BsdSocketOption.IpMulticastIf,          OptionDir.GetSet },
+            { BsdSocketOption.IpMulticastTtl,         OptionDir.GetSet },
+            { BsdSocketOption.IpMulticastLoop,        OptionDir.GetSet },
+            { BsdSocketOption.IpAddMembership,        OptionDir.GetSet },
+            { BsdSocketOption.IpDropMembership,       OptionDir.GetSet },
+            { BsdSocketOption.IpMulticastVif,         OptionDir.GetSet },
+            { BsdSocketOption.IpRsvpOn,               OptionDir.GetSet },
+            { BsdSocketOption.IpRsvpOff,              OptionDir.GetSet },
+            { BsdSocketOption.IpRsvpVifOn,            OptionDir.GetSet },
+            { BsdSocketOption.IpRsvpVifOff,           OptionDir.GetSet },
+            { BsdSocketOption.IpPortRange,            OptionDir.GetSet },
+            { BsdSocketOption.IpRecvIf,               OptionDir.GetSet },
+            { BsdSocketOption.IpIpsecPolicy,          OptionDir.GetSet },
+            { BsdSocketOption.IpOnesBcast,            OptionDir.GetSet },
+            { BsdSocketOption.IpBindany,              OptionDir.GetSet },
+            { BsdSocketOption.IpBindMulti,            OptionDir.GetSet },
+            { BsdSocketOption.IpRssListenBucket,      OptionDir.GetSet },
+            { BsdSocketOption.IpOrigDstAddr,          OptionDir.GetSet },
+            { BsdSocketOption.IpRecvTtl,              OptionDir.GetSet },
+            { BsdSocketOption.IpMinTtl,               OptionDir.GetSet },
+            { BsdSocketOption.IpDontFrag,             OptionDir.GetSet },
+            { BsdSocketOption.IpRecvTos,              OptionDir.GetSet },
+            { BsdSocketOption.IpAddSourceMembership,  OptionDir.GetSet },
+            { BsdSocketOption.IpDropSourceMembership, OptionDir.GetSet },
+            { BsdSocketOption.IpBlockSource,          OptionDir.GetSet },
+            { BsdSocketOption.IpUnblockSource,        OptionDir.GetSet },
+        };
+
+        private static readonly Dictionary<BsdSocketOption, OptionDir> _validTcpSocketOptionMap = new()
+        {
+            { BsdSocketOption.TcpNoDelay,    OptionDir.GetSet },
+            { BsdSocketOption.TcpMaxSeg,     OptionDir.GetSet },
+            { BsdSocketOption.TcpNoPush,     OptionDir.GetSet },
+            { BsdSocketOption.TcpNoOpt,      OptionDir.GetSet },
+            { BsdSocketOption.TcpMd5Sig,     OptionDir.GetSet },
+            { BsdSocketOption.TcpInfo,       OptionDir.GetSet },
+            { BsdSocketOption.TcpCongestion, OptionDir.GetSet },
+            { BsdSocketOption.TcpKeepInit,   OptionDir.GetSet },
+            { BsdSocketOption.TcpKeepIdle,   OptionDir.GetSet },
+            { BsdSocketOption.TcpKeepIntvl,  OptionDir.GetSet },
+            { BsdSocketOption.TcpKeepCnt,    OptionDir.GetSet },
+        };
+
         public static LinuxError ConvertError(WsaError errorCode)
         {
             if (OperatingSystem.IsMacOS())
@@ -221,5 +319,29 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
 
             return table.TryGetValue(option, out name);
         }
+
+        public static LinuxError ValidateSocketOption(BsdSocketOption option, SocketOptionLevel level, bool write)
+        {
+            var table = level switch
+            {
+                SocketOptionLevel.Socket => _validSoSocketOptionMap,
+                SocketOptionLevel.IP => _validIpSocketOptionMap,
+                SocketOptionLevel.Tcp => _validTcpSocketOptionMap,
+                _ => null,
+            };
+
+            OptionDir dir = write ? OptionDir.Set : OptionDir.Get;
+
+            if (table == null || !table.TryGetValue(option, out OptionDir validDir))
+            {
+                return LinuxError.ENOPROTOOPT;
+            }
+            else if ((validDir & dir) != dir)
+            {
+                return LinuxError.EOPNOTSUPP;
+            }
+
+            return LinuxError.SUCCESS;
+        }
     }
 }
diff --git a/src/Ryujinx.HLE/HOS/Services/Srepo/ISrepoService.cs b/src/Ryujinx.HLE/HOS/Services/Srepo/ISrepoService.cs
deleted file mode 100644
index f5467983a..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Srepo/ISrepoService.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Srepo
-{
-    [Service("srepo:a")] // 5.0.0+
-    [Service("srepo:u")] // 5.0.0+
-    class ISrepoService : IpcService
-    {
-        public ISrepoService(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/BufferQueueProducer.cs b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/BufferQueueProducer.cs
index 887d1861f..55bfac84d 100644
--- a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/BufferQueueProducer.cs
+++ b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/BufferQueueProducer.cs
@@ -669,6 +669,12 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
 
             lock (Core.Lock)
             {
+                // If we are replacing a buffer that has already been queued, make sure we release the references.
+                if (Core.Slots[slot].BufferState == BufferState.Queued)
+                {
+                    Core.Slots[slot].GraphicBuffer.Object.DecrementNvMapHandleRefCount(Core.Owner);
+                }
+
                 Core.Slots[slot].BufferState = BufferState.Free;
                 Core.Slots[slot].Fence = AndroidFence.NoFence;
                 Core.Slots[slot].RequestBufferCalled = false;
diff --git a/src/Ryujinx.HLE/HOS/Services/Usb/IClientRootSession.cs b/src/Ryujinx.HLE/HOS/Services/Usb/IClientRootSession.cs
deleted file mode 100644
index b41b8a48c..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Usb/IClientRootSession.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Usb
-{
-    [Service("usb:hs")]
-    [Service("usb:hs:a")] // 7.0.0+
-    class IClientRootSession : IpcService
-    {
-        public IClientRootSession(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Usb/IDsService.cs b/src/Ryujinx.HLE/HOS/Services/Usb/IDsService.cs
deleted file mode 100644
index ee6c8f070..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Usb/IDsService.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Usb
-{
-    [Service("usb:ds")]
-    class IDsService : IpcService
-    {
-        public IDsService(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Usb/IPdCradleManager.cs b/src/Ryujinx.HLE/HOS/Services/Usb/IPdCradleManager.cs
deleted file mode 100644
index 18cbce79a..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Usb/IPdCradleManager.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Usb
-{
-    [Service("usb:pd:c")]
-    class IPdCradleManager : IpcService
-    {
-        public IPdCradleManager(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Usb/IPdManager.cs b/src/Ryujinx.HLE/HOS/Services/Usb/IPdManager.cs
deleted file mode 100644
index 011debafd..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Usb/IPdManager.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Usb
-{
-    [Service("usb:pd")]
-    class IPdManager : IpcService
-    {
-        public IPdManager(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Usb/IPmService.cs b/src/Ryujinx.HLE/HOS/Services/Usb/IPmService.cs
deleted file mode 100644
index ed6bba694..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Usb/IPmService.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Usb
-{
-    [Service("usb:pm")]
-    class IPmService : IpcService
-    {
-        public IPmService(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Usb/IUnknown1.cs b/src/Ryujinx.HLE/HOS/Services/Usb/IUnknown1.cs
deleted file mode 100644
index 65bf1c9fa..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Usb/IUnknown1.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Usb
-{
-    [Service("usb:qdb")] // 7.0.0+
-    class IUnknown1 : IpcService
-    {
-        public IUnknown1(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Usb/IUnknown2.cs b/src/Ryujinx.HLE/HOS/Services/Usb/IUnknown2.cs
deleted file mode 100644
index e0bf0bf4a..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Usb/IUnknown2.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Usb
-{
-    [Service("usb:obsv")] // 8.0.0+
-    class IUnknown2 : IpcService
-    {
-        public IUnknown2(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Vi/RootService/IApplicationDisplayService.cs b/src/Ryujinx.HLE/HOS/Services/Vi/RootService/IApplicationDisplayService.cs
index 3fbd7d20d..143e21661 100644
--- a/src/Ryujinx.HLE/HOS/Services/Vi/RootService/IApplicationDisplayService.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Vi/RootService/IApplicationDisplayService.cs
@@ -142,7 +142,7 @@ namespace Ryujinx.HLE.HOS.Services.Vi.RootService
         // OpenDisplay(nn::vi::DisplayName) -> u64 display_id
         public ResultCode OpenDisplay(ServiceCtx context)
         {
-            string name = "";
+            StringBuilder nameBuilder = new();
 
             for (int index = 0; index < 8 && context.RequestData.BaseStream.Position < context.RequestData.BaseStream.Length; index++)
             {
@@ -150,11 +150,11 @@ namespace Ryujinx.HLE.HOS.Services.Vi.RootService
 
                 if (chr >= 0x20 && chr < 0x7f)
                 {
-                    name += (char)chr;
+                    nameBuilder.Append((char)chr);
                 }
             }
 
-            return OpenDisplayImpl(context, name);
+            return OpenDisplayImpl(context, nameBuilder.ToString());
         }
 
         [CommandCmif(1011)]
diff --git a/src/Ryujinx.HLE/HOS/Services/Wlan/IInfraManager.cs b/src/Ryujinx.HLE/HOS/Services/Wlan/IInfraManager.cs
deleted file mode 100644
index a84625780..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Wlan/IInfraManager.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Wlan
-{
-    [Service("wlan:inf")]
-    class IInfraManager : IpcService
-    {
-        public IInfraManager(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Wlan/ILocalGetActionFrame.cs b/src/Ryujinx.HLE/HOS/Services/Wlan/ILocalGetActionFrame.cs
deleted file mode 100644
index 060e5854d..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Wlan/ILocalGetActionFrame.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Wlan
-{
-    [Service("wlan:lga")]
-    class ILocalGetActionFrame : IpcService
-    {
-        public ILocalGetActionFrame(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Wlan/ILocalGetFrame.cs b/src/Ryujinx.HLE/HOS/Services/Wlan/ILocalGetFrame.cs
deleted file mode 100644
index d726b7d49..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Wlan/ILocalGetFrame.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Wlan
-{
-    [Service("wlan:lg")]
-    class ILocalGetFrame : IpcService
-    {
-        public ILocalGetFrame(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Wlan/ILocalManager.cs b/src/Ryujinx.HLE/HOS/Services/Wlan/ILocalManager.cs
deleted file mode 100644
index 3db07ac73..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Wlan/ILocalManager.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Wlan
-{
-    [Service("wlan:lcl")]
-    class ILocalManager : IpcService
-    {
-        public ILocalManager(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Wlan/ISocketGetFrame.cs b/src/Ryujinx.HLE/HOS/Services/Wlan/ISocketGetFrame.cs
deleted file mode 100644
index 5c9329234..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Wlan/ISocketGetFrame.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Wlan
-{
-    [Service("wlan:sg")]
-    class ISocketGetFrame : IpcService
-    {
-        public ISocketGetFrame(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Wlan/ISocketManager.cs b/src/Ryujinx.HLE/HOS/Services/Wlan/ISocketManager.cs
deleted file mode 100644
index 1b40b80e4..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Wlan/ISocketManager.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Wlan
-{
-    [Service("wlan:soc")]
-    class ISocketManager : IpcService
-    {
-        public ISocketManager(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Wlan/IUnknown1.cs b/src/Ryujinx.HLE/HOS/Services/Wlan/IUnknown1.cs
deleted file mode 100644
index 731f8c0a9..000000000
--- a/src/Ryujinx.HLE/HOS/Services/Wlan/IUnknown1.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Wlan
-{
-    [Service("wlan:dtc")] // 6.0.0+
-    class IUnknown1 : IpcService
-    {
-        public IUnknown1(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs b/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs
index a2b26bbc5..245aba778 100644
--- a/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs
+++ b/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs
@@ -151,11 +151,15 @@ namespace Ryujinx.Headless.SDL2.OpenGL
             GL.Clear(ClearBufferMask.ColorBufferBit);
             SwapBuffers();
 
-            if (IsFullscreen)
+            if (IsExclusiveFullscreen)
+            {
+                Renderer?.Window.SetSize(ExclusiveFullscreenWidth, ExclusiveFullscreenHeight);
+                MouseDriver.SetClientSize(ExclusiveFullscreenWidth, ExclusiveFullscreenHeight);
+            }
+            else if (IsFullscreen)
             {
                 // NOTE: grabbing the main display's dimensions directly as OpenGL doesn't scale along like the VulkanWindow.
-                // we might have to amend this if people run this on a non-primary display set to a different resolution.
-                if (SDL_GetDisplayBounds(0, out SDL_Rect displayBounds) < 0)
+                if (SDL_GetDisplayBounds(DisplayId, out SDL_Rect displayBounds) < 0)
                 {
                     Logger.Warning?.Print(LogClass.Application, $"Could not retrieve display bounds: {SDL_GetError()}");
 
diff --git a/src/Ryujinx.Headless.SDL2/Options.cs b/src/Ryujinx.Headless.SDL2/Options.cs
index e44cedec9..a1adcd024 100644
--- a/src/Ryujinx.Headless.SDL2/Options.cs
+++ b/src/Ryujinx.Headless.SDL2/Options.cs
@@ -14,9 +14,21 @@ namespace Ryujinx.Headless.SDL2
         [Option("profile", Required = false, HelpText = "Set the user profile to launch the game with.")]
         public string UserProfile { get; set; }
 
-        [Option("fullscreen", Required = false, HelpText = "Launch the game in fullscreen mode.")]
+        [Option("display-id", Required = false, Default = 0, HelpText = "Set the display to use - especially helpful for fullscreen mode. [0-n]")]
+        public int DisplayId { get; set; }
+
+        [Option("fullscreen", Required = false, Default = false, HelpText = "Launch the game in fullscreen mode.")]
         public bool IsFullscreen { get; set; }
 
+        [Option("exclusive-fullscreen", Required = false, Default = false, HelpText = "Launch the game in exclusive fullscreen mode.")]
+        public bool IsExclusiveFullscreen { get; set; }
+
+        [Option("exclusive-fullscreen-width", Required = false, Default = 1920, HelpText = "Set horizontal resolution for exclusive fullscreen mode.")]
+        public int ExclusiveFullscreenWidth { get; set; }
+
+        [Option("exclusive-fullscreen-height", Required = false, Default = 1080, HelpText = "Set vertical resolution for exclusive fullscreen mode.")]
+        public int ExclusiveFullscreenHeight { get; set; }
+
         // Input
 
         [Option("input-profile-1", Required = false, HelpText = "Set the input profile in use for Player 1.")]
@@ -196,6 +208,15 @@ namespace Ryujinx.Headless.SDL2
         [Option("preferred-gpu-vendor", Required = false, Default = "", HelpText = "When using the Vulkan backend, prefer using the GPU from the specified vendor.")]
         public string PreferredGPUVendor { get; set; }
 
+        [Option("anti-aliasing", Required = false, Default = AntiAliasing.None, HelpText = "Set the type of anti aliasing being used. [None|Fxaa|SmaaLow|SmaaMedium|SmaaHigh|SmaaUltra]")]
+        public AntiAliasing AntiAliasing { get; set; }
+
+        [Option("scaling-filter", Required = false, Default = ScalingFilter.Bilinear, HelpText = "Set the scaling filter. [Bilinear|Nearest|Fsr]")]
+        public ScalingFilter ScalingFilter { get; set; }
+
+        [Option("scaling-filter-level", Required = false, Default = 0, HelpText = "Set the scaling filter intensity (currently only applies to FSR). [0-100]")]
+        public int ScalingFilterLevel { get; set; }
+
         // Hacks
 
         [Option("expand-ram", Required = false, Default = false, HelpText = "Expands the RAM amount on the emulated system from 4GiB to 6GiB.")]
diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs
index 98cc5abf4..86d3e841d 100644
--- a/src/Ryujinx.Headless.SDL2/Program.cs
+++ b/src/Ryujinx.Headless.SDL2/Program.cs
@@ -556,7 +556,8 @@ namespace Ryujinx.Headless.SDL2
                 options.AspectRatio,
                 options.AudioVolume,
                 options.UseHypervisor ?? true,
-                options.MultiplayerLanInterfaceId);
+                options.MultiplayerLanInterfaceId,
+                Common.Configuration.Multiplayer.MultiplayerMode.Disabled);
 
             return new Switch(configuration);
         }
@@ -596,6 +597,13 @@ namespace Ryujinx.Headless.SDL2
             _window = window;
 
             _window.IsFullscreen = options.IsFullscreen;
+            _window.DisplayId = options.DisplayId;
+            _window.IsExclusiveFullscreen = options.IsExclusiveFullscreen;
+            _window.ExclusiveFullscreenWidth = options.ExclusiveFullscreenWidth;
+            _window.ExclusiveFullscreenHeight = options.ExclusiveFullscreenHeight;
+            _window.AntiAliasing = options.AntiAliasing;
+            _window.ScalingFilter = options.ScalingFilter;
+            _window.ScalingFilterLevel = options.ScalingFilterLevel;
 
             _emulationContext = InitializeEmulationContext(window, renderer, options);
 
diff --git a/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs b/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs
index f2f337a53..4a04b10a3 100644
--- a/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs
+++ b/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs
@@ -29,8 +29,16 @@ namespace Ryujinx.Headless.SDL2.Vulkan
 
         protected override void InitializeRenderer()
         {
-            Renderer?.Window.SetSize(DefaultWidth, DefaultHeight);
-            MouseDriver.SetClientSize(DefaultWidth, DefaultHeight);
+            if (IsExclusiveFullscreen)
+            {
+                Renderer?.Window.SetSize(ExclusiveFullscreenWidth, ExclusiveFullscreenHeight);
+                MouseDriver.SetClientSize(ExclusiveFullscreenWidth, ExclusiveFullscreenHeight);
+            }
+            else
+            {
+                Renderer?.Window.SetSize(DefaultWidth, DefaultHeight);
+                MouseDriver.SetClientSize(DefaultWidth, DefaultHeight);
+            }
         }
 
         private static void BasicInvoke(Action action)
diff --git a/src/Ryujinx.Headless.SDL2/WindowBase.cs b/src/Ryujinx.Headless.SDL2/WindowBase.cs
index c542a4e61..1b9556057 100644
--- a/src/Ryujinx.Headless.SDL2/WindowBase.cs
+++ b/src/Ryujinx.Headless.SDL2/WindowBase.cs
@@ -17,10 +17,11 @@ using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO;
-using System.Reflection;
 using System.Runtime.InteropServices;
 using System.Threading;
 using static SDL2.SDL;
+using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing;
+using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter;
 using Switch = Ryujinx.HLE.Switch;
 
 namespace Ryujinx.Headless.SDL2
@@ -29,8 +30,9 @@ namespace Ryujinx.Headless.SDL2
     {
         protected const int DefaultWidth = 1280;
         protected const int DefaultHeight = 720;
-        private const SDL_WindowFlags DefaultFlags = SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI | SDL_WindowFlags.SDL_WINDOW_RESIZABLE | SDL_WindowFlags.SDL_WINDOW_INPUT_FOCUS | SDL_WindowFlags.SDL_WINDOW_SHOWN;
         private const int TargetFps = 60;
+        private SDL_WindowFlags DefaultFlags = SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI | SDL_WindowFlags.SDL_WINDOW_RESIZABLE | SDL_WindowFlags.SDL_WINDOW_INPUT_FOCUS | SDL_WindowFlags.SDL_WINDOW_SHOWN;
+        private SDL_WindowFlags FullscreenFlag = 0;
 
         private static readonly ConcurrentQueue<Action> _mainThreadActions = new();
 
@@ -55,7 +57,14 @@ namespace Ryujinx.Headless.SDL2
         public IHostUiTheme HostUiTheme { get; }
         public int Width { get; private set; }
         public int Height { get; private set; }
+        public int DisplayId { get; set; }
         public bool IsFullscreen { get; set; }
+        public bool IsExclusiveFullscreen { get; set; }
+        public int ExclusiveFullscreenWidth { get; set; }
+        public int ExclusiveFullscreenHeight { get; set; }
+        public AntiAliasing AntiAliasing { get; set; }
+        public ScalingFilter ScalingFilter { get; set; }
+        public int ScalingFilterLevel { get; set; }
 
         protected SDL2MouseDriver MouseDriver;
         private readonly InputManager _inputManager;
@@ -122,7 +131,7 @@ namespace Ryujinx.Headless.SDL2
 
         private void SetWindowIcon()
         {
-            Stream iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Ryujinx.Headless.SDL2.Ryujinx.bmp");
+            Stream iconStream = typeof(WindowBase).Assembly.GetManifestResourceStream("Ryujinx.Headless.SDL2.Ryujinx.bmp");
             byte[] iconBytes = new byte[iconStream!.Length];
 
             if (iconStream.Read(iconBytes, 0, iconBytes.Length) != iconBytes.Length)
@@ -159,9 +168,24 @@ namespace Ryujinx.Headless.SDL2
             string titleIdSection = string.IsNullOrWhiteSpace(activeProcess.ProgramIdText) ? string.Empty : $" ({activeProcess.ProgramIdText.ToUpper()})";
             string titleArchSection = activeProcess.Is64Bit ? " (64-bit)" : " (32-bit)";
 
-            SDL_WindowFlags fullscreenFlag = IsFullscreen ? SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP : 0;
+            Width = DefaultWidth;
+            Height = DefaultHeight;
 
-            WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, DefaultWidth, DefaultHeight, DefaultFlags | fullscreenFlag | GetWindowFlags());
+            if (IsExclusiveFullscreen)
+            {
+                Width = ExclusiveFullscreenWidth;
+                Height = ExclusiveFullscreenHeight;
+
+                DefaultFlags = SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI;
+                FullscreenFlag = SDL_WindowFlags.SDL_WINDOW_FULLSCREEN;
+            }
+            else if (IsFullscreen)
+            {
+                DefaultFlags = SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI;
+                FullscreenFlag = SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP;
+            }
+
+            WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", SDL_WINDOWPOS_CENTERED_DISPLAY(DisplayId), SDL_WINDOWPOS_CENTERED_DISPLAY(DisplayId), Width, Height, DefaultFlags | FullscreenFlag | GetWindowFlags());
 
             if (WindowHandle == IntPtr.Zero)
             {
@@ -176,9 +200,6 @@ namespace Ryujinx.Headless.SDL2
 
             _windowId = SDL_GetWindowID(WindowHandle);
             SDL2Driver.Instance.RegisterWindow(_windowId, HandleWindowEvent);
-
-            Width = DefaultWidth;
-            Height = DefaultHeight;
         }
 
         private void HandleWindowEvent(SDL_Event evnt)
@@ -190,8 +211,8 @@ namespace Ryujinx.Headless.SDL2
                     case SDL_WindowEventID.SDL_WINDOWEVENT_SIZE_CHANGED:
                         // Unlike on Windows, this event fires on macOS when triggering fullscreen mode.
                         // And promptly crashes the process because `Renderer?.window.SetSize` is undefined.
-                        // As we don't need this to fire in either case we can test for isFullscreen.
-                        if (!IsFullscreen)
+                        // As we don't need this to fire in either case we can test for fullscreen.
+                        if (!IsFullscreen && !IsExclusiveFullscreen)
                         {
                             Width = evnt.window.data1;
                             Height = evnt.window.data2;
@@ -226,6 +247,17 @@ namespace Ryujinx.Headless.SDL2
             return Renderer.GetHardwareInfo().GpuVendor;
         }
 
+        private void SetAntiAliasing()
+        {
+            Renderer?.Window.SetAntiAliasing((Graphics.GAL.AntiAliasing)AntiAliasing);
+        }
+
+        private void SetScalingFilter()
+        {
+            Renderer?.Window.SetScalingFilter((Graphics.GAL.ScalingFilter)ScalingFilter);
+            Renderer?.Window.SetScalingFilterLevel(ScalingFilterLevel);
+        }
+
         public void Render()
         {
             InitializeWindowRenderer();
@@ -234,6 +266,10 @@ namespace Ryujinx.Headless.SDL2
 
             InitializeRenderer();
 
+            SetAntiAliasing();
+
+            SetScalingFilter();
+
             _gpuVendorName = GetGpuVendorName();
 
             Device.Gpu.Renderer.RunLoop(() =>
diff --git a/src/Ryujinx.Horizon.Generators/Hipc/HipcGenerator.cs b/src/Ryujinx.Horizon.Generators/Hipc/HipcGenerator.cs
index 1b92e9180..4e14f47e9 100644
--- a/src/Ryujinx.Horizon.Generators/Hipc/HipcGenerator.cs
+++ b/src/Ryujinx.Horizon.Generators/Hipc/HipcGenerator.cs
@@ -93,7 +93,7 @@ namespace Ryujinx.Horizon.Generators.Hipc
                 generator.LeaveScope();
                 generator.LeaveScope();
 
-                context.AddSource($"{className}.g.cs", generator.ToString());
+                context.AddSource($"{GetNamespaceName(commandInterface.ClassDeclarationSyntax)}.{className}.g.cs", generator.ToString());
             }
         }
 
diff --git a/src/Ryujinx.Horizon/Bcat/BcatIpcServer.cs b/src/Ryujinx.Horizon/Bcat/BcatIpcServer.cs
index f39929ddc..5d01cd9de 100644
--- a/src/Ryujinx.Horizon/Bcat/BcatIpcServer.cs
+++ b/src/Ryujinx.Horizon/Bcat/BcatIpcServer.cs
@@ -6,8 +6,8 @@ namespace Ryujinx.Horizon.Bcat
 {
     internal class BcatIpcServer
     {
-        private const int BcatMaxSessionsCount = 8;
-        private const int BcatTotalMaxSessionsCount = BcatMaxSessionsCount * 4;
+        private const int MaxSessionsCount = 8;
+        private const int TotalMaxSessionsCount = MaxSessionsCount * 4;
 
         private const int PointerBufferSize = 0x400;
         private const int MaxDomains = 64;
@@ -17,7 +17,7 @@ namespace Ryujinx.Horizon.Bcat
         private SmApi _sm;
         private BcatServerManager _serverManager;
 
-        private static readonly ManagerOptions _bcatManagerOptions = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
+        private static readonly ManagerOptions _managerOptions = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
 
         internal void Initialize()
         {
@@ -26,13 +26,13 @@ namespace Ryujinx.Horizon.Bcat
             _sm = new SmApi();
             _sm.Initialize().AbortOnFailure();
 
-            _serverManager = new BcatServerManager(allocator, _sm, MaxPortsCount, _bcatManagerOptions, BcatTotalMaxSessionsCount);
+            _serverManager = new BcatServerManager(allocator, _sm, MaxPortsCount, _managerOptions, TotalMaxSessionsCount);
 
 #pragma warning disable IDE0055 // Disable formatting
-            _serverManager.RegisterServer((int)BcatPortIndex.Admin,   ServiceName.Encode("bcat:a"), BcatMaxSessionsCount);
-            _serverManager.RegisterServer((int)BcatPortIndex.Manager, ServiceName.Encode("bcat:m"), BcatMaxSessionsCount);
-            _serverManager.RegisterServer((int)BcatPortIndex.User,    ServiceName.Encode("bcat:u"), BcatMaxSessionsCount);
-            _serverManager.RegisterServer((int)BcatPortIndex.System,  ServiceName.Encode("bcat:s"), BcatMaxSessionsCount);
+            _serverManager.RegisterServer((int)BcatPortIndex.Admin,   ServiceName.Encode("bcat:a"), MaxSessionsCount);
+            _serverManager.RegisterServer((int)BcatPortIndex.Manager, ServiceName.Encode("bcat:m"), MaxSessionsCount);
+            _serverManager.RegisterServer((int)BcatPortIndex.User,    ServiceName.Encode("bcat:u"), MaxSessionsCount);
+            _serverManager.RegisterServer((int)BcatPortIndex.System,  ServiceName.Encode("bcat:s"), MaxSessionsCount);
 #pragma warning restore IDE0055
         }
 
diff --git a/src/Ryujinx.Horizon/HorizonOptions.cs b/src/Ryujinx.Horizon/HorizonOptions.cs
index 75cc29b72..e3c862da4 100644
--- a/src/Ryujinx.Horizon/HorizonOptions.cs
+++ b/src/Ryujinx.Horizon/HorizonOptions.cs
@@ -1,4 +1,5 @@
 using LibHac;
+using Ryujinx.Horizon.Sdk.Fs;
 
 namespace Ryujinx.Horizon
 {
@@ -8,12 +9,14 @@ namespace Ryujinx.Horizon
         public bool ThrowOnInvalidCommandIds { get; }
 
         public HorizonClient BcatClient { get; }
+        public IFsClient FsClient { get; }
 
-        public HorizonOptions(bool ignoreMissingServices, HorizonClient bcatClient)
+        public HorizonOptions(bool ignoreMissingServices, HorizonClient bcatClient, IFsClient fsClient)
         {
             IgnoreMissingServices = ignoreMissingServices;
             ThrowOnInvalidCommandIds = true;
             BcatClient = bcatClient;
+            FsClient = fsClient;
         }
     }
 }
diff --git a/src/Ryujinx.Horizon/HorizonStatic.cs b/src/Ryujinx.Horizon/HorizonStatic.cs
index 1e483cd44..3e992eadb 100644
--- a/src/Ryujinx.Horizon/HorizonStatic.cs
+++ b/src/Ryujinx.Horizon/HorizonStatic.cs
@@ -4,7 +4,7 @@ using System;
 
 namespace Ryujinx.Horizon
 {
-    static class HorizonStatic
+    public static class HorizonStatic
     {
         [ThreadStatic]
         private static HorizonOptions _options;
diff --git a/src/Ryujinx.Horizon/Hshl/HshlIpcServer.cs b/src/Ryujinx.Horizon/Hshl/HshlIpcServer.cs
new file mode 100644
index 000000000..7182725cb
--- /dev/null
+++ b/src/Ryujinx.Horizon/Hshl/HshlIpcServer.cs
@@ -0,0 +1,47 @@
+using Ryujinx.Horizon.Hshl.Ipc;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using Ryujinx.Horizon.Sdk.Sm;
+
+namespace Ryujinx.Horizon.Hshl
+{
+    class HshlIpcServer
+    {
+        private const int HshlMaxSessionsCount = 10;
+        private const int TotalMaxSessionsCount = HshlMaxSessionsCount * 2;
+
+        private const int PointerBufferSize = 0;
+        private const int MaxDomains = 0;
+        private const int MaxDomainObjects = 0;
+        private const int MaxPortsCount = 2;
+
+        private static readonly ManagerOptions _options = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
+
+        private SmApi _sm;
+        private ServerManager _serverManager;
+
+        public void Initialize()
+        {
+            HeapAllocator allocator = new();
+
+            _sm = new SmApi();
+            _sm.Initialize().AbortOnFailure();
+
+            _serverManager = new ServerManager(allocator, _sm, MaxPortsCount, _options, TotalMaxSessionsCount);
+
+#pragma warning disable IDE0055 // Disable formatting
+            _serverManager.RegisterObjectForServer(new SetterManager(), ServiceName.Encode("hshl:set"), HshlMaxSessionsCount); // 11.0.0+
+            _serverManager.RegisterObjectForServer(new Manager(),       ServiceName.Encode("hshl:sys"), HshlMaxSessionsCount); // 11.0.0+
+#pragma warning restore IDE0055
+        }
+
+        public void ServiceRequests()
+        {
+            _serverManager.ServiceRequests();
+        }
+
+        public void Shutdown()
+        {
+            _serverManager.Dispose();
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Hshl/HshlMain.cs b/src/Ryujinx.Horizon/Hshl/HshlMain.cs
new file mode 100644
index 000000000..4e894b6f6
--- /dev/null
+++ b/src/Ryujinx.Horizon/Hshl/HshlMain.cs
@@ -0,0 +1,17 @@
+namespace Ryujinx.Horizon.Hshl
+{
+    class HshlMain : IService
+    {
+        public static void Main(ServiceTable serviceTable)
+        {
+            HshlIpcServer ipcServer = new();
+
+            ipcServer.Initialize();
+
+            serviceTable.SignalServiceReady();
+
+            ipcServer.ServiceRequests();
+            ipcServer.Shutdown();
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Hshl/Ipc/Manager.cs b/src/Ryujinx.Horizon/Hshl/Ipc/Manager.cs
new file mode 100644
index 000000000..29d9069ac
--- /dev/null
+++ b/src/Ryujinx.Horizon/Hshl/Ipc/Manager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Hshl;
+
+namespace Ryujinx.Horizon.Hshl.Ipc
+{
+    partial class Manager : IManager
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Hshl/Ipc/SetterManager.cs b/src/Ryujinx.Horizon/Hshl/Ipc/SetterManager.cs
new file mode 100644
index 000000000..ac1006f0f
--- /dev/null
+++ b/src/Ryujinx.Horizon/Hshl/Ipc/SetterManager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Hshl;
+
+namespace Ryujinx.Horizon.Hshl.Ipc
+{
+    partial class SetterManager : ISetterManager
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Ins/InsIpcServer.cs b/src/Ryujinx.Horizon/Ins/InsIpcServer.cs
new file mode 100644
index 000000000..68698bf6a
--- /dev/null
+++ b/src/Ryujinx.Horizon/Ins/InsIpcServer.cs
@@ -0,0 +1,47 @@
+using Ryujinx.Horizon.Ins.Ipc;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using Ryujinx.Horizon.Sdk.Sm;
+
+namespace Ryujinx.Horizon.Ins
+{
+    class InsIpcServer
+    {
+        private const int InsMaxSessionsCount = 8;
+        private const int TotalMaxSessionsCount = InsMaxSessionsCount * 2;
+
+        private const int PointerBufferSize = 0x200;
+        private const int MaxDomains = 0;
+        private const int MaxDomainObjects = 0;
+        private const int MaxPortsCount = 2;
+
+        private static readonly ManagerOptions _options = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
+
+        private SmApi _sm;
+        private ServerManager _serverManager;
+
+        public void Initialize()
+        {
+            HeapAllocator allocator = new();
+
+            _sm = new SmApi();
+            _sm.Initialize().AbortOnFailure();
+
+            _serverManager = new ServerManager(allocator, _sm, MaxPortsCount, _options, TotalMaxSessionsCount);
+
+#pragma warning disable IDE0055 // Disable formatting
+            _serverManager.RegisterObjectForServer(new ReceiverManager(), ServiceName.Encode("ins:r"), InsMaxSessionsCount); // 9.0.0+
+            _serverManager.RegisterObjectForServer(new SenderManager(),   ServiceName.Encode("ins:s"), InsMaxSessionsCount); // 9.0.0+
+#pragma warning restore IDE0055
+        }
+
+        public void ServiceRequests()
+        {
+            _serverManager.ServiceRequests();
+        }
+
+        public void Shutdown()
+        {
+            _serverManager.Dispose();
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Ins/InsMain.cs b/src/Ryujinx.Horizon/Ins/InsMain.cs
new file mode 100644
index 000000000..e428d090a
--- /dev/null
+++ b/src/Ryujinx.Horizon/Ins/InsMain.cs
@@ -0,0 +1,17 @@
+namespace Ryujinx.Horizon.Ins
+{
+    class InsMain : IService
+    {
+        public static void Main(ServiceTable serviceTable)
+        {
+            InsIpcServer ipcServer = new();
+
+            ipcServer.Initialize();
+
+            serviceTable.SignalServiceReady();
+
+            ipcServer.ServiceRequests();
+            ipcServer.Shutdown();
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Ins/Ipc/ReceiverManager.cs b/src/Ryujinx.Horizon/Ins/Ipc/ReceiverManager.cs
new file mode 100644
index 000000000..6e9b29a99
--- /dev/null
+++ b/src/Ryujinx.Horizon/Ins/Ipc/ReceiverManager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Ins;
+
+namespace Ryujinx.Horizon.Ins.Ipc
+{
+    partial class ReceiverManager : IReceiverManager
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Ins/Ipc/SenderManager.cs b/src/Ryujinx.Horizon/Ins/Ipc/SenderManager.cs
new file mode 100644
index 000000000..e133014e1
--- /dev/null
+++ b/src/Ryujinx.Horizon/Ins/Ipc/SenderManager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Ins;
+
+namespace Ryujinx.Horizon.Ins.Ipc
+{
+    partial class SenderManager : ISenderManager
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Lbl/Ipc/LblController.cs b/src/Ryujinx.Horizon/Lbl/Ipc/LblController.cs
new file mode 100644
index 000000000..0a27d5efa
--- /dev/null
+++ b/src/Ryujinx.Horizon/Lbl/Ipc/LblController.cs
@@ -0,0 +1,130 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Lbl;
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Lbl.Ipc
+{
+    partial class LblController : ILblController
+    {
+        private bool _vrModeEnabled;
+        private float _currentBrightnessSettingForVrMode;
+
+        [CmifCommand(17)]
+        public Result SetBrightnessReflectionDelayLevel(float unknown0, float unknown1)
+        {
+            // NOTE: Stubbed in system module.
+
+            return Result.Success;
+        }
+
+        [CmifCommand(18)]
+        public Result GetBrightnessReflectionDelayLevel(out float unknown1, float unknown0)
+        {
+            // NOTE: Stubbed in system module.
+
+            unknown1 = 0.0f;
+
+            return Result.Success;
+        }
+
+        [CmifCommand(19)]
+        public Result SetCurrentBrightnessMapping(float unknown0, float unknown1, float unknown2)
+        {
+            // NOTE: Stubbed in system module.
+
+            return Result.Success;
+        }
+
+        [CmifCommand(20)]
+        public Result GetCurrentBrightnessMapping(out float unknown0, out float unknown1, out float unknown2)
+        {
+            // NOTE: Stubbed in system module.
+
+            unknown0 = 0.0f;
+            unknown1 = 0.0f;
+            unknown2 = 0.0f;
+
+            return Result.Success;
+        }
+
+        [CmifCommand(21)]
+        public Result SetCurrentAmbientLightSensorMapping(float unknown0, float unknown1, float unknown2)
+        {
+            // NOTE: Stubbed in system module.
+
+            return Result.Success;
+        }
+
+        [CmifCommand(22)]
+        public Result GetCurrentAmbientLightSensorMapping(out float unknown0, out float unknown1, out float unknown2)
+        {
+            // NOTE: Stubbed in system module.
+
+            unknown0 = 0.0f;
+            unknown1 = 0.0f;
+            unknown2 = 0.0f;
+
+            return Result.Success;
+        }
+
+        [CmifCommand(24)]
+        public Result SetCurrentBrightnessSettingForVrMode(float currentBrightnessSettingForVrMode)
+        {
+            if (float.IsNaN(currentBrightnessSettingForVrMode) || float.IsInfinity(currentBrightnessSettingForVrMode))
+            {
+                _currentBrightnessSettingForVrMode = 0.0f;
+            }
+            else
+            {
+                _currentBrightnessSettingForVrMode = currentBrightnessSettingForVrMode;
+            }
+
+            return Result.Success;
+        }
+
+        [CmifCommand(25)]
+        public Result GetCurrentBrightnessSettingForVrMode(out float currentBrightnessSettingForVrMode)
+        {
+            if (float.IsNaN(_currentBrightnessSettingForVrMode) || float.IsInfinity(_currentBrightnessSettingForVrMode))
+            {
+                currentBrightnessSettingForVrMode = 0.0f;
+            }
+            else
+            {
+                currentBrightnessSettingForVrMode = _currentBrightnessSettingForVrMode;
+            }
+
+            return Result.Success;
+        }
+
+        [CmifCommand(26)]
+        public Result EnableVrMode()
+        {
+            _vrModeEnabled = true;
+
+            // NOTE: The service checks _vrModeEnabled field value in a thread and then changes the screen brightness.
+            //       Since we don't support that, it's fine to do nothing.
+
+            return Result.Success;
+        }
+
+        [CmifCommand(27)]
+        public Result DisableVrMode()
+        {
+            _vrModeEnabled = false;
+
+            // NOTE: The service checks _vrModeEnabled field value in a thread and then changes the screen brightness.
+            //       Since we don't support that, it's fine to do nothing.
+
+            return Result.Success;
+        }
+
+        [CmifCommand(28)]
+        public Result IsVrModeEnabled(out bool vrModeEnabled)
+        {
+            vrModeEnabled = _vrModeEnabled;
+
+            return Result.Success;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Lbl/LblIpcServer.cs b/src/Ryujinx.Horizon/Lbl/LblIpcServer.cs
new file mode 100644
index 000000000..53e74d515
--- /dev/null
+++ b/src/Ryujinx.Horizon/Lbl/LblIpcServer.cs
@@ -0,0 +1,43 @@
+using Ryujinx.Horizon.Lbl.Ipc;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using Ryujinx.Horizon.Sdk.Sm;
+
+namespace Ryujinx.Horizon.Lbl
+{
+    class LblIpcServer
+    {
+        private const int MaxSessionsCount = 5;
+
+        private const int PointerBufferSize = 0;
+        private const int MaxDomains = 0;
+        private const int MaxDomainObjects = 0;
+        private const int MaxPortsCount = 1;
+
+        private static readonly ManagerOptions _managerOptions = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
+
+        private SmApi _sm;
+        private ServerManager _serverManager;
+
+        public void Initialize()
+        {
+            HeapAllocator allocator = new();
+
+            _sm = new SmApi();
+            _sm.Initialize().AbortOnFailure();
+
+            _serverManager = new ServerManager(allocator, _sm, MaxPortsCount, _managerOptions, MaxSessionsCount);
+
+            _serverManager.RegisterObjectForServer(new LblController(), ServiceName.Encode("lbl"), MaxSessionsCount);
+        }
+
+        public void ServiceRequests()
+        {
+            _serverManager.ServiceRequests();
+        }
+
+        public void Shutdown()
+        {
+            _serverManager.Dispose();
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Lbl/LblMain.cs b/src/Ryujinx.Horizon/Lbl/LblMain.cs
new file mode 100644
index 000000000..f471f31b7
--- /dev/null
+++ b/src/Ryujinx.Horizon/Lbl/LblMain.cs
@@ -0,0 +1,17 @@
+namespace Ryujinx.Horizon.Lbl
+{
+    class LblMain : IService
+    {
+        public static void Main(ServiceTable serviceTable)
+        {
+            LblIpcServer ipcServer = new();
+
+            ipcServer.Initialize();
+
+            serviceTable.SignalServiceReady();
+
+            ipcServer.ServiceRequests();
+            ipcServer.Shutdown();
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/LibHacResultExtensions.cs b/src/Ryujinx.Horizon/LibHacResultExtensions.cs
index 01d181640..8c994f149 100644
--- a/src/Ryujinx.Horizon/LibHacResultExtensions.cs
+++ b/src/Ryujinx.Horizon/LibHacResultExtensions.cs
@@ -2,7 +2,7 @@
 
 namespace Ryujinx.Horizon
 {
-    internal static class LibHacResultExtensions
+    public static class LibHacResultExtensions
     {
         public static Result ToHorizonResult(this LibHac.Result result)
         {
diff --git a/src/Ryujinx.Horizon/LogManager/Ipc/LogService.cs b/src/Ryujinx.Horizon/LogManager/Ipc/LogService.cs
index 9ac9c27e6..c266d0e94 100644
--- a/src/Ryujinx.Horizon/LogManager/Ipc/LogService.cs
+++ b/src/Ryujinx.Horizon/LogManager/Ipc/LogService.cs
@@ -11,7 +11,7 @@ namespace Ryujinx.Horizon.LogManager.Ipc
         [CmifCommand(0)]
         public Result OpenLogger(out LmLogger logger, [ClientProcessId] ulong pid)
         {
-            // NOTE: Internal name is Logger, but we rename it LmLogger to avoid name clash with Ryujinx.Common.Logging logger.
+            // NOTE: Internal name is Logger, but we rename it to LmLogger to avoid name clash with Ryujinx.Common.Logging logger.
             logger = new LmLogger(this, pid);
 
             return Result.Success;
diff --git a/src/Ryujinx.Horizon/LogManager/LmIpcServer.cs b/src/Ryujinx.Horizon/LogManager/LmIpcServer.cs
index d1a405b87..6bdc3c429 100644
--- a/src/Ryujinx.Horizon/LogManager/LmIpcServer.cs
+++ b/src/Ryujinx.Horizon/LogManager/LmIpcServer.cs
@@ -6,14 +6,14 @@ namespace Ryujinx.Horizon.LogManager
 {
     class LmIpcServer
     {
-        private const int LogMaxSessionsCount = 42;
+        private const int MaxSessionsCount = 42;
 
         private const int PointerBufferSize = 0x400;
         private const int MaxDomains = 31;
         private const int MaxDomainObjects = 61;
         private const int MaxPortsCount = 1;
 
-        private static readonly ManagerOptions _logManagerOptions = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
+        private static readonly ManagerOptions _managerOptions = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
 
         private SmApi _sm;
         private ServerManager _serverManager;
@@ -25,9 +25,9 @@ namespace Ryujinx.Horizon.LogManager
             _sm = new SmApi();
             _sm.Initialize().AbortOnFailure();
 
-            _serverManager = new ServerManager(allocator, _sm, MaxPortsCount, _logManagerOptions, LogMaxSessionsCount);
+            _serverManager = new ServerManager(allocator, _sm, MaxPortsCount, _managerOptions, MaxSessionsCount);
 
-            _serverManager.RegisterObjectForServer(new LogService(), ServiceName.Encode("lm"), LogMaxSessionsCount);
+            _serverManager.RegisterObjectForServer(new LogService(), ServiceName.Encode("lm"), MaxSessionsCount);
         }
 
         public void ServiceRequests()
diff --git a/src/Ryujinx.Horizon/MmNv/Ipc/Request.cs b/src/Ryujinx.Horizon/MmNv/Ipc/Request.cs
new file mode 100644
index 000000000..9a24c75e8
--- /dev/null
+++ b/src/Ryujinx.Horizon/MmNv/Ipc/Request.cs
@@ -0,0 +1,160 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.MmNv;
+using Ryujinx.Horizon.Sdk.Sf;
+using System.Collections.Generic;
+
+namespace Ryujinx.Horizon.MmNv.Ipc
+{
+    partial class Request : IRequest
+    {
+        private readonly List<Session> _sessionList = new();
+
+        private uint _uniqueId = 1;
+
+        [CmifCommand(0)]
+        public Result InitializeOld(Module module, uint fgmPriority, uint autoClearEvent)
+        {
+            bool isAutoClearEvent = autoClearEvent != 0;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceMm, new { module, fgmPriority, isAutoClearEvent });
+
+            Register(module, fgmPriority, isAutoClearEvent);
+
+            return Result.Success;
+        }
+
+        [CmifCommand(1)]
+        public Result FinalizeOld(Module module)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceMm, new { module });
+
+            lock (_sessionList)
+            {
+                _sessionList.Remove(GetSessionByModule(module));
+            }
+
+            return Result.Success;
+        }
+
+        [CmifCommand(2)]
+        public Result SetAndWaitOld(Module module, uint clockRateMin, int clockRateMax)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceMm, new { module, clockRateMin, clockRateMax });
+
+            lock (_sessionList)
+            {
+                GetSessionByModule(module)?.SetAndWait(clockRateMin, clockRateMax);
+            }
+
+            return Result.Success;
+        }
+
+        [CmifCommand(3)]
+        public Result GetOld(out uint clockRateActual, Module module)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceMm, new { module });
+
+            lock (_sessionList)
+            {
+                Session session = GetSessionByModule(module);
+
+                clockRateActual = session == null ? 0 : session.ClockRateMin;
+            }
+
+            return Result.Success;
+        }
+
+        [CmifCommand(4)]
+        public Result Initialize(out uint requestId, Module module, uint fgmPriority, uint autoClearEvent)
+        {
+            bool isAutoClearEvent = autoClearEvent != 0;
+
+            Logger.Stub?.PrintStub(LogClass.ServiceMm, new { module, fgmPriority, isAutoClearEvent });
+
+            requestId = Register(module, fgmPriority, isAutoClearEvent);
+
+            return Result.Success;
+        }
+
+        [CmifCommand(5)]
+        public Result Finalize(uint requestId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceMm, new { requestId });
+
+            lock (_sessionList)
+            {
+                _sessionList.Remove(GetSessionById(requestId));
+            }
+
+            return Result.Success;
+        }
+
+        [CmifCommand(6)]
+        public Result SetAndWait(uint requestId, uint clockRateMin, int clockRateMax)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceMm, new { requestId, clockRateMin, clockRateMax });
+
+            lock (_sessionList)
+            {
+                GetSessionById(requestId)?.SetAndWait(clockRateMin, clockRateMax);
+            }
+
+            return Result.Success;
+        }
+
+        [CmifCommand(7)]
+        public Result Get(out uint clockRateActual, uint requestId)
+        {
+            Logger.Stub?.PrintStub(LogClass.ServiceMm, new { requestId });
+
+            lock (_sessionList)
+            {
+                Session session = GetSessionById(requestId);
+
+                clockRateActual = session == null ? 0 : session.ClockRateMin;
+            }
+
+            return Result.Success;
+        }
+
+        private Session GetSessionById(uint id)
+        {
+            foreach (Session session in _sessionList)
+            {
+                if (session.Id == id)
+                {
+                    return session;
+                }
+            }
+
+            return null;
+        }
+
+        private Session GetSessionByModule(Module module)
+        {
+            foreach (Session session in _sessionList)
+            {
+                if (session.Module == module)
+                {
+                    return session;
+                }
+            }
+
+            return null;
+        }
+
+        private uint Register(Module module, uint fgmPriority, bool isAutoClearEvent)
+        {
+            lock (_sessionList)
+            {
+                // Nintendo ignores the fgm priority as the other services were deprecated.
+                Session session = new(_uniqueId++, module, isAutoClearEvent);
+
+                _sessionList.Add(session);
+
+                return session.Id;
+            }
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/MmNv/MmNvIpcServer.cs b/src/Ryujinx.Horizon/MmNv/MmNvIpcServer.cs
new file mode 100644
index 000000000..b6615d2cd
--- /dev/null
+++ b/src/Ryujinx.Horizon/MmNv/MmNvIpcServer.cs
@@ -0,0 +1,43 @@
+using Ryujinx.Horizon.MmNv.Ipc;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using Ryujinx.Horizon.Sdk.Sm;
+
+namespace Ryujinx.Horizon.MmNv
+{
+    class MmNvIpcServer
+    {
+        private const int MaxSessionsCount = 40;
+
+        private const int PointerBufferSize = 0;
+        private const int MaxDomains = 0;
+        private const int MaxDomainObjects = 0;
+        private const int MaxPortsCount = 1;
+
+        private static readonly ManagerOptions _managerOptions = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
+
+        private SmApi _sm;
+        private ServerManager _serverManager;
+
+        public void Initialize()
+        {
+            HeapAllocator allocator = new();
+
+            _sm = new SmApi();
+            _sm.Initialize().AbortOnFailure();
+
+            _serverManager = new ServerManager(allocator, _sm, MaxPortsCount, _managerOptions, MaxSessionsCount);
+
+            _serverManager.RegisterObjectForServer(new Request(), ServiceName.Encode("mm:u"), MaxSessionsCount);
+        }
+
+        public void ServiceRequests()
+        {
+            _serverManager.ServiceRequests();
+        }
+
+        public void Shutdown()
+        {
+            _serverManager.Dispose();
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/MmNv/MmNvMain.cs b/src/Ryujinx.Horizon/MmNv/MmNvMain.cs
new file mode 100644
index 000000000..ac5eff1a9
--- /dev/null
+++ b/src/Ryujinx.Horizon/MmNv/MmNvMain.cs
@@ -0,0 +1,17 @@
+namespace Ryujinx.Horizon.MmNv
+{
+    class MmNvMain : IService
+    {
+        public static void Main(ServiceTable serviceTable)
+        {
+            MmNvIpcServer ipcServer = new();
+
+            ipcServer.Initialize();
+
+            serviceTable.SignalServiceReady();
+
+            ipcServer.ServiceRequests();
+            ipcServer.Shutdown();
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Ngc/Ipc/Service.cs b/src/Ryujinx.Horizon/Ngc/Ipc/Service.cs
new file mode 100644
index 000000000..828c09199
--- /dev/null
+++ b/src/Ryujinx.Horizon/Ngc/Ipc/Service.cs
@@ -0,0 +1,64 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Ngc;
+using Ryujinx.Horizon.Sdk.Ngc.Detail;
+using Ryujinx.Horizon.Sdk.Sf;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using System;
+
+namespace Ryujinx.Horizon.Ngc.Ipc
+{
+    partial class Service : INgcService
+    {
+        private readonly ProfanityFilter _profanityFilter;
+
+        public Service(ProfanityFilter profanityFilter)
+        {
+            _profanityFilter = profanityFilter;
+        }
+
+        [CmifCommand(0)]
+        public Result GetContentVersion(out uint version)
+        {
+            lock (_profanityFilter)
+            {
+                return _profanityFilter.GetContentVersion(out version);
+            }
+        }
+
+        [CmifCommand(1)]
+        public Result Check(out uint checkMask, ReadOnlySpan<byte> text, uint regionMask, ProfanityFilterOption option)
+        {
+            lock (_profanityFilter)
+            {
+                return _profanityFilter.CheckProfanityWords(out checkMask, text, regionMask, option);
+            }
+        }
+
+        [CmifCommand(2)]
+        public Result Mask(
+            out int maskedWordsCount,
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<byte> filteredText,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> text,
+            uint regionMask,
+            ProfanityFilterOption option)
+        {
+            lock (_profanityFilter)
+            {
+                int length = Math.Min(filteredText.Length, text.Length);
+
+                text[..length].CopyTo(filteredText[..length]);
+
+                return _profanityFilter.MaskProfanityWordsInText(out maskedWordsCount, filteredText, regionMask, option);
+            }
+        }
+
+        [CmifCommand(3)]
+        public Result Reload()
+        {
+            lock (_profanityFilter)
+            {
+                return _profanityFilter.Reload();
+            }
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Ngc/NgcIpcServer.cs b/src/Ryujinx.Horizon/Ngc/NgcIpcServer.cs
new file mode 100644
index 000000000..b2a74fb22
--- /dev/null
+++ b/src/Ryujinx.Horizon/Ngc/NgcIpcServer.cs
@@ -0,0 +1,51 @@
+using Ryujinx.Horizon.Ngc.Ipc;
+using Ryujinx.Horizon.Sdk.Fs;
+using Ryujinx.Horizon.Sdk.Ngc.Detail;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using Ryujinx.Horizon.Sdk.Sm;
+using System;
+
+namespace Ryujinx.Horizon.Ngc
+{
+    class NgcIpcServer
+    {
+        private const int MaxSessionsCount = 4;
+
+        private const int PointerBufferSize = 0;
+        private const int MaxDomains = 0;
+        private const int MaxDomainObjects = 0;
+        private const int MaxPortsCount = 1;
+
+        private static readonly ManagerOptions _options = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
+
+        private SmApi _sm;
+        private ServerManager _serverManager;
+        private ProfanityFilter _profanityFilter;
+
+        public void Initialize(IFsClient fsClient)
+        {
+            HeapAllocator allocator = new();
+
+            _sm = new SmApi();
+            _sm.Initialize().AbortOnFailure();
+
+            _profanityFilter = new(fsClient);
+            _profanityFilter.Initialize().AbortOnFailure();
+
+            _serverManager = new ServerManager(allocator, _sm, MaxPortsCount, _options, MaxSessionsCount);
+
+            _serverManager.RegisterObjectForServer(new Service(_profanityFilter), ServiceName.Encode("ngc:u"), MaxSessionsCount);
+        }
+
+        public void ServiceRequests()
+        {
+            _serverManager.ServiceRequests();
+        }
+
+        public void Shutdown()
+        {
+            _serverManager.Dispose();
+            _profanityFilter.Dispose();
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Ngc/NgcMain.cs b/src/Ryujinx.Horizon/Ngc/NgcMain.cs
new file mode 100644
index 000000000..1a584d665
--- /dev/null
+++ b/src/Ryujinx.Horizon/Ngc/NgcMain.cs
@@ -0,0 +1,21 @@
+namespace Ryujinx.Horizon.Ngc
+{
+    class NgcMain : IService
+    {
+        public static void Main(ServiceTable serviceTable)
+        {
+            NgcIpcServer ipcServer = new();
+
+            ipcServer.Initialize(HorizonStatic.Options.FsClient);
+
+            // TODO: Notification thread, requires implementing OpenSystemDataUpdateEventNotifier on FS.
+            // The notification thread seems to wait until the event returned by OpenSystemDataUpdateEventNotifier is signalled
+            // in a loop. When it receives the signal, it calls ContentsReader.Reload and then waits for the next signal.
+
+            serviceTable.SignalServiceReady();
+
+            ipcServer.ServiceRequests();
+            ipcServer.Shutdown();
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Ovln/Ipc/ReceiverService.cs b/src/Ryujinx.Horizon/Ovln/Ipc/ReceiverService.cs
new file mode 100644
index 000000000..6cc448e8a
--- /dev/null
+++ b/src/Ryujinx.Horizon/Ovln/Ipc/ReceiverService.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Ovln;
+
+namespace Ryujinx.Horizon.Ovln.Ipc
+{
+    partial class ReceiverService : IReceiverService
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Ovln/Ipc/SenderService.cs b/src/Ryujinx.Horizon/Ovln/Ipc/SenderService.cs
new file mode 100644
index 000000000..cab123ecf
--- /dev/null
+++ b/src/Ryujinx.Horizon/Ovln/Ipc/SenderService.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Ovln;
+
+namespace Ryujinx.Horizon.Ovln.Ipc
+{
+    partial class SenderService : ISenderService
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Ovln/OvlnIpcServer.cs b/src/Ryujinx.Horizon/Ovln/OvlnIpcServer.cs
new file mode 100644
index 000000000..2c00107fb
--- /dev/null
+++ b/src/Ryujinx.Horizon/Ovln/OvlnIpcServer.cs
@@ -0,0 +1,48 @@
+using Ryujinx.Horizon.Ovln.Ipc;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using Ryujinx.Horizon.Sdk.Sm;
+
+namespace Ryujinx.Horizon.Ovln
+{
+    class OvlnIpcServer
+    {
+        private const int OvlnRcvMaxSessionsCount = 2;
+        private const int OvlnSndMaxSessionsCount = 20;
+        private const int TotalMaxSessionsCount = OvlnRcvMaxSessionsCount + OvlnSndMaxSessionsCount;
+
+        private const int PointerBufferSize = 0;
+        private const int MaxDomains = 21;
+        private const int MaxDomainObjects = 60;
+        private const int MaxPortsCount = 2;
+
+        private static readonly ManagerOptions _options = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
+
+        private SmApi _sm;
+        private ServerManager _serverManager;
+
+        public void Initialize()
+        {
+            HeapAllocator allocator = new();
+
+            _sm = new SmApi();
+            _sm.Initialize().AbortOnFailure();
+
+            _serverManager = new ServerManager(allocator, _sm, MaxPortsCount, _options, TotalMaxSessionsCount);
+
+#pragma warning disable IDE0055 // Disable formatting
+            _serverManager.RegisterObjectForServer(new ReceiverService(), ServiceName.Encode("ovln:rcv"), OvlnRcvMaxSessionsCount); // 8.0.0+
+            _serverManager.RegisterObjectForServer(new SenderService(),   ServiceName.Encode("ovln:snd"), OvlnSndMaxSessionsCount); // 8.0.0+
+#pragma warning restore IDE0055
+        }
+
+        public void ServiceRequests()
+        {
+            _serverManager.ServiceRequests();
+        }
+
+        public void Shutdown()
+        {
+            _serverManager.Dispose();
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Ovln/OvlnMain.cs b/src/Ryujinx.Horizon/Ovln/OvlnMain.cs
new file mode 100644
index 000000000..8c6cf84e7
--- /dev/null
+++ b/src/Ryujinx.Horizon/Ovln/OvlnMain.cs
@@ -0,0 +1,17 @@
+namespace Ryujinx.Horizon.Ovln
+{
+    class OvlnMain : IService
+    {
+        public static void Main(ServiceTable serviceTable)
+        {
+            OvlnIpcServer ipcServer = new();
+
+            ipcServer.Initialize();
+
+            serviceTable.SignalServiceReady();
+
+            ipcServer.ServiceRequests();
+            ipcServer.Shutdown();
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Prepo/PrepoIpcServer.cs b/src/Ryujinx.Horizon/Prepo/PrepoIpcServer.cs
index 9c185520d..410f997e7 100644
--- a/src/Ryujinx.Horizon/Prepo/PrepoIpcServer.cs
+++ b/src/Ryujinx.Horizon/Prepo/PrepoIpcServer.cs
@@ -6,15 +6,15 @@ namespace Ryujinx.Horizon.Prepo
 {
     class PrepoIpcServer
     {
-        private const int PrepoMaxSessionsCount = 12;
-        private const int PrepoTotalMaxSessionsCount = PrepoMaxSessionsCount * 6;
+        private const int MaxSessionsCount = 12;
+        private const int TotalMaxSessionsCount = MaxSessionsCount * 6;
 
         private const int PointerBufferSize = 0x80;
         private const int MaxDomains = 64;
         private const int MaxDomainObjects = 16;
         private const int MaxPortsCount = 6;
 
-        private static readonly ManagerOptions _prepoManagerOptions = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
+        private static readonly ManagerOptions _managerOptions = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
 
         private SmApi _sm;
         private PrepoServerManager _serverManager;
@@ -26,15 +26,15 @@ namespace Ryujinx.Horizon.Prepo
             _sm = new SmApi();
             _sm.Initialize().AbortOnFailure();
 
-            _serverManager = new PrepoServerManager(allocator, _sm, MaxPortsCount, _prepoManagerOptions, PrepoTotalMaxSessionsCount);
+            _serverManager = new PrepoServerManager(allocator, _sm, MaxPortsCount, _managerOptions, TotalMaxSessionsCount);
 
 #pragma warning disable IDE0055 // Disable formatting
-            _serverManager.RegisterServer((int)PrepoPortIndex.Admin,   ServiceName.Encode("prepo:a"),  PrepoMaxSessionsCount); // 1.0.0-5.1.0
-            _serverManager.RegisterServer((int)PrepoPortIndex.Admin2,  ServiceName.Encode("prepo:a2"), PrepoMaxSessionsCount); // 6.0.0+
-            _serverManager.RegisterServer((int)PrepoPortIndex.Manager, ServiceName.Encode("prepo:m"),  PrepoMaxSessionsCount);
-            _serverManager.RegisterServer((int)PrepoPortIndex.User,    ServiceName.Encode("prepo:u"),  PrepoMaxSessionsCount);
-            _serverManager.RegisterServer((int)PrepoPortIndex.System,  ServiceName.Encode("prepo:s"),  PrepoMaxSessionsCount);
-            _serverManager.RegisterServer((int)PrepoPortIndex.Debug,   ServiceName.Encode("prepo:d"),  PrepoMaxSessionsCount); // 1.0.0
+            _serverManager.RegisterServer((int)PrepoPortIndex.Admin,   ServiceName.Encode("prepo:a"),  MaxSessionsCount); // 1.0.0-5.1.0
+            _serverManager.RegisterServer((int)PrepoPortIndex.Admin2,  ServiceName.Encode("prepo:a2"), MaxSessionsCount); // 6.0.0+
+            _serverManager.RegisterServer((int)PrepoPortIndex.Manager, ServiceName.Encode("prepo:m"),  MaxSessionsCount);
+            _serverManager.RegisterServer((int)PrepoPortIndex.User,    ServiceName.Encode("prepo:u"),  MaxSessionsCount);
+            _serverManager.RegisterServer((int)PrepoPortIndex.System,  ServiceName.Encode("prepo:s"),  MaxSessionsCount);
+            _serverManager.RegisterServer((int)PrepoPortIndex.Debug,   ServiceName.Encode("prepo:d"),  MaxSessionsCount); // 1.0.0
 #pragma warning restore IDE0055
         }
 
diff --git a/src/Ryujinx.Horizon/Psc/Ipc/PmControl.cs b/src/Ryujinx.Horizon/Psc/Ipc/PmControl.cs
new file mode 100644
index 000000000..671472e4e
--- /dev/null
+++ b/src/Ryujinx.Horizon/Psc/Ipc/PmControl.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Psc;
+
+namespace Ryujinx.Horizon.Psc.Ipc
+{
+    partial class PmControl : IPmControl
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Psc/Ipc/PmService.cs b/src/Ryujinx.Horizon/Psc/Ipc/PmService.cs
new file mode 100644
index 000000000..c38da8581
--- /dev/null
+++ b/src/Ryujinx.Horizon/Psc/Ipc/PmService.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Psc;
+
+namespace Ryujinx.Horizon.Psc.Ipc
+{
+    partial class PmService : IPmService
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Psc/Ipc/PmStateLock.cs b/src/Ryujinx.Horizon/Psc/Ipc/PmStateLock.cs
new file mode 100644
index 000000000..cef68ac54
--- /dev/null
+++ b/src/Ryujinx.Horizon/Psc/Ipc/PmStateLock.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Psc;
+
+namespace Ryujinx.Horizon.Psc.Ipc
+{
+    partial class PmStateLock : IPmStateLock
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Psc/PscIpcServer.cs b/src/Ryujinx.Horizon/Psc/PscIpcServer.cs
new file mode 100644
index 000000000..f8da56724
--- /dev/null
+++ b/src/Ryujinx.Horizon/Psc/PscIpcServer.cs
@@ -0,0 +1,50 @@
+using Ryujinx.Horizon.Psc.Ipc;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using Ryujinx.Horizon.Sdk.Sm;
+
+namespace Ryujinx.Horizon.Psc
+{
+    class PscIpcServer
+    {
+        private const int PscCMaxSessionsCount = 1;
+        private const int PscMMaxSessionsCount = 50;
+        private const int PscLMaxSessionsCount = 5;
+        private const int TotalMaxSessionsCount = PscCMaxSessionsCount + PscMMaxSessionsCount + PscLMaxSessionsCount;
+
+        private const int PointerBufferSize = 0;
+        private const int MaxDomains = 0;
+        private const int MaxDomainObjects = 0;
+        private const int MaxPortsCount = 3;
+
+        private static readonly ManagerOptions _options = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
+
+        private SmApi _sm;
+        private ServerManager _serverManager;
+
+        public void Initialize()
+        {
+            HeapAllocator allocator = new();
+
+            _sm = new SmApi();
+            _sm.Initialize().AbortOnFailure();
+
+            _serverManager = new ServerManager(allocator, _sm, MaxPortsCount, _options, TotalMaxSessionsCount);
+
+#pragma warning disable IDE0055 // Disable formatting
+            _serverManager.RegisterObjectForServer(new PmControl(),   ServiceName.Encode("psc:c"), PscCMaxSessionsCount);
+            _serverManager.RegisterObjectForServer(new PmService(),   ServiceName.Encode("psc:m"), PscMMaxSessionsCount);
+            _serverManager.RegisterObjectForServer(new PmStateLock(), ServiceName.Encode("psc:l"), PscLMaxSessionsCount); // 9.0.0+
+#pragma warning restore IDE0055
+        }
+
+        public void ServiceRequests()
+        {
+            _serverManager.ServiceRequests();
+        }
+
+        public void Shutdown()
+        {
+            _serverManager.Dispose();
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Psc/PscMain.cs b/src/Ryujinx.Horizon/Psc/PscMain.cs
new file mode 100644
index 000000000..facb6bc08
--- /dev/null
+++ b/src/Ryujinx.Horizon/Psc/PscMain.cs
@@ -0,0 +1,17 @@
+namespace Ryujinx.Horizon.Psc
+{
+    class PscMain : IService
+    {
+        public static void Main(ServiceTable serviceTable)
+        {
+            PscIpcServer ipcServer = new();
+
+            ipcServer.Initialize();
+
+            serviceTable.SignalServiceReady();
+
+            ipcServer.ServiceRequests();
+            ipcServer.Shutdown();
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Fs/FileHandle.cs b/src/Ryujinx.Horizon/Sdk/Fs/FileHandle.cs
new file mode 100644
index 000000000..1993577d9
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Fs/FileHandle.cs
@@ -0,0 +1,13 @@
+
+namespace Ryujinx.Horizon.Sdk.Fs
+{
+    public readonly struct FileHandle
+    {
+        public object Value { get; }
+
+        public FileHandle(object value)
+        {
+            Value = value;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Fs/FsResult.cs b/src/Ryujinx.Horizon/Sdk/Fs/FsResult.cs
new file mode 100644
index 000000000..a4b70bd58
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Fs/FsResult.cs
@@ -0,0 +1,13 @@
+using Ryujinx.Horizon.Common;
+
+namespace Ryujinx.Horizon.Sdk.Fs
+{
+    static class FsResult
+    {
+        private const int ModuleId = 2;
+
+        public static Result PathNotFound => new(ModuleId, 1);
+        public static Result PathAlreadyExists => new(ModuleId, 2);
+        public static Result TargetNotFound => new(ModuleId, 1002);
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Fs/IFsClient.cs b/src/Ryujinx.Horizon/Sdk/Fs/IFsClient.cs
new file mode 100644
index 000000000..caf6b03e9
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Fs/IFsClient.cs
@@ -0,0 +1,16 @@
+using Ryujinx.Horizon.Common;
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Fs
+{
+    public interface IFsClient
+    {
+        Result QueryMountSystemDataCacheSize(out long size, ulong dataId);
+        Result MountSystemData(string mountName, ulong dataId);
+        Result OpenFile(out FileHandle handle, string path, OpenMode openMode);
+        Result ReadFile(FileHandle handle, long offset, Span<byte> destination);
+        Result GetFileSize(out long size, FileHandle handle);
+        void CloseFile(FileHandle handle);
+        void Unmount(string mountName);
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Fs/OpenMode.cs b/src/Ryujinx.Horizon/Sdk/Fs/OpenMode.cs
new file mode 100644
index 000000000..add2ca48a
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Fs/OpenMode.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Fs
+{
+    [Flags]
+    public enum OpenMode
+    {
+        Read = 1,
+        Write = 2,
+        AllowAppend = 4,
+        ReadWrite = 3,
+        All = 7,
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Hshl/IManager.cs b/src/Ryujinx.Horizon/Sdk/Hshl/IManager.cs
new file mode 100644
index 000000000..13955c692
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Hshl/IManager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Hshl
+{
+    interface IManager : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Hshl/ISetterManager.cs b/src/Ryujinx.Horizon/Sdk/Hshl/ISetterManager.cs
new file mode 100644
index 000000000..8a4b93dd1
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Hshl/ISetterManager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Hshl
+{
+    interface ISetterManager : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ins/IReceiverManager.cs b/src/Ryujinx.Horizon/Sdk/Ins/IReceiverManager.cs
new file mode 100644
index 000000000..28fc757e5
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ins/IReceiverManager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Ins
+{
+    interface IReceiverManager : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ins/ISenderManager.cs b/src/Ryujinx.Horizon/Sdk/Ins/ISenderManager.cs
new file mode 100644
index 000000000..878dbfb32
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ins/ISenderManager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Ins
+{
+    interface ISenderManager : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Lbl/ILblController.cs b/src/Ryujinx.Horizon/Sdk/Lbl/ILblController.cs
new file mode 100644
index 000000000..594722e9c
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Lbl/ILblController.cs
@@ -0,0 +1,20 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Lbl
+{
+    interface ILblController : IServiceObject
+    {
+        Result SetBrightnessReflectionDelayLevel(float unknown0, float unknown1);
+        Result GetBrightnessReflectionDelayLevel(out float unknown1, float unknown0);
+        Result SetCurrentBrightnessMapping(float unknown0, float unknown1, float unknown2);
+        Result GetCurrentBrightnessMapping(out float unknown0, out float unknown1, out float unknown2);
+        Result SetCurrentAmbientLightSensorMapping(float unknown0, float unknown1, float unknown2);
+        Result GetCurrentAmbientLightSensorMapping(out float unknown0, out float unknown1, out float unknown2);
+        Result SetCurrentBrightnessSettingForVrMode(float currentBrightnessSettingForVrMode);
+        Result GetCurrentBrightnessSettingForVrMode(out float currentBrightnessSettingForVrMode);
+        Result EnableVrMode();
+        Result DisableVrMode();
+        Result IsVrModeEnabled(out bool vrModeEnabled);
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Lbl/LblApi.cs b/src/Ryujinx.Horizon/Sdk/Lbl/LblApi.cs
new file mode 100644
index 000000000..843a9acd6
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Lbl/LblApi.cs
@@ -0,0 +1,43 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Sm;
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Lbl
+{
+    public class LblApi : IDisposable
+    {
+        private const string LblName = "lbl";
+
+        private int _sessionHandle;
+
+        public LblApi()
+        {
+            using var smApi = new SmApi();
+
+            smApi.Initialize();
+            smApi.GetServiceHandle(out _sessionHandle, ServiceName.Encode(LblName)).AbortOnFailure();
+        }
+
+        public Result EnableVrMode()
+        {
+            return ServiceUtil.SendRequest(out _, _sessionHandle, 26, sendPid: false, ReadOnlySpan<byte>.Empty);
+        }
+
+        public Result DisableVrMode()
+        {
+            return ServiceUtil.SendRequest(out _, _sessionHandle, 27, sendPid: false, ReadOnlySpan<byte>.Empty);
+        }
+
+        public void Dispose()
+        {
+            if (_sessionHandle != 0)
+            {
+                HorizonStatic.Syscall.CloseHandle(_sessionHandle);
+
+                _sessionHandle = 0;
+            }
+
+            GC.SuppressFinalize(this);
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/MmNv/IRequest.cs b/src/Ryujinx.Horizon/Sdk/MmNv/IRequest.cs
new file mode 100644
index 000000000..300b957fd
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/MmNv/IRequest.cs
@@ -0,0 +1,17 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.MmNv
+{
+    interface IRequest : IServiceObject
+    {
+        Result InitializeOld(Module module, uint fgmPriority, uint autoClearEvent);
+        Result FinalizeOld(Module module);
+        Result SetAndWaitOld(Module module, uint clockRateMin, int clockRateMax);
+        Result GetOld(out uint clockRateActual, Module module);
+        Result Initialize(out uint requestId, Module module, uint fgmPriority, uint autoClearEvent);
+        Result Finalize(uint requestId);
+        Result SetAndWait(uint requestId, uint clockRateMin, int clockRateMax);
+        Result Get(out uint clockRateActual, uint requestId);
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/MmNv/Module.cs b/src/Ryujinx.Horizon/Sdk/MmNv/Module.cs
new file mode 100644
index 000000000..e029d037b
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/MmNv/Module.cs
@@ -0,0 +1,15 @@
+namespace Ryujinx.Horizon.Sdk.MmNv
+{
+    enum Module : uint
+    {
+        Cpu,
+        Gpu,
+        Emc,
+        SysBus,
+        MSelect,
+        NvDec,
+        NvEnc,
+        NvJpg,
+        Test,
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/MmNv/Session.cs b/src/Ryujinx.Horizon/Sdk/MmNv/Session.cs
new file mode 100644
index 000000000..b91585693
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/MmNv/Session.cs
@@ -0,0 +1,26 @@
+namespace Ryujinx.Horizon.Sdk.MmNv
+{
+    class Session
+    {
+        public Module Module { get; }
+        public uint Id { get; }
+        public bool IsAutoClearEvent { get; }
+        public uint ClockRateMin { get; private set; }
+        public int ClockRateMax { get; private set; }
+
+        public Session(uint id, Module module, bool isAutoClearEvent)
+        {
+            Module = module;
+            Id = id;
+            IsAutoClearEvent = isAutoClearEvent;
+            ClockRateMin = 0;
+            ClockRateMax = -1;
+        }
+
+        public void SetAndWait(uint clockRateMin, int clockRateMax)
+        {
+            ClockRateMin = clockRateMin;
+            ClockRateMax = clockRateMax;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/AhoCorasick.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/AhoCorasick.cs
new file mode 100644
index 000000000..e772427c0
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/AhoCorasick.cs
@@ -0,0 +1,251 @@
+using System;
+using System.Diagnostics;
+using System.Text;
+
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    class AhoCorasick
+    {
+        public delegate bool MatchCallback(ReadOnlySpan<byte> text, int matchStartOffset, int matchEndOffset, int nodeId, ref MatchState state);
+        public delegate bool MatchCallback<T>(ReadOnlySpan<byte> text, int matchStartOffset, int matchEndOffset, int nodeId, ref T state);
+
+        private readonly SparseSet _wordMap = new();
+        private readonly CompressedArray _wordLengths = new();
+        private readonly SparseSet _multiWordMap = new();
+        private readonly CompressedArray _multiWordIndices = new();
+        private readonly SparseSet _nodeMap = new();
+        private uint _nodesPerCharacter;
+        private readonly Bp _bp = new();
+
+        public bool Import(ref BinaryReader reader)
+        {
+            if (!_wordLengths.Import(ref reader) ||
+                !_wordMap.Import(ref reader) ||
+                !_multiWordIndices.Import(ref reader) ||
+                !_multiWordMap.Import(ref reader))
+            {
+                return false;
+            }
+
+            if (!reader.Read(out _nodesPerCharacter))
+            {
+                return false;
+            }
+
+            return _nodeMap.Import(ref reader) && _bp.Import(ref reader);
+        }
+
+        public void Match(ReadOnlySpan<byte> utf8Text, MatchCallback callback, ref MatchState state)
+        {
+            int nodeId = 0;
+
+            for (int index = 0; index < utf8Text.Length; index++)
+            {
+                long c = utf8Text[index];
+
+                while (true)
+                {
+                    long nodeSparseIndex = _nodesPerCharacter * c + (uint)nodeId;
+                    int nodePlainIndex = _nodeMap.Rank1(nodeSparseIndex);
+
+                    if (nodePlainIndex != 0)
+                    {
+                        long foundNodeSparseIndex = _nodeMap.Select1Ex(nodePlainIndex - 1);
+
+                        if (foundNodeSparseIndex > 0 && foundNodeSparseIndex == nodeSparseIndex)
+                        {
+                            nodeId = nodePlainIndex;
+
+                            if (callback != null)
+                            {
+                                // Match full word.
+                                if (_wordMap.Has(nodePlainIndex))
+                                {
+                                    int wordLength = _wordLengths[_wordMap.Rank1((uint)nodePlainIndex) - 1];
+                                    int startIndex = index + 1 - wordLength;
+
+                                    if (!callback(utf8Text, startIndex, index + 1, nodeId, ref state))
+                                    {
+                                        return;
+                                    }
+                                }
+
+                                // If this is a phrase composed of multiple words, also match each sub-word.
+                                while (_multiWordMap.Has(nodePlainIndex))
+                                {
+                                    nodePlainIndex = _multiWordIndices[_multiWordMap.Rank1((uint)nodePlainIndex) - 1];
+
+                                    int wordLength = _wordMap.Has(nodePlainIndex) ? _wordLengths[_wordMap.Rank1(nodePlainIndex) - 1] : 0;
+                                    int startIndex = index + 1 - wordLength;
+
+                                    if (!callback(utf8Text, startIndex, index + 1, nodePlainIndex, ref state))
+                                    {
+                                        return;
+                                    }
+                                }
+                            }
+
+                            break;
+                        }
+                    }
+
+                    if (nodeId == 0)
+                    {
+                        break;
+                    }
+
+                    int nodePos = _bp.ToPos(nodeId);
+                    nodePos = _bp.Enclose(nodePos);
+                    if (nodePos < 0)
+                    {
+                        return;
+                    }
+
+                    nodeId = _bp.ToNodeId(nodePos);
+                }
+            }
+        }
+
+        public void Match<T>(ReadOnlySpan<byte> utf8Text, MatchCallback<T> callback, ref T state)
+        {
+            int nodeId = 0;
+
+            for (int index = 0; index < utf8Text.Length; index++)
+            {
+                long c = utf8Text[index];
+
+                while (true)
+                {
+                    long nodeSparseIndex = _nodesPerCharacter * c + (uint)nodeId;
+                    int nodePlainIndex = _nodeMap.Rank1(nodeSparseIndex);
+
+                    if (nodePlainIndex != 0)
+                    {
+                        long foundNodeSparseIndex = _nodeMap.Select1Ex(nodePlainIndex - 1);
+
+                        if (foundNodeSparseIndex > 0 && foundNodeSparseIndex == nodeSparseIndex)
+                        {
+                            nodeId = nodePlainIndex;
+
+                            if (callback != null)
+                            {
+                                // Match full word.
+                                if (_wordMap.Has(nodePlainIndex))
+                                {
+                                    int wordLength = _wordLengths[_wordMap.Rank1((uint)nodePlainIndex) - 1];
+                                    int startIndex = index + 1 - wordLength;
+
+                                    if (!callback(utf8Text, startIndex, index + 1, nodeId, ref state))
+                                    {
+                                        return;
+                                    }
+                                }
+
+                                // If this is a phrase composed of multiple words, also match each sub-word.
+                                while (_multiWordMap.Has(nodePlainIndex))
+                                {
+                                    nodePlainIndex = _multiWordIndices[_multiWordMap.Rank1((uint)nodePlainIndex) - 1];
+
+                                    int wordLength = _wordMap.Has(nodePlainIndex) ? _wordLengths[_wordMap.Rank1(nodePlainIndex) - 1] : 0;
+                                    int startIndex = index + 1 - wordLength;
+
+                                    if (!callback(utf8Text, startIndex, index + 1, nodePlainIndex, ref state))
+                                    {
+                                        return;
+                                    }
+                                }
+                            }
+
+                            break;
+                        }
+                    }
+
+                    if (nodeId == 0)
+                    {
+                        break;
+                    }
+
+                    int nodePos = _bp.ToPos(nodeId);
+                    nodePos = _bp.Enclose(nodePos);
+                    if (nodePos < 0)
+                    {
+                        return;
+                    }
+
+                    nodeId = _bp.ToNodeId(nodePos);
+                }
+            }
+        }
+
+        public string GetWordList(bool includeMultiWord = true)
+        {
+            // Storage must be large enough to fit the largest word in the dictionary.
+            // Since this is only used for debugging, it's fine to increase the size manually if needed.
+            StringBuilder sb = new();
+            Span<byte> storage = new byte[1024];
+
+            // Traverse trie from the root.
+            GetWord(sb, storage, 0, 0, includeMultiWord);
+
+            return sb.ToString();
+        }
+
+        private void GetWord(StringBuilder sb, Span<byte> storage, int storageOffset, int nodeId, bool includeMultiWord)
+        {
+            int characters = (int)((_nodeMap.RangeEndValue + _nodesPerCharacter - 1) / _nodesPerCharacter);
+
+            for (int c = 0; c < characters; c++)
+            {
+                long nodeSparseIndex = _nodesPerCharacter * c + (uint)nodeId;
+                int nodePlainIndex = _nodeMap.Rank1(nodeSparseIndex);
+
+                if (nodePlainIndex != 0)
+                {
+                    long foundNodeSparseIndex = _nodeMap.Select1Ex(nodePlainIndex - 1);
+
+                    if (foundNodeSparseIndex > 0 && foundNodeSparseIndex == nodeSparseIndex)
+                    {
+                        storage[storageOffset] = (byte)c;
+                        int nextNodeId = nodePlainIndex;
+
+                        if (_wordMap.Has(nodePlainIndex))
+                        {
+                            sb.AppendLine(Encoding.UTF8.GetString(storage[..(storageOffset + 1)]));
+
+                            // Some basic validation to ensure we imported the dictionary properly.
+                            int wordLength = _wordLengths[_wordMap.Rank1((uint)nodePlainIndex) - 1];
+
+                            Debug.Assert(storageOffset + 1 == wordLength);
+                        }
+
+                        if (includeMultiWord)
+                        {
+                            int lastMultiWordIndex = 0;
+                            string multiWord = "";
+
+                            while (_multiWordMap.Has(nodePlainIndex))
+                            {
+                                nodePlainIndex = _multiWordIndices[_multiWordMap.Rank1((uint)nodePlainIndex) - 1];
+
+                                int wordLength = _wordMap.Has(nodePlainIndex) ? _wordLengths[_wordMap.Rank1(nodePlainIndex) - 1] : 0;
+                                int startIndex = storageOffset + 1 - wordLength;
+
+                                multiWord += Encoding.UTF8.GetString(storage[lastMultiWordIndex..startIndex]) + " ";
+                                lastMultiWordIndex = startIndex;
+                            }
+
+                            if (lastMultiWordIndex != 0)
+                            {
+                                multiWord += Encoding.UTF8.GetString(storage[lastMultiWordIndex..(storageOffset + 1)]);
+
+                                sb.AppendLine(multiWord);
+                            }
+                        }
+
+                        GetWord(sb, storage, storageOffset + 1, nextNodeId, includeMultiWord);
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/BinaryReader.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/BinaryReader.cs
new file mode 100644
index 000000000..5bad376a5
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/BinaryReader.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    ref struct BinaryReader
+    {
+        private readonly ReadOnlySpan<byte> _data;
+        private int _offset;
+
+        public BinaryReader(ReadOnlySpan<byte> data)
+        {
+            _data = data;
+        }
+
+        public bool Read<T>(out T value) where T : unmanaged
+        {
+            int byteLength = Unsafe.SizeOf<T>();
+
+            if ((uint)(_offset + byteLength) <= (uint)_data.Length)
+            {
+                value = MemoryMarshal.Cast<byte, T>(_data[_offset..])[0];
+                _offset += byteLength;
+
+                return true;
+            }
+
+            value = default;
+
+            return false;
+        }
+
+        public int AllocateAndReadArray<T>(ref T[] array, int length, int maxLengthExclusive) where T : unmanaged
+        {
+            return AllocateAndReadArray(ref array, Math.Min(length, maxLengthExclusive));
+        }
+
+        public int AllocateAndReadArray<T>(ref T[] array, int length) where T : unmanaged
+        {
+            array = new T[length];
+
+            return ReadArray(array);
+        }
+
+        public int ReadArray<T>(T[] array) where T : unmanaged
+        {
+            if (array != null)
+            {
+                int byteLength = array.Length * Unsafe.SizeOf<T>();
+                byteLength = Math.Min(byteLength, _data.Length - _offset);
+
+                MemoryMarshal.Cast<byte, T>(_data.Slice(_offset, byteLength)).CopyTo(array);
+
+                _offset += byteLength;
+
+                return byteLength / Unsafe.SizeOf<T>();
+            }
+
+            return 0;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/BitVector32.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/BitVector32.cs
new file mode 100644
index 000000000..f54562011
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/BitVector32.cs
@@ -0,0 +1,78 @@
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    class BitVector32
+    {
+        private const int BitsPerWord = Set.BitsPerWord;
+
+        private int _bitLength;
+        private uint[] _array;
+
+        public int BitLength => _bitLength;
+        public uint[] Array => _array;
+
+        public BitVector32()
+        {
+            _bitLength = 0;
+            _array = null;
+        }
+
+        public BitVector32(int length)
+        {
+            _bitLength = length;
+            _array = new uint[(length + BitsPerWord - 1) / BitsPerWord];
+        }
+
+        public bool Has(int index)
+        {
+            if ((uint)index < (uint)_bitLength)
+            {
+                int wordIndex = index / BitsPerWord;
+                int wordBitOffset = index % BitsPerWord;
+
+                return ((_array[wordIndex] >> wordBitOffset) & 1u) != 0;
+            }
+
+            return false;
+        }
+
+        public bool TurnOn(int index, int count)
+        {
+            for (int bit = 0; bit < count; bit++)
+            {
+                if (!TurnOn(index + bit))
+                {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        public bool TurnOn(int index)
+        {
+            if ((uint)index < (uint)_bitLength)
+            {
+                int wordIndex = index / BitsPerWord;
+                int wordBitOffset = index % BitsPerWord;
+
+                _array[wordIndex] |= 1u << wordBitOffset;
+
+                return true;
+            }
+
+            return false;
+        }
+
+        public bool Import(ref BinaryReader reader)
+        {
+            if (!reader.Read(out _bitLength))
+            {
+                return false;
+            }
+
+            int arrayLength = (_bitLength + BitsPerWord - 1) / BitsPerWord;
+
+            return reader.AllocateAndReadArray(ref _array, arrayLength) == arrayLength;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/Bp.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/Bp.cs
new file mode 100644
index 000000000..5a8f3df29
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/Bp.cs
@@ -0,0 +1,54 @@
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    class Bp
+    {
+        private readonly BpNode _firstNode = new();
+        private readonly SbvSelect _sbvSelect = new();
+
+        public bool Import(ref BinaryReader reader)
+        {
+            return _firstNode.Import(ref reader) && _sbvSelect.Import(ref reader);
+        }
+
+        public int ToPos(int index)
+        {
+            return _sbvSelect.Select(_firstNode.Set, index);
+        }
+
+        public int Enclose(int index)
+        {
+            if ((uint)index < (uint)_firstNode.Set.BitVector.BitLength)
+            {
+                if (!_firstNode.Set.Has(index))
+                {
+                    index = _firstNode.FindOpen(index);
+                }
+
+                if (index > 0)
+                {
+                    return _firstNode.Enclose(index);
+                }
+            }
+
+            return -1;
+        }
+
+        public int ToNodeId(int index)
+        {
+            if ((uint)index < (uint)_firstNode.Set.BitVector.BitLength)
+            {
+                if (!_firstNode.Set.Has(index))
+                {
+                    index = _firstNode.FindOpen(index);
+                }
+
+                if (index >= 0)
+                {
+                    return _firstNode.Set.Rank1(index) - 1;
+                }
+            }
+
+            return -1;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/BpNode.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/BpNode.cs
new file mode 100644
index 000000000..6884cddd2
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/BpNode.cs
@@ -0,0 +1,241 @@
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    class BpNode
+    {
+        private readonly Set _set = new();
+        private SparseSet _sparseSet;
+        private BpNode _nextNode;
+
+        public Set Set => _set;
+
+        public bool Import(ref BinaryReader reader)
+        {
+            if (!_set.Import(ref reader))
+            {
+                return false;
+            }
+
+            if (!reader.Read(out byte hasNext))
+            {
+                return false;
+            }
+
+            if (hasNext == 0)
+            {
+                return true;
+            }
+
+            _sparseSet = new();
+            _nextNode = new();
+
+            return _sparseSet.Import(ref reader) && _nextNode.Import(ref reader);
+        }
+
+        public int FindOpen(int index)
+        {
+            uint membershipBits = _set.BitVector.Array[index / Set.BitsPerWord];
+
+            int wordBitOffset = index % Set.BitsPerWord;
+            int unsetBits = 1;
+
+            for (int bit = wordBitOffset - 1; bit >= 0; bit--)
+            {
+                if (((membershipBits >> bit) & 1) != 0)
+                {
+                    if (--unsetBits == 0)
+                    {
+                        return (index & ~(Set.BitsPerWord - 1)) | bit;
+                    }
+                }
+                else
+                {
+                    unsetBits++;
+                }
+            }
+
+            int plainIndex = _sparseSet.Rank1(index);
+            if (plainIndex == 0)
+            {
+                return -1;
+            }
+
+            int newIndex = index;
+
+            if (!_sparseSet.Has(index))
+            {
+                if (plainIndex == 0 || _nextNode == null)
+                {
+                    return -1;
+                }
+
+                newIndex = _sparseSet.Select1(plainIndex);
+                if (newIndex < 0)
+                {
+                    return -1;
+                }
+            }
+            else
+            {
+                plainIndex--;
+            }
+
+            int openIndex = _nextNode.FindOpen(plainIndex);
+            if (openIndex < 0)
+            {
+                return -1;
+            }
+
+            int openSparseIndex = _sparseSet.Select1(openIndex);
+            if (openSparseIndex < 0)
+            {
+                return -1;
+            }
+
+            if (newIndex != index)
+            {
+                unsetBits = 1;
+
+                for (int bit = newIndex % Set.BitsPerWord - 1; bit > wordBitOffset; bit--)
+                {
+                    unsetBits += ((membershipBits >> bit) & 1) != 0 ? -1 : 1;
+                }
+
+                int bestCandidate = -1;
+
+                membershipBits = _set.BitVector.Array[openSparseIndex / Set.BitsPerWord];
+
+                for (int bit = openSparseIndex % Set.BitsPerWord + 1; bit < Set.BitsPerWord; bit++)
+                {
+                    if (unsetBits - 1 == 0)
+                    {
+                        bestCandidate = bit;
+                    }
+
+                    unsetBits += ((membershipBits >> bit) & 1) != 0 ? -1 : 1;
+                }
+
+                return (openSparseIndex & ~(Set.BitsPerWord - 1)) | bestCandidate;
+            }
+            else
+            {
+                return openSparseIndex;
+            }
+        }
+
+        public int Enclose(int index)
+        {
+            uint membershipBits = _set.BitVector.Array[index / Set.BitsPerWord];
+
+            int unsetBits = 1;
+
+            for (int bit = index % Set.BitsPerWord - 1; bit >= 0; bit--)
+            {
+                if (((membershipBits >> bit) & 1) != 0)
+                {
+                    if (--unsetBits == 0)
+                    {
+                        return (index & ~(Set.BitsPerWord - 1)) + bit;
+                    }
+                }
+                else
+                {
+                    unsetBits++;
+                }
+            }
+
+            int setBits = 2;
+
+            for (int bit = index % Set.BitsPerWord + 1; bit < Set.BitsPerWord; bit++)
+            {
+                if (((membershipBits >> bit) & 1) != 0)
+                {
+                    setBits++;
+                }
+                else
+                {
+                    if (--setBits == 0)
+                    {
+                        return FindOpen((index & ~(Set.BitsPerWord - 1)) + bit);
+                    }
+                }
+            }
+
+            int newIndex = index;
+
+            if (!_sparseSet.Has(index))
+            {
+                newIndex = _sparseSet.Select1(_sparseSet.Rank1(index));
+                if (newIndex < 0)
+                {
+                    return -1;
+                }
+            }
+
+            if (!_set.Has(newIndex))
+            {
+                newIndex = FindOpen(newIndex);
+                if (newIndex < 0)
+                {
+                    return -1;
+                }
+            }
+            else
+            {
+                newIndex = _nextNode.Enclose(_sparseSet.Rank1(newIndex) - 1);
+                if (newIndex < 0)
+                {
+                    return -1;
+                }
+
+                newIndex = _sparseSet.Select1(newIndex);
+            }
+
+            int nearestIndex = _sparseSet.Select1(_sparseSet.Rank1(newIndex));
+            if (nearestIndex < 0)
+            {
+                return -1;
+            }
+
+            setBits = 0;
+
+            membershipBits = _set.BitVector.Array[newIndex / Set.BitsPerWord];
+
+            if ((newIndex / Set.BitsPerWord) == (nearestIndex / Set.BitsPerWord))
+            {
+                for (int bit = nearestIndex % Set.BitsPerWord - 1; bit >= newIndex % Set.BitsPerWord; bit--)
+                {
+                    if (((membershipBits >> bit) & 1) != 0)
+                    {
+                        if (++setBits > 0)
+                        {
+                            return (newIndex & ~(Set.BitsPerWord - 1)) + bit;
+                        }
+                    }
+                    else
+                    {
+                        setBits--;
+                    }
+                }
+            }
+            else
+            {
+                for (int bit = Set.BitsPerWord - 1; bit >= newIndex % Set.BitsPerWord; bit--)
+                {
+                    if (((membershipBits >> bit) & 1) != 0)
+                    {
+                        if (++setBits > 0)
+                        {
+                            return (newIndex & ~(Set.BitsPerWord - 1)) + bit;
+                        }
+                    }
+                    else
+                    {
+                        setBits--;
+                    }
+                }
+            }
+
+            return -1;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/CompressedArray.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/CompressedArray.cs
new file mode 100644
index 000000000..1200f1def
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/CompressedArray.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    class CompressedArray
+    {
+        private const int MaxUncompressedEntries = 64;
+        private const int CompressedEntriesPerBlock = 64;
+        private const int BitsPerWord = Set.BitsPerWord;
+
+        private readonly struct BitfieldRange
+        {
+            private readonly uint _range;
+            private readonly int _baseValue;
+
+            public int BitfieldIndex => (int)(_range & 0x7ffffff);
+            public int BitfieldLength => (int)(_range >> 27) + 1;
+            public int BaseValue => _baseValue;
+
+            public BitfieldRange(uint range, int baseValue)
+            {
+                _range = range;
+                _baseValue = baseValue;
+            }
+        }
+
+        private uint[] _bitfieldRanges;
+        private uint[] _bitfields;
+        private int[] _uncompressedArray;
+
+        public int Length => (_bitfieldRanges.Length / 2) * CompressedEntriesPerBlock + _uncompressedArray.Length;
+
+        public int this[int index]
+        {
+            get
+            {
+                var ranges = GetBitfieldRanges();
+
+                int rangeBlockIndex = index / CompressedEntriesPerBlock;
+
+                if (rangeBlockIndex < ranges.Length)
+                {
+                    var range = ranges[rangeBlockIndex];
+
+                    int bitfieldLength = range.BitfieldLength;
+                    int bitfieldOffset = (index % CompressedEntriesPerBlock) * bitfieldLength;
+                    int bitfieldIndex = range.BitfieldIndex + (bitfieldOffset / BitsPerWord);
+                    int bitOffset = bitfieldOffset % BitsPerWord;
+
+                    ulong bitfieldValue = _bitfields[bitfieldIndex];
+
+                    // If the bit fields crosses the word boundary, let's load the next one to ensure we
+                    // have access to the full value.
+                    if (bitOffset + bitfieldLength > BitsPerWord)
+                    {
+                        bitfieldValue |= (ulong)_bitfields[bitfieldIndex + 1] << 32;
+                    }
+
+                    int value = (int)(bitfieldValue >> bitOffset) & ((1 << bitfieldLength) - 1);
+
+                    // Sign-extend.
+                    int remainderBits = BitsPerWord - bitfieldLength;
+                    value <<= remainderBits;
+                    value >>= remainderBits;
+
+                    return value + range.BaseValue;
+                }
+                else if (rangeBlockIndex < _uncompressedArray.Length + _bitfieldRanges.Length * BitsPerWord)
+                {
+                    return _uncompressedArray[index % MaxUncompressedEntries];
+                }
+
+                return 0;
+            }
+        }
+
+        private ReadOnlySpan<BitfieldRange> GetBitfieldRanges()
+        {
+            return MemoryMarshal.Cast<uint, BitfieldRange>(_bitfieldRanges);
+        }
+
+        public bool Import(ref BinaryReader reader)
+        {
+            if (!reader.Read(out int bitfieldRangesCount) ||
+                reader.AllocateAndReadArray(ref _bitfieldRanges, bitfieldRangesCount) != bitfieldRangesCount)
+            {
+                return false;
+            }
+
+            if (!reader.Read(out int bitfieldsCount) || reader.AllocateAndReadArray(ref _bitfields, bitfieldsCount) != bitfieldsCount)
+            {
+                return false;
+            }
+
+            return reader.Read(out byte uncompressedArrayLength) &&
+                reader.AllocateAndReadArray(ref _uncompressedArray, uncompressedArrayLength, MaxUncompressedEntries) == uncompressedArrayLength;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/ContentsReader.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/ContentsReader.cs
new file mode 100644
index 000000000..cb865fa0f
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/ContentsReader.cs
@@ -0,0 +1,404 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Fs;
+using System;
+using System.IO;
+using System.IO.Compression;
+
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    class ContentsReader : IDisposable
+    {
+        private const string MountName = "NgWord";
+        private const string VersionFilePath = $"{MountName}:/version.dat";
+        private const ulong DataId = 0x100000000000823UL;
+
+        private enum AcType
+        {
+            AcNotB,
+            AcB1,
+            AcB2,
+            AcSimilarForm,
+            TableSimilarForm,
+        }
+
+        private readonly IFsClient _fsClient;
+        private readonly object _lock;
+        private bool _intialized;
+        private ulong _cacheSize;
+
+        public ContentsReader(IFsClient fsClient)
+        {
+            _lock = new();
+            _fsClient = fsClient;
+        }
+
+        private static void MakeMountPoint(out string path, AcType type, int regionIndex)
+        {
+            path = null;
+
+            switch (type)
+            {
+                case AcType.AcNotB:
+                    if (regionIndex < 0)
+                    {
+                        path = $"{MountName}:/ac_common_not_b_nx";
+                    }
+                    else
+                    {
+                        path = $"{MountName}:/ac_{regionIndex}_not_b_nx";
+                    }
+                    break;
+                case AcType.AcB1:
+                    if (regionIndex < 0)
+                    {
+                        path = $"{MountName}:/ac_common_b1_nx";
+                    }
+                    else
+                    {
+                        path = $"{MountName}:/ac_{regionIndex}_b1_nx";
+                    }
+                    break;
+                case AcType.AcB2:
+                    if (regionIndex < 0)
+                    {
+                        path = $"{MountName}:/ac_common_b2_nx";
+                    }
+                    else
+                    {
+                        path = $"{MountName}:/ac_{regionIndex}_b2_nx";
+                    }
+                    break;
+                case AcType.AcSimilarForm:
+                    path = $"{MountName}:/ac_similar_form_nx";
+                    break;
+                case AcType.TableSimilarForm:
+                    path = $"{MountName}:/table_similar_form_nx";
+                    break;
+            }
+        }
+
+        public Result Initialize(ulong cacheSize)
+        {
+            lock (_lock)
+            {
+                if (_intialized)
+                {
+                    return Result.Success;
+                }
+
+                Result result = _fsClient.QueryMountSystemDataCacheSize(out long dataCacheSize, DataId);
+                if (result.IsFailure)
+                {
+                    return result;
+                }
+
+                if (cacheSize < (ulong)dataCacheSize)
+                {
+                    return NgcResult.InvalidSize;
+                }
+
+                result = _fsClient.MountSystemData(MountName, DataId);
+                if (result.IsFailure)
+                {
+                    // Official firmware would return the result here,
+                    // we don't to support older firmware where the archive didn't exist yet.
+                    return Result.Success;
+                }
+
+                _cacheSize = cacheSize;
+                _intialized = true;
+
+                return Result.Success;
+            }
+        }
+
+        public Result Reload()
+        {
+            lock (_lock)
+            {
+                if (!_intialized)
+                {
+                    return Result.Success;
+                }
+
+                _fsClient.Unmount(MountName);
+
+                Result result = Result.Success;
+
+                try
+                {
+                    result = _fsClient.QueryMountSystemDataCacheSize(out long cacheSize, DataId);
+                    if (result.IsFailure)
+                    {
+                        return result;
+                    }
+
+                    if (_cacheSize < (ulong)cacheSize)
+                    {
+                        result = NgcResult.InvalidSize;
+                        return NgcResult.InvalidSize;
+                    }
+
+                    result = _fsClient.MountSystemData(MountName, DataId);
+                    if (result.IsFailure)
+                    {
+                        return result;
+                    }
+                }
+                finally
+                {
+                    if (result.IsFailure)
+                    {
+                        _intialized = false;
+                        _cacheSize = 0;
+                    }
+                }
+            }
+
+            return Result.Success;
+        }
+
+        private Result GetFileSize(out long size, string filePath)
+        {
+            size = 0;
+
+            lock (_lock)
+            {
+                Result result = _fsClient.OpenFile(out FileHandle handle, filePath, OpenMode.Read);
+                if (result.IsFailure)
+                {
+                    return result;
+                }
+
+                try
+                {
+                    result = _fsClient.GetFileSize(out size, handle);
+                    if (result.IsFailure)
+                    {
+                        return result;
+                    }
+                }
+                finally
+                {
+                    _fsClient.CloseFile(handle);
+                }
+            }
+
+            return Result.Success;
+        }
+
+        private Result GetFileContent(Span<byte> destination, string filePath)
+        {
+            lock (_lock)
+            {
+                Result result = _fsClient.OpenFile(out FileHandle handle, filePath, OpenMode.Read);
+                if (result.IsFailure)
+                {
+                    return result;
+                }
+
+                try
+                {
+                    result = _fsClient.ReadFile(handle, 0, destination);
+                    if (result.IsFailure)
+                    {
+                        return result;
+                    }
+                }
+                finally
+                {
+                    _fsClient.CloseFile(handle);
+                }
+            }
+
+            return Result.Success;
+        }
+
+        public Result GetVersionDataSize(out long size)
+        {
+            return GetFileSize(out size, VersionFilePath);
+        }
+
+        public Result GetVersionData(Span<byte> destination)
+        {
+            return GetFileContent(destination, VersionFilePath);
+        }
+
+        public Result ReadDictionaries(out AhoCorasick partialWordsTrie, out AhoCorasick completeWordsTrie, out AhoCorasick delimitedWordsTrie, int regionIndex)
+        {
+            completeWordsTrie = null;
+            delimitedWordsTrie = null;
+
+            MakeMountPoint(out string partialWordsTriePath, AcType.AcNotB, regionIndex);
+            MakeMountPoint(out string completeWordsTriePath, AcType.AcB1, regionIndex);
+            MakeMountPoint(out string delimitedWordsTriePath, AcType.AcB2, regionIndex);
+
+            Result result = ReadDictionary(out partialWordsTrie, partialWordsTriePath);
+            if (result.IsFailure)
+            {
+                return NgcResult.DataAccessError;
+            }
+
+            result = ReadDictionary(out completeWordsTrie, completeWordsTriePath);
+            if (result.IsFailure)
+            {
+                return NgcResult.DataAccessError;
+            }
+
+            return ReadDictionary(out delimitedWordsTrie, delimitedWordsTriePath);
+        }
+
+        public Result ReadSimilarFormDictionary(out AhoCorasick similarFormTrie)
+        {
+            MakeMountPoint(out string similarFormTriePath, AcType.AcSimilarForm, 0);
+
+            return ReadDictionary(out similarFormTrie, similarFormTriePath);
+        }
+
+        public Result ReadSimilarFormTable(out SimilarFormTable similarFormTable)
+        {
+            similarFormTable = null;
+
+            MakeMountPoint(out string similarFormTablePath, AcType.TableSimilarForm, 0);
+
+            Result result = ReadGZipCompressedArchive(out byte[] data, similarFormTablePath);
+            if (result.IsFailure)
+            {
+                return result;
+            }
+
+            BinaryReader reader = new(data);
+            SimilarFormTable table = new();
+
+            if (!table.Import(ref reader))
+            {
+                // Official firmware doesn't return an error here and just assumes the import was successful.
+                return NgcResult.DataAccessError;
+            }
+
+            similarFormTable = table;
+
+            return Result.Success;
+        }
+
+        public static Result ReadNotSeparatorDictionary(out AhoCorasick notSeparatorTrie)
+        {
+            notSeparatorTrie = null;
+
+            BinaryReader reader = new(EmbeddedTries.NotSeparatorTrie);
+            AhoCorasick ac = new();
+
+            if (!ac.Import(ref reader))
+            {
+                // Official firmware doesn't return an error here and just assumes the import was successful.
+                return NgcResult.DataAccessError;
+            }
+
+            notSeparatorTrie = ac;
+
+            return Result.Success;
+        }
+
+        private Result ReadDictionary(out AhoCorasick trie, string path)
+        {
+            trie = null;
+
+            Result result = ReadGZipCompressedArchive(out byte[] data, path);
+            if (result.IsFailure)
+            {
+                return result;
+            }
+
+            BinaryReader reader = new(data);
+            AhoCorasick ac = new();
+
+            if (!ac.Import(ref reader))
+            {
+                // Official firmware doesn't return an error here and just assumes the import was successful.
+                return NgcResult.DataAccessError;
+            }
+
+            trie = ac;
+
+            return Result.Success;
+        }
+
+        private Result ReadGZipCompressedArchive(out byte[] data, string filePath)
+        {
+            data = null;
+
+            Result result = _fsClient.OpenFile(out FileHandle handle, filePath, OpenMode.Read);
+            if (result.IsFailure)
+            {
+                return result;
+            }
+
+            try
+            {
+                result = _fsClient.GetFileSize(out long fileSize, handle);
+                if (result.IsFailure)
+                {
+                    return result;
+                }
+
+                data = new byte[fileSize];
+
+                result = _fsClient.ReadFile(handle, 0, data.AsSpan());
+                if (result.IsFailure)
+                {
+                    return result;
+                }
+            }
+            finally
+            {
+                _fsClient.CloseFile(handle);
+            }
+
+            try
+            {
+                data = DecompressGZipCompressedStream(data);
+            }
+            catch (InvalidDataException)
+            {
+                // Official firmware returns a different error, but it is translated to this error on the caller.
+                return NgcResult.DataAccessError;
+            }
+
+            return Result.Success;
+        }
+
+        private static byte[] DecompressGZipCompressedStream(byte[] data)
+        {
+            using MemoryStream input = new(data);
+            using GZipStream gZipStream = new(input, CompressionMode.Decompress);
+            using MemoryStream output = new();
+
+            gZipStream.CopyTo(output);
+
+            return output.ToArray();
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                lock (_lock)
+                {
+                    if (!_intialized)
+                    {
+                        return;
+                    }
+
+                    _fsClient.Unmount(MountName);
+                    _intialized = false;
+                }
+            }
+        }
+
+        public void Dispose()
+        {
+            Dispose(disposing: true);
+            GC.SuppressFinalize(this);
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/EmbeddedTries.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/EmbeddedTries.cs
new file mode 100644
index 000000000..37ee43fa3
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/EmbeddedTries.cs
@@ -0,0 +1,266 @@
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    static class EmbeddedTries
+    {
+        public static ReadOnlySpan<byte> NotSeparatorTrie => new byte[]
+        {
+            0x1A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
+            0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00,
+            0x04, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00,
+            0x04, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00,
+            0x04, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00,
+            0x04, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00,
+            0x04, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00,
+            0xE9, 0xFF, 0xE9, 0xFF, 0xF4, 0xFF, 0xFA, 0xBF, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x5F, 0xFF, 0xAF,
+            0xFF, 0xEB, 0xFF, 0xFA, 0x00, 0x00, 0x00, 0x00, 0xBF, 0xFF, 0xFB, 0x7F, 0xFF, 0xEF, 0xFF, 0xFD,
+            0x00, 0x00, 0x00, 0x00, 0xBF, 0xFF, 0xF7, 0xFF, 0xE8, 0xFF, 0xE9, 0xFF, 0x00, 0x00, 0x00, 0x00,
+            0xFC, 0x3F, 0xFF, 0xCF, 0xFF, 0xF3, 0xFF, 0xFA, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xE7, 0xFF,
+            0xFC, 0x9F, 0xFF, 0xF3, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x9F, 0xFF, 0xE7, 0xFF, 0xF9, 0x7F,
+            0x00, 0x00, 0x00, 0x00, 0xFE, 0x5F, 0xFF, 0xCF, 0xFF, 0xF3, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00,
+            0x3F, 0xFF, 0xCF, 0xFF, 0xF3, 0xFF, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xCF, 0xFF, 0xF3,
+            0xFF, 0xFC, 0x3F, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xCF, 0xFF, 0xFB, 0x7F, 0xFE, 0x9F, 0xFF, 0xF3,
+            0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x9F, 0xFF, 0xF3, 0xFF, 0xFC, 0x9F, 0x00, 0x00, 0x00, 0x00,
+            0xFF, 0xF3, 0x7F, 0xFE, 0xCF, 0xFF, 0xF5, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x03, 0x00, 0x00,
+            0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
+            0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
+            0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
+            0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
+            0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
+            0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
+            0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
+            0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
+            0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
+            0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
+            0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
+            0x00, 0x03, 0x00, 0x00, 0x00, 0x8A, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x85, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6E, 0x03, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00,
+            0x00, 0xAA, 0xAA, 0xAA, 0xAA, 0x54, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
+            0x55, 0x55, 0x55, 0xA9, 0x52, 0x55, 0x55, 0xA9, 0xAA, 0xAA, 0xAA, 0x54, 0x55, 0x55, 0x55, 0x55,
+            0x55, 0x55, 0xAA, 0x54, 0x55, 0xA5, 0x4A, 0x55, 0x55, 0x55, 0xAA, 0xAA, 0xAA, 0x52, 0x55, 0x55,
+            0x95, 0xAA, 0xAA, 0xAA, 0x54, 0x55, 0x55, 0xA5, 0xAA, 0xAA, 0x2A, 0x55, 0x55, 0x55, 0x55, 0x55,
+            0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0xA5, 0xAA, 0xAA, 0xAA, 0x54, 0x55, 0x55, 0x55, 0x55, 0x55,
+            0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
+            0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
+            0x55, 0x55, 0x55, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
+            0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
+            0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
+            0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
+            0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
+            0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0x4A,
+            0x55, 0x55, 0x55, 0xA9, 0xAA, 0xAA, 0x52, 0x55, 0x55, 0xA5, 0xAA, 0xAA, 0x4A, 0x55, 0x55, 0x05,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x7D, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x77, 0x01, 0x00,
+            0x00, 0xF7, 0x01, 0x00, 0x00, 0x77, 0x02, 0x00, 0x00, 0xF7, 0x02, 0x00, 0x00, 0x6E, 0x03, 0x00,
+            0x00, 0x6E, 0x03, 0x00, 0x00, 0x6E, 0x03, 0x00, 0x00, 0x6E, 0x03, 0x00, 0x00, 0x6E, 0x03, 0x00,
+            0x00, 0x6E, 0x03, 0x00, 0x00, 0x6E, 0x03, 0x00, 0x00, 0x6E, 0x03, 0x00, 0x00, 0x6E, 0x03, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x1F, 0x2F, 0x3F, 0x4E, 0x5E, 0x6D, 0x00, 0x0F, 0x1E,
+            0x2E, 0x3D, 0x4C, 0x5C, 0x6B, 0x00, 0x10, 0x20, 0x2F, 0x3F, 0x4F, 0x5F, 0x6F, 0x00, 0x10, 0x20,
+            0x30, 0x40, 0x50, 0x60, 0x70, 0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x00, 0x10, 0x20,
+            0x30, 0x40, 0x50, 0x60, 0x70, 0x00, 0x10, 0x20, 0x30, 0x3F, 0x4F, 0x5E, 0x6D, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6E, 0x03,
+            0x00, 0x00, 0x01, 0x00, 0x01, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x40, 0x00, 0x40, 0x00, 0x20,
+            0x00, 0x20, 0x00, 0x10, 0x00, 0x08, 0x00, 0x08, 0x00, 0x04, 0x00, 0x02, 0x00, 0x02, 0x00, 0x01,
+            0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00,
+            0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00,
+            0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00,
+            0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00,
+            0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x40, 0x00, 0x40, 0x00, 0x20, 0x00, 0x10, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00,
+            0x21, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x00, 0x03, 0x05, 0x07, 0x09, 0x0B, 0x0D, 0x0F,
+            0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E,
+            0x00, 0x02, 0x04, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8A, 0x03, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x8A, 0x03, 0x00, 0x00, 0x00, 0x8A, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0xC5, 0x01,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x51, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x89, 0x03, 0x00,
+            0x00, 0x07, 0x00, 0x00, 0x00, 0xC6, 0x00, 0x00, 0x00, 0xCF, 0xED, 0x81, 0x61, 0xD9, 0xDC, 0x8A,
+            0xD3, 0xF0, 0xBB, 0x05, 0x6E, 0xEB, 0x0D, 0x88, 0x6C, 0x39, 0x62, 0x01, 0x95, 0x82, 0xCF, 0xEE,
+            0x3A, 0x7F, 0x53, 0xDF, 0x09, 0x90, 0xF7, 0x06, 0xA4, 0x7A, 0x2D, 0xB3, 0xE7, 0xFA, 0x20, 0x48,
+            0x0F, 0x38, 0x34, 0xED, 0xBC, 0x8A, 0x96, 0xAB, 0x8E, 0xE3, 0xFF, 0xC6, 0xD2, 0xBF, 0xC0, 0x90,
+            0x06, 0x34, 0xDF, 0xF0, 0xDB, 0xDE, 0x27, 0x2E, 0xD5, 0x3C, 0xA2, 0x22, 0x72, 0xBD, 0x02, 0x0D,
+            0x1F, 0xB2, 0x99, 0xBE, 0x17, 0x26, 0xA1, 0xEF, 0x40, 0xF2, 0x61, 0xE1, 0x16, 0x17, 0xA4, 0xF4,
+            0x3A, 0x0F, 0x3C, 0x3A, 0xAB, 0x74, 0x83, 0x93, 0xB2, 0x09, 0x43, 0x52, 0x6E, 0xB8, 0xBF, 0xC8,
+            0x9C, 0x6A, 0x73, 0xD3, 0x0C, 0xC8, 0x5C, 0x71, 0xCD, 0x87, 0xCA, 0x28, 0xF6, 0xEB, 0x87, 0x60,
+            0x3D, 0xA5, 0x15, 0x9B, 0xAA, 0x99, 0x23, 0x9F, 0xD6, 0x2E, 0x79, 0x58, 0xE9, 0x8E, 0x54, 0xB0,
+            0xF8, 0x07, 0x6F, 0x6C, 0x52, 0xB7, 0xE2, 0x34, 0x42, 0x8C, 0x7A, 0xD5, 0xEC, 0xA4, 0xFE, 0x52,
+            0x9A, 0x05, 0x9F, 0xDD, 0x8D, 0x73, 0x8B, 0xA6, 0xDB, 0xA7, 0x84, 0xD0, 0xAB, 0xB7, 0xCC, 0x9E,
+            0x4B, 0xD8, 0xB2, 0xDC, 0x0F, 0xE8, 0x3A, 0x56, 0xB9, 0x63, 0x75, 0x1C, 0x7F, 0x89, 0xDF, 0x7C,
+            0x84, 0xE2, 0x8C, 0xA9, 0x0D, 0xA3, 0xDF, 0xF6, 0x3E, 0xC7, 0xCE, 0x1B, 0x24, 0x94, 0xB8, 0xE8,
+            0xD7, 0xDC, 0xA6, 0xEF, 0x85, 0xA1, 0x7D, 0x00, 0xE1, 0x78, 0xD4, 0x8B, 0x13, 0xCB, 0xB6, 0x4B,
+            0x5E, 0xCB, 0xF3, 0xC0, 0xA3, 0x09, 0x68, 0x68, 0x4C, 0xF4, 0x98, 0x0D, 0x38, 0x0D, 0xBF, 0xFB,
+            0x8B, 0xCC, 0x55, 0x71, 0x21, 0xC1, 0xFC, 0x3B, 0x60, 0x77, 0x9D, 0x3F, 0x54, 0x46, 0x61, 0x4A,
+            0xC8, 0xA5, 0xDB, 0x21, 0x8A, 0xCA, 0x73, 0x7D, 0x10, 0xF9, 0xB4, 0xD6, 0x9E, 0x15, 0x8E, 0x58,
+            0x94, 0x3C, 0xA9, 0xF1, 0x7F, 0x63, 0x93, 0xBA, 0xD5, 0x51, 0x35, 0xA1, 0x93, 0x93, 0xF5, 0xEE,
+            0x13, 0x97, 0xD2, 0x2C, 0xF8, 0x97, 0xFD, 0x98, 0x58, 0xD3, 0x6A, 0x8C, 0x2E, 0x4C, 0x42, 0xAF,
+            0xDE, 0x32, 0xC1, 0x4B, 0x5A, 0x61, 0x6D, 0xF9, 0xA3, 0xB3, 0xCA, 0x1D, 0xAB, 0x13, 0xE3, 0x14,
+            0xAC, 0xBB, 0xF3, 0x33, 0xA7, 0xDA, 0x30, 0xFA, 0xED, 0x40, 0xBB, 0x6A, 0x62, 0xC0, 0x30, 0x8A,
+            0xFD, 0x9A, 0xDB, 0xF4, 0x49, 0x7B, 0xA6, 0x3B, 0x17, 0x90, 0xD6, 0x2E, 0x79, 0x2D, 0xCF, 0x63,
+            0xE4, 0xB8, 0x1F, 0x5B, 0xD1, 0xDC, 0x8A, 0xD3, 0xF0, 0xBB, 0xBF, 0x73, 0xEF, 0x11, 0xE2, 0x0F,
+            0x29, 0xF8, 0xEC, 0xAE, 0xF3, 0x07, 0x5B, 0x11, 0x5F, 0x90, 0xB0, 0x53, 0xAE, 0x65, 0xF6, 0x5C,
+            0x1F, 0x44, 0x80, 0x4F, 0xC1, 0x83, 0x63, 0x9F, 0xE1, 0xAA, 0xE3, 0xF8, 0xBF, 0xB1, 0x51, 0x66,
+            0x19, 0x19, 0x13, 0xA0, 0xF7, 0x6D, 0xEF, 0x13, 0x97, 0x12, 0x75, 0xAC, 0xB7, 0x8C, 0x60, 0x3F,
+            0xC5, 0x71, 0x9B, 0xBE, 0x17, 0x26, 0xA1, 0x97, 0xB7, 0x0D, 0x6A, 0xE9, 0x28, 0x99, 0x68, 0x79,
+            0x1E, 0x78, 0x74, 0x56, 0x39, 0xF4, 0x5D, 0x75, 0x23, 0x7A, 0xB6, 0xEF, 0xFE, 0x22, 0x73, 0xAA,
+            0x0D, 0xE5, 0x01, 0x5A, 0xD0, 0x89, 0x2A, 0xE7, 0x0F, 0x95, 0x51, 0xEC, 0xD7, 0xE4, 0x2F, 0x7C,
+            0x4B, 0xAC, 0xEC, 0x3D, 0x88, 0x7C, 0x5A, 0xBB, 0xE4, 0xD5, 0x50, 0x41, 0x56, 0xC5, 0xBC, 0x7C,
+            0x63, 0x93, 0xBA, 0x15, 0xA7, 0x61, 0xC8, 0x47, 0xFA, 0x65, 0x1B, 0x07, 0x97, 0xD2, 0x2C, 0xF8,
+            0xEC, 0xAE, 0x35, 0x29, 0x6E, 0xDA, 0x0E, 0x6D, 0x84, 0x5E, 0xBD, 0x65, 0xF6, 0x5C, 0x27, 0xCD,
+            0xCC, 0x73, 0x80, 0xF6, 0xB2, 0xCA, 0x1D, 0xAB, 0xE3, 0xF8, 0xDF, 0xD5, 0x83, 0xF7, 0x15, 0xE4,
+            0x50, 0x6D, 0x18, 0xFD, 0xB6, 0xF7, 0x09, 0xDC, 0x51, 0x7F, 0xA0, 0xB8, 0x57, 0xB0, 0x5F, 0x73,
+            0x9B, 0xBE, 0x17, 0x26, 0x42, 0x42, 0xC4, 0x83, 0xAF, 0xE9, 0x92, 0xD7, 0xF2, 0x3C, 0xF0, 0xE8,
+            0x30, 0x1D, 0x1B, 0x94, 0xE0, 0x47, 0x9C, 0x86, 0xDF, 0xFD, 0x45, 0xE6, 0x64, 0xC5, 0x94, 0x64,
+            0x8C, 0xA4, 0xB3, 0xBB, 0xCE, 0x1F, 0x2A, 0xA3, 0x18, 0x58, 0xF4, 0xE2, 0x59, 0xA6, 0xD8, 0x73,
+            0x7D, 0x10, 0xF9, 0xB4, 0x76, 0x6A, 0x56, 0xCE, 0xD8, 0x15, 0xC7, 0xFF, 0x8D, 0x4D, 0xEA, 0x56,
+            0xA4, 0xDB, 0x86, 0x50, 0xD5, 0x99, 0xBD, 0x4F, 0x5C, 0x4A, 0xB3, 0xE0, 0xD3, 0x0F, 0x6C, 0x6A,
+            0x69, 0x71, 0x7B, 0x21, 0xF4, 0xEA, 0x2D, 0xB3, 0x08, 0xE5, 0x95, 0xEC, 0xDB, 0x03, 0x1E, 0xAB,
+            0xDC, 0xB1, 0x3A, 0x96, 0x50, 0xC3, 0x6E, 0x64, 0x41, 0x91, 0xA9, 0x0D, 0xA3, 0xDF, 0x36, 0x27,
+            0xEA, 0x5D, 0xE3, 0xA5, 0x0F, 0xCA, 0xE8, 0xD7, 0xDC, 0xA6, 0xEF, 0x26, 0x74, 0x5D, 0xC0, 0xCD,
+            0x78, 0x5A, 0xC9, 0x6B, 0x79, 0x1E, 0x80, 0xC9, 0xFF, 0x8C, 0x96, 0x79, 0x84, 0xBA, 0x4D, 0xC3,
+            0xEF, 0xFE, 0x42, 0xC7, 0x4F, 0x58, 0xE0, 0x2D, 0x59, 0xB0, 0xBB, 0xCE, 0x1F, 0x2A, 0x44, 0xC3,
+            0x04, 0xA4, 0xBF, 0xF1, 0x96, 0xE7, 0xFA, 0x20, 0xF2, 0x71, 0x42, 0x3A, 0x2A, 0x42, 0xD0, 0x58,
+            0x8D, 0xFF, 0x1B, 0x9B, 0x14, 0x56, 0x73, 0xA2, 0x39, 0x96, 0xD0, 0xEF, 0x3E, 0x71, 0x29, 0xCD,
+            0xC4, 0xA4, 0x98, 0x6F, 0x89, 0xE9, 0x54, 0xB5, 0xE9, 0xC2, 0x24, 0xF4, 0xEA, 0xB1, 0x5D, 0x3B,
+            0x64, 0x55, 0x44, 0x9E, 0x3F, 0x3A, 0xAB, 0xDC, 0xD1, 0x8E, 0x2B, 0x4A, 0xBF, 0x2C, 0x77, 0x3F,
+            0x73, 0xAA, 0x0D, 0xA3, 0x00, 0xE1, 0x93, 0x9B, 0xB6, 0xE1, 0x0F, 0xA3, 0xD8, 0xAF, 0xB9, 0x55,
+            0x30, 0xB3, 0xE6, 0x39, 0x50, 0xD0, 0xDA, 0x25, 0xAF, 0x65, 0x8A, 0x75, 0x0C, 0xEF, 0x53, 0xBD,
+            0x60, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEC, 0xBF, 0x70, 0xEF, 0xBF, 0xB0, 0xFB, 0x37, 0xF4, 0xFD,
+            0x0D, 0xDD, 0xDF, 0x85, 0xEF, 0xEF, 0x89, 0xF7, 0xFB, 0xC4, 0xFB, 0x3E, 0x78, 0xF7, 0x13, 0xDF,
+            0x7D, 0xC5, 0xB7, 0x5F, 0xF8, 0xF6, 0x0B, 0x5F, 0x7F, 0xE1, 0xED, 0x2F, 0xDC, 0xFD, 0x85, 0xBD,
+            0xDF, 0xD0, 0xF7, 0xDF, 0xC1, 0xF7, 0x77, 0xF0, 0x7D, 0x0F, 0xBE, 0xEF, 0x83, 0xEF, 0xFB, 0xE0,
+            0xBD, 0x1F, 0xBC, 0xF7, 0x0B, 0x77, 0xBF, 0x70, 0xF7, 0x0B, 0xD7, 0xBF, 0x70, 0xFD, 0x0B, 0xD7,
+            0xBF, 0xB0, 0xFD, 0x1D, 0xBA, 0xDF, 0x83, 0xF7, 0x7B, 0x70, 0xDF, 0x87, 0xDE, 0xF7, 0x83, 0xFB,
+            0xFE, 0xE0, 0xDE, 0x2F, 0xDC, 0xFD, 0x85, 0xDB, 0xDF, 0x70, 0xFB, 0x1B, 0xAE, 0x7F, 0xC3, 0xF5,
+            0x6F, 0xD8, 0xFE, 0x0D, 0xDB, 0xDF, 0xA1, 0xFB, 0x3B, 0x78, 0xBF, 0x07, 0xF7, 0xF7, 0xE0, 0x7E,
+            0x1F, 0xDC, 0xF7, 0x83, 0x7B, 0x3F, 0xB8, 0xF7, 0x07, 0x77, 0xBF, 0x70, 0xFB, 0x0B, 0xD7, 0xBF,
+            0xF0, 0xFA, 0x17, 0xB6, 0xBF, 0x61, 0xF7, 0x37, 0x74, 0xBF, 0x83, 0xF7, 0x3D, 0xB8, 0xDF, 0x83,
+            0xFB, 0x3E, 0x78, 0xDF, 0x0F, 0xDE, 0xFD, 0xE0, 0xDD, 0x17, 0xDE, 0x7E, 0xE1, 0xF5, 0x0B, 0x0F,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x20, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xA8, 0x01, 0x00,
+            0x00, 0x53, 0x02, 0x00, 0x00, 0xFD, 0x02, 0x00, 0x00, 0x86, 0x03, 0x00, 0x00, 0x88, 0x03, 0x00,
+            0x00, 0x89, 0x03, 0x00, 0x00, 0x89, 0x03, 0x00, 0x00, 0x89, 0x03, 0x00, 0x00, 0x89, 0x03, 0x00,
+            0x00, 0x89, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x27, 0x3B, 0x00, 0x18, 0x2B, 0x42, 0x57, 0x6D, 0x81,
+            0x98, 0x00, 0x17, 0x2B, 0x42, 0x56, 0x6D, 0x80, 0x97, 0x00, 0x17, 0x2B, 0x43, 0x56, 0x6D, 0x80,
+            0x97, 0x00, 0x16, 0x2B, 0x40, 0x55, 0x69, 0x80, 0x94, 0x00, 0x13, 0x29, 0x3E, 0x52, 0x68, 0x7C,
+            0x89, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01,
+            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1C, 0x00,
+            0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2D, 0x00,
+            0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x89, 0x03, 0x00, 0x00, 0x01, 0x80,
+            0x00, 0x00, 0x80, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00,
+            0x10, 0x00, 0x00, 0x02, 0x00, 0x80, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x40, 0x00, 0x00,
+            0x20, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x40, 0x00, 0x00,
+            0x20, 0x00, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x40, 0x00, 0x00,
+            0x20, 0x00, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0x08, 0x00, 0x00, 0x02, 0x00, 0x40, 0x00, 0x00,
+            0x08, 0x00, 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x80, 0x00, 0x00, 0x20, 0x00, 0x00,
+            0x01, 0x00, 0x40, 0x00, 0x00, 0x08, 0x00, 0x80, 0x00, 0x00, 0x20, 0x00, 0x00, 0x02, 0x40, 0x01,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00,
+            0x19, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x05, 0x07, 0x08, 0x0A, 0x0B,
+            0x00, 0x01, 0x02, 0x04, 0x06, 0x07, 0x09, 0x0A, 0x00, 0x01, 0x03, 0x04, 0x06, 0x07, 0x09, 0x0A,
+            0x00, 0x01, 0x03, 0x04, 0x06, 0x14, 0x07, 0x00, 0x00, 0xAB, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
+            0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
+            0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
+            0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
+            0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
+            0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
+            0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
+            0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
+            0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
+            0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
+            0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
+            0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
+            0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
+            0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
+            0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0x02, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x81, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x81, 0x01, 0x00, 0x00, 0x01, 0x02, 0x00,
+            0x00, 0x81, 0x02, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x81, 0x03, 0x00, 0x00, 0x00, 0x11, 0x21,
+            0x31, 0x41, 0x51, 0x61, 0x71, 0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x00, 0x10, 0x20,
+            0x30, 0x40, 0x50, 0x60, 0x70, 0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x00, 0x10, 0x20,
+            0x30, 0x40, 0x50, 0x60, 0x70, 0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x00, 0x10, 0x20,
+            0x30, 0x40, 0x50, 0x60, 0x70, 0x00, 0x01, 0x14, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x72,
+            0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x38, 0x8E, 0xE3, 0x38, 0x8E,
+            0xE3, 0x38, 0x8E, 0xE3, 0x38, 0x8E, 0xE3, 0x38, 0x8E, 0xE3, 0x38, 0x8E, 0xE3, 0x38, 0x8E, 0xE3,
+            0x38, 0x8E, 0xE3, 0x38, 0x8E, 0xE3, 0x38, 0x8E, 0xE3, 0x38, 0x8E, 0xE3, 0x38, 0x8E, 0xE3, 0x38,
+            0x8E, 0xE3, 0x38, 0x8E, 0xE3, 0x18, 0x00, 0x00, 0x02, 0x00, 0x00, 0x51, 0x14, 0x45, 0x51, 0x14,
+            0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45,
+            0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51,
+            0x14, 0x45, 0x51, 0x14, 0x45, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x15, 0x20, 0x2B, 0x35, 0x40, 0x4B, 0x00,
+            0x0B, 0x16, 0x1D, 0x1D, 0x1D, 0x1D, 0x1D, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x72, 0x00, 0x00, 0x00,
+            0x01, 0x08, 0x20, 0x00, 0x01, 0x08, 0x20, 0x00, 0x01, 0x08, 0x20, 0x00, 0x01, 0x08, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x06, 0x09, 0x72, 0x00, 0x00,
+            0x00, 0xAB, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x21, 0x31, 0x01, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x38, 0x8E,
+            0x23, 0x00, 0x20, 0x00, 0x00, 0x00, 0x51, 0x14, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+            0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x2B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x8A, 0x03, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00,
+            0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00,
+            0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00,
+            0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00,
+            0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00,
+            0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00,
+            0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00,
+            0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x30, 0x00,
+            0x00, 0x00, 0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x00, 0x02, 0x04, 0x06, 0x08, 0x0A,
+            0x0C, 0x0E, 0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x00, 0x02, 0x04, 0x06, 0x08,
+        };
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/MatchCheckState.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/MatchCheckState.cs
new file mode 100644
index 000000000..4c014578e
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/MatchCheckState.cs
@@ -0,0 +1,16 @@
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    struct MatchCheckState
+    {
+        public uint CheckMask;
+        public readonly uint RegionMask;
+        public readonly ProfanityFilterOption Option;
+
+        public MatchCheckState(uint checkMask, uint regionMask, ProfanityFilterOption option)
+        {
+            CheckMask = checkMask;
+            RegionMask = regionMask;
+            Option = option;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/MatchDelimitedState.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/MatchDelimitedState.cs
new file mode 100644
index 000000000..d9b82d422
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/MatchDelimitedState.cs
@@ -0,0 +1,24 @@
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    struct MatchDelimitedState
+    {
+        public bool Matched;
+        public readonly bool PrevCharIsWordSeparator;
+        public readonly bool NextCharIsWordSeparator;
+        public readonly Sbv NoSeparatorMap;
+        public readonly AhoCorasick DelimitedWordsTrie;
+
+        public MatchDelimitedState(
+            bool prevCharIsWordSeparator,
+            bool nextCharIsWordSeparator,
+            Sbv noSeparatorMap,
+            AhoCorasick delimitedWordsTrie)
+        {
+            Matched = false;
+            PrevCharIsWordSeparator = prevCharIsWordSeparator;
+            NextCharIsWordSeparator = nextCharIsWordSeparator;
+            NoSeparatorMap = noSeparatorMap;
+            DelimitedWordsTrie = delimitedWordsTrie;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/MatchRangeList.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/MatchRangeList.cs
new file mode 100644
index 000000000..ad2ad7a90
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/MatchRangeList.cs
@@ -0,0 +1,113 @@
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    readonly struct MatchRange
+    {
+        public readonly int StartOffset;
+        public readonly int EndOffset;
+
+        public MatchRange(int startOffset, int endOffset)
+        {
+            StartOffset = startOffset;
+            EndOffset = endOffset;
+        }
+    }
+
+    struct MatchRangeList
+    {
+        private int _capacity;
+        private int _count;
+        private MatchRange[] _ranges;
+
+        public readonly int Count => _count;
+
+        public readonly MatchRange this[int index] => _ranges[index];
+
+        public MatchRangeList()
+        {
+            _capacity = 0;
+            _count = 0;
+            _ranges = Array.Empty<MatchRange>();
+        }
+
+        public void Add(int startOffset, int endOffset)
+        {
+            if (_count == _capacity)
+            {
+                int newCapacity = _count * 2;
+
+                if (newCapacity == 0)
+                {
+                    newCapacity = 1;
+                }
+
+                Array.Resize(ref _ranges, newCapacity);
+
+                _capacity = newCapacity;
+            }
+
+            _ranges[_count++] = new(startOffset, endOffset);
+        }
+
+        public readonly MatchRangeList Deduplicate()
+        {
+            MatchRangeList output = new();
+
+            if (_count != 0)
+            {
+                int prevStartOffset = _ranges[0].StartOffset;
+                int prevEndOffset = _ranges[0].EndOffset;
+
+                for (int index = 1; index < _count; index++)
+                {
+                    int currStartOffset = _ranges[index].StartOffset;
+                    int currEndOffset = _ranges[index].EndOffset;
+
+                    if (prevStartOffset == currStartOffset)
+                    {
+                        if (prevEndOffset <= currEndOffset)
+                        {
+                            prevEndOffset = currEndOffset;
+                        }
+                    }
+                    else if (prevEndOffset <= currStartOffset)
+                    {
+                        output.Add(prevStartOffset, prevEndOffset);
+
+                        prevStartOffset = currStartOffset;
+                        prevEndOffset = currEndOffset;
+                    }
+                }
+
+                output.Add(prevStartOffset, prevEndOffset);
+            }
+
+            return output;
+        }
+
+        public readonly int Find(int startOffset, int endOffset)
+        {
+            int baseIndex = 0;
+            int range = _count;
+
+            while (range != 0)
+            {
+                MatchRange currRange = _ranges[baseIndex + (range / 2)];
+
+                if (currRange.StartOffset < startOffset || (currRange.StartOffset == startOffset && currRange.EndOffset < endOffset))
+                {
+                    int nextHalf = (range / 2) + 1;
+                    baseIndex += nextHalf;
+                    range -= nextHalf;
+                }
+                else
+                {
+                    range /= 2;
+                }
+            }
+
+            return baseIndex;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/MatchRangeListState.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/MatchRangeListState.cs
new file mode 100644
index 000000000..44a63449e
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/MatchRangeListState.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    struct MatchRangeListState
+    {
+        public MatchRangeList MatchRanges;
+
+        public MatchRangeListState()
+        {
+            MatchRanges = new();
+        }
+
+        public static bool AddMatch(ReadOnlySpan<byte> text, int startOffset, int endOffset, int nodeId, ref MatchRangeListState state)
+        {
+            state.MatchRanges.Add(startOffset, endOffset);
+
+            return true;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/MatchSimilarFormState.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/MatchSimilarFormState.cs
new file mode 100644
index 000000000..eab9bf959
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/MatchSimilarFormState.cs
@@ -0,0 +1,18 @@
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    struct MatchSimilarFormState
+    {
+        public MatchRangeList MatchRanges;
+        public SimilarFormTable SimilarFormTable;
+        public Utf8Text CanonicalText;
+        public int ReplaceEndOffset;
+
+        public MatchSimilarFormState(MatchRangeList matchRanges, SimilarFormTable similarFormTable)
+        {
+            MatchRanges = matchRanges;
+            SimilarFormTable = similarFormTable;
+            CanonicalText = new();
+            ReplaceEndOffset = 0;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/MatchState.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/MatchState.cs
new file mode 100644
index 000000000..04fc1850f
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/MatchState.cs
@@ -0,0 +1,49 @@
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    readonly ref struct MatchState
+    {
+        public readonly Span<byte> OriginalText;
+        public readonly Span<byte> ConvertedText;
+        public readonly ReadOnlySpan<sbyte> DeltaTable;
+        public readonly ref int MaskedCount;
+        public readonly MaskMode MaskMode;
+        public readonly Sbv NoSeparatorMap;
+        public readonly AhoCorasick DelimitedWordsTrie;
+
+        public MatchState(
+            Span<byte> originalText,
+            Span<byte> convertedText,
+            ReadOnlySpan<sbyte> deltaTable,
+            ref int maskedCount,
+            MaskMode maskMode,
+            Sbv noSeparatorMap = null,
+            AhoCorasick delimitedWordsTrie = null)
+        {
+            OriginalText = originalText;
+            ConvertedText = convertedText;
+            DeltaTable = deltaTable;
+            MaskedCount = ref maskedCount;
+            MaskMode = maskMode;
+            NoSeparatorMap = noSeparatorMap;
+            DelimitedWordsTrie = delimitedWordsTrie;
+        }
+
+        public readonly (int, int) GetOriginalRange(int convertedStartOffest, int convertedEndOffset)
+        {
+            int originalStartOffset = 0;
+            int originalEndOffset = 0;
+
+            for (int index = 0; index < convertedEndOffset; index++)
+            {
+                int byteLength = Math.Abs(DeltaTable[index]);
+
+                originalStartOffset += index < convertedStartOffest ? byteLength : 0;
+                originalEndOffset += byteLength;
+            }
+
+            return (originalStartOffset, originalEndOffset);
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/ProfanityFilter.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/ProfanityFilter.cs
new file mode 100644
index 000000000..980126a6f
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/ProfanityFilter.cs
@@ -0,0 +1,886 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Fs;
+using System;
+using System.Buffers.Binary;
+using System.Numerics;
+using System.Text;
+
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    class ProfanityFilter : ProfanityFilterBase, IDisposable
+    {
+        private const int MaxBufferLength = 0x800;
+        private const int MaxUtf8CharacterLength = 4;
+        private const int MaxUtf8Characters = MaxBufferLength / MaxUtf8CharacterLength;
+        private const int RegionsCount = 16;
+        private const int MountCacheSize = 0x2000;
+
+        private readonly ContentsReader _contentsReader;
+
+        public ProfanityFilter(IFsClient fsClient)
+        {
+            _contentsReader = new(fsClient);
+        }
+
+        public Result Initialize()
+        {
+            return _contentsReader.Initialize(MountCacheSize);
+        }
+
+        public override Result Reload()
+        {
+            return _contentsReader.Reload();
+        }
+
+        public override Result GetContentVersion(out uint version)
+        {
+            version = 0;
+
+            Result result = _contentsReader.GetVersionDataSize(out long size);
+            if (result.IsFailure && size != 4)
+            {
+                return Result.Success;
+            }
+
+            Span<byte> data = stackalloc byte[4];
+            result = _contentsReader.GetVersionData(data);
+            if (result.IsFailure)
+            {
+                return Result.Success;
+            }
+
+            version = BinaryPrimitives.ReadUInt32BigEndian(data);
+
+            return Result.Success;
+        }
+
+        public override Result CheckProfanityWords(out uint checkMask, ReadOnlySpan<byte> word, uint regionMask, ProfanityFilterOption option)
+        {
+            checkMask = 0;
+
+            int length = word.IndexOf((byte)0);
+            if (length >= 0)
+            {
+                word = word[..length];
+            }
+
+            UTF8Encoding encoding = new(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
+
+            string decodedWord;
+
+            try
+            {
+                decodedWord = encoding.GetString(word);
+            }
+            catch (ArgumentException)
+            {
+                return NgcResult.InvalidUtf8Encoding;
+            }
+
+            return CheckProfanityWordsMultiRegionImpl(ref checkMask, decodedWord, regionMask, option);
+        }
+
+        private Result CheckProfanityWordsMultiRegionImpl(ref uint checkMask, string word, uint regionMask, ProfanityFilterOption option)
+        {
+            // Check using common dictionary.
+            Result result = CheckProfanityWordsImpl(ref checkMask, word, 0, option);
+            if (result.IsFailure)
+            {
+                return result;
+            }
+
+            if (checkMask != 0)
+            {
+                checkMask = (ushort)(regionMask | option.SystemRegionMask);
+            }
+
+            // Check using region specific dictionaries if needed.
+            for (int regionIndex = 0; regionIndex < RegionsCount; regionIndex++)
+            {
+                if (((regionMask | option.SystemRegionMask) & (1 << regionIndex)) != 0)
+                {
+                    result = CheckProfanityWordsImpl(ref checkMask, word, 1u << regionIndex, option);
+                    if (result.IsFailure)
+                    {
+                        return result;
+                    }
+                }
+            }
+
+            return Result.Success;
+        }
+
+        private Result CheckProfanityWordsImpl(ref uint checkMask, string word, uint regionMask, ProfanityFilterOption option)
+        {
+            ConvertUserInputForWord(out string convertedWord, word);
+
+            if (IsIncludesAtSign(convertedWord))
+            {
+                checkMask |= regionMask != 0 ? regionMask : option.SystemRegionMask;
+            }
+
+            byte[] utf8Text = Encoding.UTF8.GetBytes(convertedWord);
+            byte[] convertedText = new byte[utf8Text.Length + 5];
+
+            utf8Text.CopyTo(convertedText.AsSpan().Slice(2, utf8Text.Length));
+
+            convertedText[0] = (byte)'\\';
+            convertedText[1] = (byte)'b';
+            convertedText[2 + utf8Text.Length] = (byte)'\\';
+            convertedText[3 + utf8Text.Length] = (byte)'b';
+            convertedText[4 + utf8Text.Length] = 0;
+
+            int regionIndex = (ushort)regionMask != 0 ? BitOperations.TrailingZeroCount(regionMask) : -1;
+
+            Result result = _contentsReader.ReadDictionaries(out AhoCorasick partialWordsTrie, out _, out AhoCorasick delimitedWordsTrie, regionIndex);
+            if (result.IsFailure)
+            {
+                return result;
+            }
+
+            if ((checkMask & regionMask) == 0)
+            {
+                MatchCheckState state = new(checkMask, regionMask, option);
+
+                partialWordsTrie.Match(convertedText, MatchCheck, ref state);
+                delimitedWordsTrie.Match(convertedText, MatchCheck, ref state);
+
+                checkMask = state.CheckMask;
+            }
+
+            return Result.Success;
+        }
+
+        public override Result MaskProfanityWordsInText(out int maskedWordsCount, Span<byte> text, uint regionMask, ProfanityFilterOption option)
+        {
+            maskedWordsCount = 0;
+
+            Span<byte> output = text;
+            Span<byte> convertedText = new byte[MaxBufferLength];
+            Span<sbyte> deltaTable = new sbyte[MaxBufferLength];
+
+            int nullTerminatorIndex = GetUtf8Length(out _, text, MaxUtf8Characters);
+
+            // Ensure that the text has a null terminator if we can.
+            // If the text is too long, it will be truncated.
+            byte replacedCharacter = 0;
+
+            if (nullTerminatorIndex > 0 && nullTerminatorIndex < text.Length)
+            {
+                replacedCharacter = text[nullTerminatorIndex];
+                text[nullTerminatorIndex] = 0;
+            }
+
+            // Truncate the text if needed.
+            int length = text.IndexOf((byte)0);
+            if (length >= 0)
+            {
+                text = text[..length];
+            }
+
+            // If requested, mask e-mail addresses.
+            if (option.SkipAtSignCheck == SkipMode.DoNotSkip)
+            {
+                maskedWordsCount += FilterAtSign(text, option.MaskMode);
+                text = MaskText(text);
+            }
+
+            // Convert the text to lower case, required for string matching.
+            ConvertUserInputForText(convertedText, deltaTable, text);
+
+            // Mask words for common and requested regions.
+            Result result = MaskProfanityWordsInTextMultiRegion(ref maskedWordsCount, ref text, ref convertedText, deltaTable, regionMask, option);
+            if (result.IsFailure)
+            {
+                return result;
+            }
+
+            // If requested, also try to match and mask the canonicalized string.
+            if (option.Flags != ProfanityFilterFlags.None)
+            {
+                result = MaskProfanityWordsInTextCanonicalizedMultiRegion(ref maskedWordsCount, text, regionMask, option);
+                if (result.IsFailure)
+                {
+                    return result;
+                }
+            }
+
+            // If we received more text than we can process, copy unprocessed portion to the end of the new text.
+            if (replacedCharacter != 0)
+            {
+                length = text.IndexOf((byte)0);
+
+                if (length < 0)
+                {
+                    length = text.Length;
+                }
+
+                output[length++] = replacedCharacter;
+                int unprocessedLength = output.Length - nullTerminatorIndex - 1;
+                output.Slice(nullTerminatorIndex + 1, unprocessedLength).CopyTo(output.Slice(length, unprocessedLength));
+            }
+
+            return Result.Success;
+        }
+
+        private Result MaskProfanityWordsInTextMultiRegion(
+            ref int maskedWordsCount,
+            ref Span<byte> originalText,
+            ref Span<byte> convertedText,
+            Span<sbyte> deltaTable,
+            uint regionMask,
+            ProfanityFilterOption option)
+        {
+            // Filter using common dictionary.
+            Result result = MaskProfanityWordsInTextImpl(ref maskedWordsCount, ref originalText, ref convertedText, deltaTable, -1, option);
+            if (result.IsFailure)
+            {
+                return result;
+            }
+
+            // Filter using region specific dictionaries if needed.
+            for (int regionIndex = 0; regionIndex < RegionsCount; regionIndex++)
+            {
+                if (((regionMask | option.SystemRegionMask) & (1 << regionIndex)) != 0)
+                {
+                    result = MaskProfanityWordsInTextImpl(ref maskedWordsCount, ref originalText, ref convertedText, deltaTable, regionIndex, option);
+                    if (result.IsFailure)
+                    {
+                        return result;
+                    }
+                }
+            }
+
+            return Result.Success;
+        }
+
+        private Result MaskProfanityWordsInTextImpl(
+            ref int maskedWordsCount,
+            ref Span<byte> originalText,
+            ref Span<byte> convertedText,
+            Span<sbyte> deltaTable,
+            int regionIndex,
+            ProfanityFilterOption option)
+        {
+            Result result = _contentsReader.ReadDictionaries(
+                out AhoCorasick partialWordsTrie,
+                out AhoCorasick completeWordsTrie,
+                out AhoCorasick delimitedWordsTrie,
+                regionIndex);
+
+            if (result.IsFailure)
+            {
+                return result;
+            }
+
+            // Match single words.
+
+            MatchState state = new(originalText, convertedText, deltaTable, ref maskedWordsCount, option.MaskMode);
+
+            partialWordsTrie.Match(convertedText, MatchSingleWord, ref state);
+
+            MaskText(ref originalText, ref convertedText, deltaTable);
+
+            // Match single words and phrases.
+            // We remove word separators on the string used for the match.
+
+            Span<byte> noSeparatorText = new byte[originalText.Length];
+            Sbv noSeparatorMap = new(convertedText.Length);
+            noSeparatorText = RemoveWordSeparators(noSeparatorText, convertedText, noSeparatorMap);
+
+            state = new(
+                originalText,
+                convertedText,
+                deltaTable,
+                ref maskedWordsCount,
+                option.MaskMode,
+                noSeparatorMap,
+                delimitedWordsTrie);
+
+            partialWordsTrie.Match(noSeparatorText, MatchMultiWord, ref state);
+
+            MaskText(ref originalText, ref convertedText, deltaTable);
+
+            // Match whole words, which must be surrounded by word separators.
+
+            noSeparatorText = new byte[originalText.Length];
+            noSeparatorMap = new(convertedText.Length);
+            noSeparatorText = RemoveWordSeparators(noSeparatorText, convertedText, noSeparatorMap);
+
+            state = new(
+                originalText,
+                convertedText,
+                deltaTable,
+                ref maskedWordsCount,
+                option.MaskMode,
+                noSeparatorMap,
+                delimitedWordsTrie);
+
+            completeWordsTrie.Match(noSeparatorText, MatchDelimitedWord, ref state);
+
+            MaskText(ref originalText, ref convertedText, deltaTable);
+
+            return Result.Success;
+        }
+
+        private static void MaskText(ref Span<byte> originalText, ref Span<byte> convertedText, Span<sbyte> deltaTable)
+        {
+            originalText = MaskText(originalText);
+            UpdateDeltaTable(deltaTable, convertedText);
+            convertedText = MaskText(convertedText);
+        }
+
+        private Result MaskProfanityWordsInTextCanonicalizedMultiRegion(ref int maskedWordsCount, Span<byte> text, uint regionMask, ProfanityFilterOption option)
+        {
+            // Filter using common dictionary.
+            Result result = MaskProfanityWordsInTextCanonicalized(ref maskedWordsCount, text, 0, option);
+            if (result.IsFailure)
+            {
+                return result;
+            }
+
+            // Filter using region specific dictionaries if needed.
+            for (int index = 0; index < RegionsCount; index++)
+            {
+                if ((((regionMask | option.SystemRegionMask) >> index) & 1) != 0)
+                {
+                    result = MaskProfanityWordsInTextCanonicalized(ref maskedWordsCount, text, 1u << index, option);
+                    if (result.IsFailure)
+                    {
+                        return result;
+                    }
+                }
+            }
+
+            return Result.Success;
+        }
+
+        private Result MaskProfanityWordsInTextCanonicalized(ref int maskedWordsCount, Span<byte> text, uint regionMask, ProfanityFilterOption option)
+        {
+            Utf8Text maskedText = new();
+            Utf8ParseResult parseResult = Utf8Text.Create(out Utf8Text inputText, text);
+            if (parseResult != Utf8ParseResult.Success)
+            {
+                return NgcResult.InvalidUtf8Encoding;
+            }
+
+            ReadOnlySpan<byte> prevCharacter = ReadOnlySpan<byte>.Empty;
+
+            int charStartIndex = 0;
+
+            for (int charEndIndex = 1; charStartIndex < inputText.CharacterCount;)
+            {
+                ReadOnlySpan<byte> nextCharacter = charEndIndex < inputText.CharacterCount
+                    ? inputText.AsSubstring(charEndIndex, charEndIndex + 1)
+                    : ReadOnlySpan<byte>.Empty;
+
+                Result result = CheckProfanityWordsInTextCanonicalized(
+                    out bool matched,
+                    inputText.AsSubstring(charStartIndex, charEndIndex),
+                    prevCharacter,
+                    nextCharacter,
+                    regionMask,
+                    option);
+
+                if (result.IsFailure && result != NgcResult.InvalidSize)
+                {
+                    return result;
+                }
+
+                if (matched)
+                {
+                    // We had a match, we know where it ends, now we need to find where it starts.
+
+                    int previousCharStartIndex = charStartIndex;
+
+                    for (; charStartIndex < charEndIndex; charStartIndex++)
+                    {
+                        result = CheckProfanityWordsInTextCanonicalized(
+                            out matched,
+                            inputText.AsSubstring(charStartIndex, charEndIndex),
+                            prevCharacter,
+                            nextCharacter,
+                            regionMask,
+                            option);
+
+                        if (result.IsFailure && result != NgcResult.InvalidSize)
+                        {
+                            return result;
+                        }
+
+                        // When we get past the start of the matched substring, the match will fail,
+                        // so that's when we know we found the start.
+                        if (!matched)
+                        {
+                            break;
+                        }
+                    }
+
+                    // Append substring before the match start.
+                    maskedText = maskedText.Append(inputText.AsSubstring(previousCharStartIndex, charStartIndex - 1));
+
+                    // Mask matched substring with asterisks.
+                    if (option.MaskMode == MaskMode.ReplaceByOneCharacter)
+                    {
+                        maskedText = maskedText.Append("*"u8);
+                        prevCharacter = "*"u8;
+                    }
+                    else if (option.MaskMode == MaskMode.Overwrite && charStartIndex <= charEndIndex)
+                    {
+                        int maskLength = charEndIndex - charStartIndex + 1;
+
+                        while (maskLength-- > 0)
+                        {
+                            maskedText = maskedText.Append("*"u8);
+                        }
+
+                        prevCharacter = "*"u8;
+                    }
+
+                    charStartIndex = charEndIndex;
+                    maskedWordsCount++;
+                }
+
+                if (charEndIndex < inputText.CharacterCount)
+                {
+                    charEndIndex++;
+                }
+                else if (charStartIndex < inputText.CharacterCount)
+                {
+                    prevCharacter = inputText.AsSubstring(charStartIndex, charStartIndex + 1);
+                    maskedText = maskedText.Append(prevCharacter);
+                    charStartIndex++;
+                }
+            }
+
+            // Replace text with the masked text.
+            maskedText.CopyTo(text);
+
+            return Result.Success;
+        }
+
+        private Result CheckProfanityWordsInTextCanonicalized(
+            out bool matched,
+            ReadOnlySpan<byte> text,
+            ReadOnlySpan<byte> prevCharacter,
+            ReadOnlySpan<byte> nextCharacter,
+            uint regionMask,
+            ProfanityFilterOption option)
+        {
+            matched = false;
+
+            Span<byte> convertedText = new byte[MaxBufferLength + 1];
+            text.CopyTo(convertedText[..text.Length]);
+
+            Result result;
+
+            if (text.Length > 0)
+            {
+                // If requested, normalize.
+                // This will convert different encodings for the same character in their canonical encodings.
+                if (option.Flags.HasFlag(ProfanityFilterFlags.MatchNormalizedFormKC))
+                {
+                    Utf8ParseResult parseResult = Utf8Util.NormalizeFormKC(convertedText, convertedText);
+
+                    if (parseResult != Utf8ParseResult.Success)
+                    {
+                        return NgcResult.InvalidUtf8Encoding;
+                    }
+                }
+
+                // Convert to lower case.
+                ConvertUserInputForText(convertedText, Span<sbyte>.Empty, convertedText);
+
+                // If requested, also try to replace similar characters with their canonical form.
+                // For example, vv is similar to w, and 1 or | is similar to i.
+                if (option.Flags.HasFlag(ProfanityFilterFlags.MatchSimilarForm))
+                {
+                    result = ConvertInputTextFromSimilarForm(convertedText, convertedText);
+                    if (result.IsFailure)
+                    {
+                        return result;
+                    }
+                }
+
+                int length = convertedText.IndexOf((byte)0);
+                if (length >= 0)
+                {
+                    convertedText = convertedText[..length];
+                }
+            }
+
+            int regionIndex = (ushort)regionMask != 0 ? BitOperations.TrailingZeroCount(regionMask) : -1;
+
+            result = _contentsReader.ReadDictionaries(
+                out AhoCorasick partialWordsTrie,
+                out AhoCorasick completeWordsTrie,
+                out AhoCorasick delimitedWordsTrie,
+                regionIndex);
+
+            if (result.IsFailure)
+            {
+                return result;
+            }
+
+            result = ContentsReader.ReadNotSeparatorDictionary(out AhoCorasick notSeparatorTrie);
+            if (result.IsFailure)
+            {
+                return result;
+            }
+
+            // Match single words.
+
+            bool trieMatched = false;
+
+            partialWordsTrie.Match(convertedText, MatchSimple, ref trieMatched);
+
+            if (trieMatched)
+            {
+                matched = true;
+
+                return Result.Success;
+            }
+
+            // Match single words and phrases.
+            // We remove word separators on the string used for the match.
+
+            Span<byte> noSeparatorText = new byte[text.Length];
+            Sbv noSeparatorMap = new(convertedText.Length);
+            noSeparatorText = RemoveWordSeparators(noSeparatorText, convertedText, noSeparatorMap, notSeparatorTrie);
+
+            trieMatched = false;
+
+            partialWordsTrie.Match(noSeparatorText, MatchSimple, ref trieMatched);
+
+            if (trieMatched)
+            {
+                matched = true;
+
+                return Result.Success;
+            }
+
+            // Match whole words, which must be surrounded by word separators.
+
+            bool prevCharIsWordSeparator = prevCharacter.Length == 0 || IsWordSeparator(prevCharacter, notSeparatorTrie);
+            bool nextCharIsWordSeparator = nextCharacter.Length == 0 || IsWordSeparator(nextCharacter, notSeparatorTrie);
+
+            MatchDelimitedState state = new(prevCharIsWordSeparator, nextCharIsWordSeparator, noSeparatorMap, delimitedWordsTrie);
+
+            completeWordsTrie.Match(noSeparatorText, MatchDelimitedWordSimple, ref state);
+
+            if (state.Matched)
+            {
+                matched = true;
+            }
+
+            return Result.Success;
+        }
+
+        private Result ConvertInputTextFromSimilarForm(Span<byte> convertedText, ReadOnlySpan<byte> text)
+        {
+            int length = text.IndexOf((byte)0);
+            if (length >= 0)
+            {
+                text = text[..length];
+            }
+
+            Result result = _contentsReader.ReadSimilarFormDictionary(out AhoCorasick similarFormTrie);
+            if (result.IsFailure)
+            {
+                return result;
+            }
+
+            result = _contentsReader.ReadSimilarFormTable(out SimilarFormTable similarFormTable);
+            if (result.IsFailure)
+            {
+                return result;
+            }
+
+            // Find all characters that have a similar form.
+            MatchRangeListState listState = new();
+
+            similarFormTrie.Match(text, MatchRangeListState.AddMatch, ref listState);
+
+            // Filter found match ranges.
+            // Because some similar form strings are a subset of others, we need to remove overlapping matches.
+            // For example, | can be replaced with i, but |-| can be replaced with h.
+            // We prefer the latter match (|-|) because it is more specific.
+            MatchRangeList deduplicatedMatches = listState.MatchRanges.Deduplicate();
+
+            MatchSimilarFormState state = new(deduplicatedMatches, similarFormTable);
+
+            similarFormTrie.Match(text, MatchAndReplace, ref state);
+
+            // Append remaining characters.
+            state.CanonicalText = state.CanonicalText.Append(text[state.ReplaceEndOffset..]);
+
+            // Set canonical text to output.
+            ReadOnlySpan<byte> canonicalText = state.CanonicalText.AsSpan();
+            canonicalText.CopyTo(convertedText[..canonicalText.Length]);
+            convertedText[canonicalText.Length] = 0;
+
+            return Result.Success;
+        }
+
+        private static bool MatchCheck(ReadOnlySpan<byte> text, int matchStartOffset, int matchEndOffset, int nodeId, ref MatchCheckState state)
+        {
+            state.CheckMask |= state.RegionMask != 0 ? state.RegionMask : state.Option.SystemRegionMask;
+
+            return true;
+        }
+
+        private static bool MatchSingleWord(ReadOnlySpan<byte> text, int matchStartOffset, int matchEndOffset, int nodeId, ref MatchState state)
+        {
+            MatchCommon(ref state, matchStartOffset, matchEndOffset);
+
+            return true;
+        }
+
+        private static bool MatchMultiWord(ReadOnlySpan<byte> text, int matchStartOffset, int matchEndOffset, int nodeId, ref MatchState state)
+        {
+            int convertedStartOffset = state.NoSeparatorMap.Set.Select0(matchStartOffset);
+            int convertedEndOffset = state.NoSeparatorMap.Set.Select0(matchEndOffset);
+
+            if (convertedEndOffset < 0)
+            {
+                convertedEndOffset = state.NoSeparatorMap.Set.BitVector.BitLength;
+            }
+
+            int endOffsetBeforeSeparator = TrimEnd(state.ConvertedText, convertedEndOffset);
+
+            MatchCommon(ref state, convertedStartOffset, endOffsetBeforeSeparator);
+
+            return true;
+        }
+
+        private static bool MatchDelimitedWord(ReadOnlySpan<byte> text, int matchStartOffset, int matchEndOffset, int nodeId, ref MatchState state)
+        {
+            int convertedStartOffset = state.NoSeparatorMap.Set.Select0(matchStartOffset);
+            int convertedEndOffset = state.NoSeparatorMap.Set.Select0(matchEndOffset);
+
+            if (convertedEndOffset < 0)
+            {
+                convertedEndOffset = state.NoSeparatorMap.Set.BitVector.BitLength;
+            }
+
+            int endOffsetBeforeSeparator = TrimEnd(state.ConvertedText, convertedEndOffset);
+
+            Span<byte> delimitedText = new byte[64];
+
+            // If the word is prefixed by a word separator, insert "\b" delimiter, otherwise insert "a" delimitar.
+            // The start of the string is also considered a "word separator".
+
+            bool startIsPrefixedByWordSeparator =
+                convertedStartOffset == 0 ||
+                IsPrefixedByWordSeparator(state.ConvertedText, convertedStartOffset);
+
+            int delimitedTextOffset = 0;
+
+            if (startIsPrefixedByWordSeparator)
+            {
+                delimitedText[delimitedTextOffset++] = (byte)'\\';
+                delimitedText[delimitedTextOffset++] = (byte)'b';
+            }
+            else
+            {
+                delimitedText[delimitedTextOffset++] = (byte)'a';
+            }
+
+            // Copy the word to our temporary buffer used for the next match.
+
+            int matchLength = matchEndOffset - matchStartOffset;
+
+            text.Slice(matchStartOffset, matchLength).CopyTo(delimitedText.Slice(delimitedTextOffset, matchLength));
+
+            delimitedTextOffset += matchLength;
+
+            // If the word is suffixed by a word separator, insert "\b" delimiter, otherwise insert "a" delimiter.
+            // The end of the string is also considered a "word separator".
+
+            bool endIsSuffixedByWordSeparator =
+                endOffsetBeforeSeparator == state.NoSeparatorMap.Set.BitVector.BitLength ||
+                state.ConvertedText[endOffsetBeforeSeparator] == 0 ||
+                IsWordSeparator(state.ConvertedText, endOffsetBeforeSeparator);
+
+            if (endIsSuffixedByWordSeparator)
+            {
+                delimitedText[delimitedTextOffset++] = (byte)'\\';
+                delimitedText[delimitedTextOffset++] = (byte)'b';
+            }
+            else
+            {
+                delimitedText[delimitedTextOffset++] = (byte)'a';
+            }
+
+            // Create our temporary match state for the next match.
+            bool matched = false;
+
+            // Insert the null terminator.
+            delimitedText[delimitedTextOffset] = 0;
+
+            // Check if the delimited word is on the dictionary.
+            state.DelimitedWordsTrie.Match(delimitedText, MatchSimple, ref matched);
+
+            // If we have a match, mask the word.
+            if (matched)
+            {
+                MatchCommon(ref state, convertedStartOffset, endOffsetBeforeSeparator);
+            }
+
+            return true;
+        }
+
+        private static void MatchCommon(ref MatchState state, int matchStartOffset, int matchEndOffset)
+        {
+            // If length is zero or negative, there was no match.
+            if (matchStartOffset >= matchEndOffset)
+            {
+                return;
+            }
+
+            Span<byte> convertedText = state.ConvertedText;
+            Span<byte> originalText = state.OriginalText;
+
+            int matchLength = matchEndOffset - matchStartOffset;
+            int characterCount = Encoding.UTF8.GetCharCount(state.ConvertedText.Slice(matchStartOffset, matchLength));
+
+            // Exit early if there are no character, or if we matched past the end of the string.
+            if (characterCount == 0 ||
+                (matchStartOffset > 0 && convertedText[matchStartOffset - 1] == 0) ||
+                (matchStartOffset > 1 && convertedText[matchStartOffset - 2] == 0))
+            {
+                return;
+            }
+
+            state.MaskedCount++;
+
+            (int originalStartOffset, int originalEndOffset) = state.GetOriginalRange(matchStartOffset, matchEndOffset);
+
+            PreMaskCharacterRange(convertedText, matchStartOffset, matchEndOffset, state.MaskMode, characterCount);
+            PreMaskCharacterRange(originalText, originalStartOffset, originalEndOffset, state.MaskMode, characterCount);
+        }
+
+        private static bool MatchDelimitedWordSimple(ReadOnlySpan<byte> text, int matchStartOffset, int matchEndOffset, int nodeId, ref MatchDelimitedState state)
+        {
+            int convertedStartOffset = state.NoSeparatorMap.Set.Select0(matchStartOffset);
+
+            Span<byte> delimitedText = new byte[64];
+
+            // If the word is prefixed by a word separator, insert "\b" delimiter, otherwise insert "a" delimitar.
+            // The start of the string is also considered a "word separator".
+
+            bool startIsPrefixedByWordSeparator =
+                (convertedStartOffset == 0 && state.PrevCharIsWordSeparator) ||
+                state.NoSeparatorMap.Set.Has(convertedStartOffset - 1);
+
+            int delimitedTextOffset = 0;
+
+            if (startIsPrefixedByWordSeparator)
+            {
+                delimitedText[delimitedTextOffset++] = (byte)'\\';
+                delimitedText[delimitedTextOffset++] = (byte)'b';
+            }
+            else
+            {
+                delimitedText[delimitedTextOffset++] = (byte)'a';
+            }
+
+            // Copy the word to our temporary buffer used for the next match.
+
+            int matchLength = matchEndOffset - matchStartOffset;
+
+            text.Slice(matchStartOffset, matchLength).CopyTo(delimitedText.Slice(delimitedTextOffset, matchLength));
+
+            delimitedTextOffset += matchLength;
+
+            // If the word is suffixed by a word separator, insert "\b" delimiter, otherwise insert "a" delimiter.
+            // The end of the string is also considered a "word separator".
+
+            int convertedEndOffset = state.NoSeparatorMap.Set.Select0(matchEndOffset);
+
+            bool endIsSuffixedByWordSeparator =
+                (convertedEndOffset < 0 && state.NextCharIsWordSeparator) ||
+                state.NoSeparatorMap.Set.Has(convertedEndOffset - 1);
+
+            if (endIsSuffixedByWordSeparator)
+            {
+                delimitedText[delimitedTextOffset++] = (byte)'\\';
+                delimitedText[delimitedTextOffset++] = (byte)'b';
+            }
+            else
+            {
+                delimitedText[delimitedTextOffset++] = (byte)'a';
+            }
+
+            // Create our temporary match state for the next match.
+            bool matched = false;
+
+            // Insert the null terminator.
+            delimitedText[delimitedTextOffset] = 0;
+
+            // Check if the delimited word is on the dictionary.
+            state.DelimitedWordsTrie.Match(delimitedText, MatchSimple, ref matched);
+
+            // If we have a match, mask the word.
+            if (matched)
+            {
+                state.Matched = true;
+            }
+
+            return !matched;
+        }
+
+        private static bool MatchAndReplace(ReadOnlySpan<byte> text, int matchStartOffset, int matchEndOffset, int nodeId, ref MatchSimilarFormState state)
+        {
+            if (matchStartOffset < state.ReplaceEndOffset || state.MatchRanges.Count == 0)
+            {
+                return true;
+            }
+
+            // Check if the match range exists on our list of ranges.
+            int rangeIndex = state.MatchRanges.Find(matchStartOffset, matchEndOffset);
+
+            if ((uint)rangeIndex >= (uint)state.MatchRanges.Count)
+            {
+                return true;
+            }
+
+            MatchRange range = state.MatchRanges[rangeIndex];
+
+            // We only replace if the match has the same size or is larger than an existing match on the list.
+            if (range.StartOffset <= matchStartOffset &&
+                (range.StartOffset != matchStartOffset || range.EndOffset <= matchEndOffset))
+            {
+                // Copy all characters since the last match to the output.
+                int endOffset = state.ReplaceEndOffset;
+
+                if (endOffset < matchStartOffset)
+                {
+                    state.CanonicalText = state.CanonicalText.Append(text[endOffset..matchStartOffset]);
+                }
+
+                // Get canonical character from the similar one, and append it.
+                // For example, |-| is replaced with h, vv is replaced with w, etc.
+                ReadOnlySpan<byte> matchText = text[matchStartOffset..matchEndOffset];
+                state.CanonicalText = state.CanonicalText.AppendNullTerminated(state.SimilarFormTable.FindCanonicalString(matchText));
+                state.ReplaceEndOffset = matchEndOffset;
+            }
+
+            return true;
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                _contentsReader.Dispose();
+            }
+        }
+
+        public void Dispose()
+        {
+            Dispose(disposing: true);
+            GC.SuppressFinalize(this);
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/ProfanityFilterBase.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/ProfanityFilterBase.cs
new file mode 100644
index 000000000..e45c9f478
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/ProfanityFilterBase.cs
@@ -0,0 +1,789 @@
+using Ryujinx.Horizon.Common;
+using System;
+using System.Globalization;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    abstract class ProfanityFilterBase
+    {
+#pragma warning disable IDE0230 // Use UTF-8 string literal
+        private static readonly byte[][] _wordSeparators = {
+            new byte[] { 0x0D },
+            new byte[] { 0x0A },
+            new byte[] { 0xC2, 0x85 },
+            new byte[] { 0xE2, 0x80, 0xA8 },
+            new byte[] { 0xE2, 0x80, 0xA9 },
+            new byte[] { 0x09 },
+            new byte[] { 0x0B },
+            new byte[] { 0x0C },
+            new byte[] { 0x20 },
+            new byte[] { 0xEF, 0xBD, 0xA1 },
+            new byte[] { 0xEF, 0xBD, 0xA4 },
+            new byte[] { 0x2E },
+            new byte[] { 0x2C },
+            new byte[] { 0x5B },
+            new byte[] { 0x21 },
+            new byte[] { 0x22 },
+            new byte[] { 0x23 },
+            new byte[] { 0x24 },
+            new byte[] { 0x25 },
+            new byte[] { 0x26 },
+            new byte[] { 0x27 },
+            new byte[] { 0x28 },
+            new byte[] { 0x29 },
+            new byte[] { 0x2A },
+            new byte[] { 0x2B },
+            new byte[] { 0x2F },
+            new byte[] { 0x3A },
+            new byte[] { 0x3B },
+            new byte[] { 0x3C },
+            new byte[] { 0x3D },
+            new byte[] { 0x3E },
+            new byte[] { 0x3F },
+            new byte[] { 0x5C },
+            new byte[] { 0x40 },
+            new byte[] { 0x5E },
+            new byte[] { 0x5F },
+            new byte[] { 0x60 },
+            new byte[] { 0x7B },
+            new byte[] { 0x7C },
+            new byte[] { 0x7D },
+            new byte[] { 0x7E },
+            new byte[] { 0x2D },
+            new byte[] { 0x5D },
+            new byte[] { 0xE3, 0x80, 0x80 },
+            new byte[] { 0xE3, 0x80, 0x82 },
+            new byte[] { 0xE3, 0x80, 0x81 },
+            new byte[] { 0xEF, 0xBC, 0x8E },
+            new byte[] { 0xEF, 0xBC, 0x8C },
+            new byte[] { 0xEF, 0xBC, 0xBB },
+            new byte[] { 0xEF, 0xBC, 0x81 },
+            new byte[] { 0xE2, 0x80, 0x9C },
+            new byte[] { 0xE2, 0x80, 0x9D },
+            new byte[] { 0xEF, 0xBC, 0x83 },
+            new byte[] { 0xEF, 0xBC, 0x84 },
+            new byte[] { 0xEF, 0xBC, 0x85 },
+            new byte[] { 0xEF, 0xBC, 0x86 },
+            new byte[] { 0xE2, 0x80, 0x98 },
+            new byte[] { 0xE2, 0x80, 0x99 },
+            new byte[] { 0xEF, 0xBC, 0x88 },
+            new byte[] { 0xEF, 0xBC, 0x89 },
+            new byte[] { 0xEF, 0xBC, 0x8A },
+            new byte[] { 0xEF, 0xBC, 0x8B },
+            new byte[] { 0xEF, 0xBC, 0x8F },
+            new byte[] { 0xEF, 0xBC, 0x9A },
+            new byte[] { 0xEF, 0xBC, 0x9B },
+            new byte[] { 0xEF, 0xBC, 0x9C },
+            new byte[] { 0xEF, 0xBC, 0x9D },
+            new byte[] { 0xEF, 0xBC, 0x9E },
+            new byte[] { 0xEF, 0xBC, 0x9F },
+            new byte[] { 0xEF, 0xBC, 0xA0 },
+            new byte[] { 0xEF, 0xBF, 0xA5 },
+            new byte[] { 0xEF, 0xBC, 0xBE },
+            new byte[] { 0xEF, 0xBC, 0xBF },
+            new byte[] { 0xEF, 0xBD, 0x80 },
+            new byte[] { 0xEF, 0xBD, 0x9B },
+            new byte[] { 0xEF, 0xBD, 0x9C },
+            new byte[] { 0xEF, 0xBD, 0x9D },
+            new byte[] { 0xEF, 0xBD, 0x9E },
+            new byte[] { 0xEF, 0xBC, 0x8D },
+            new byte[] { 0xEF, 0xBC, 0xBD },
+        };
+#pragma warning restore IDE0230
+
+        private enum SignFilterStep
+        {
+            DetectEmailStart,
+            DetectEmailUserAtSign,
+            DetectEmailDomain,
+            DetectEmailEnd,
+        }
+
+        public abstract Result GetContentVersion(out uint version);
+        public abstract Result CheckProfanityWords(out uint checkMask, ReadOnlySpan<byte> word, uint regionMask, ProfanityFilterOption option);
+        public abstract Result MaskProfanityWordsInText(out int maskedWordsCount, Span<byte> text, uint regionMask, ProfanityFilterOption option);
+        public abstract Result Reload();
+
+        protected static bool IsIncludesAtSign(string word)
+        {
+            for (int index = 0; index < word.Length; index++)
+            {
+                if (word[index] == '\0')
+                {
+                    break;
+                }
+                else if (word[index] == '@' || word[index] == '\uFF20')
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        protected static int FilterAtSign(Span<byte> text, MaskMode maskMode)
+        {
+            SignFilterStep step = SignFilterStep.DetectEmailStart;
+            int matchStart = 0;
+            int matchCount = 0;
+
+            for (int index = 0; index < text.Length; index++)
+            {
+                byte character = text[index];
+
+                switch (step)
+                {
+                    case SignFilterStep.DetectEmailStart:
+                        if (char.IsAsciiLetterOrDigit((char)character))
+                        {
+                            step = SignFilterStep.DetectEmailUserAtSign;
+                            matchStart = index;
+                        }
+                        break;
+                    case SignFilterStep.DetectEmailUserAtSign:
+                        bool hasMatch = false;
+
+                        while (IsValidEmailAddressCharacter(character))
+                        {
+                            hasMatch = true;
+
+                            if (index + 1 >= text.Length)
+                            {
+                                break;
+                            }
+
+                            character = text[++index];
+                        }
+
+                        step = hasMatch && character == '@' ? SignFilterStep.DetectEmailDomain : SignFilterStep.DetectEmailStart;
+                        break;
+                    case SignFilterStep.DetectEmailDomain:
+                        step = char.IsAsciiLetterOrDigit((char)character) ? SignFilterStep.DetectEmailEnd : SignFilterStep.DetectEmailStart;
+                        break;
+                    case SignFilterStep.DetectEmailEnd:
+                        int domainIndex = index;
+
+                        while (index + 1 < text.Length && IsValidEmailAddressCharacter(text[++index]))
+                        {
+                        }
+
+                        int addressLastIndex = index - 1;
+                        int lastIndex = 0;
+                        bool lastIndexSet = false;
+
+                        while (matchStart < addressLastIndex)
+                        {
+                            character = text[addressLastIndex];
+
+                            if (char.IsAsciiLetterOrDigit((char)character))
+                            {
+                                if (!lastIndexSet)
+                                {
+                                    lastIndexSet = true;
+                                    lastIndex = addressLastIndex;
+                                }
+                            }
+                            else if (lastIndexSet)
+                            {
+                                break;
+                            }
+
+                            addressLastIndex--;
+                        }
+
+                        step = SignFilterStep.DetectEmailStart;
+
+                        if (domainIndex < addressLastIndex && character == '.')
+                        {
+                            PreMaskCharacterRange(text, matchStart, lastIndex + 1, maskMode, (lastIndex - matchStart) + 1);
+                            matchCount++;
+                        }
+                        else
+                        {
+                            index = domainIndex - 1;
+                        }
+                        break;
+                }
+            }
+
+            return matchCount;
+        }
+
+        private static bool IsValidEmailAddressCharacter(byte character)
+        {
+            return char.IsAsciiLetterOrDigit((char)character) || character == '-' || character == '.' || character == '_';
+        }
+
+        protected static void PreMaskCharacterRange(Span<byte> text, int startOffset, int endOffset, MaskMode maskMode, int characterCount)
+        {
+            int byteLength = endOffset - startOffset;
+
+            if (byteLength == 1)
+            {
+                text[startOffset] = 0xc1;
+            }
+            else if (byteLength == 2)
+            {
+                if (maskMode == MaskMode.Overwrite && Encoding.UTF8.GetCharCount(text.Slice(startOffset, 2)) != 1)
+                {
+                    text[startOffset] = 0xc1;
+                    text[startOffset + 1] = 0xc1;
+                }
+                else if (maskMode == MaskMode.Overwrite || maskMode == MaskMode.ReplaceByOneCharacter)
+                {
+                    text[startOffset] = 0xc0;
+                    text[startOffset + 1] = 0xc0;
+                }
+            }
+            else
+            {
+                text[startOffset++] = 0;
+
+                if (byteLength >= 0xff)
+                {
+                    int fillLength = (byteLength - 0xff) / 0xff + 1;
+
+                    text.Slice(startOffset++, fillLength).Fill(0xff);
+
+                    byteLength -= fillLength * 0xff;
+                    startOffset += fillLength;
+                }
+
+                text[startOffset++] = (byte)byteLength;
+
+                if (maskMode == MaskMode.ReplaceByOneCharacter)
+                {
+                    text[startOffset++] = 1;
+                }
+                else if (maskMode == MaskMode.Overwrite)
+                {
+                    if (characterCount >= 0xff)
+                    {
+                        int fillLength = (characterCount - 0xff) / 0xff + 1;
+
+                        text.Slice(startOffset, fillLength).Fill(0xff);
+
+                        characterCount -= fillLength * 0xff;
+                        startOffset += fillLength;
+                    }
+
+                    text[startOffset++] = (byte)characterCount;
+                }
+
+                if (startOffset < endOffset)
+                {
+                    text[startOffset..endOffset].Fill(0xc1);
+                }
+            }
+        }
+
+        protected static void ConvertUserInputForWord(out string outputText, string inputText)
+        {
+            outputText = inputText.ToLowerInvariant();
+        }
+
+        protected static void ConvertUserInputForText(Span<byte> outputText, Span<sbyte> deltaTable, ReadOnlySpan<byte> inputText)
+        {
+            int outputIndex = 0;
+            int deltaTableIndex = 0;
+
+            for (int index = 0; index < inputText.Length;)
+            {
+                byte character = inputText[index];
+                bool isInvalid = false;
+                int characterByteLength = 1;
+
+                if (character == 0xef && index + 4 < inputText.Length)
+                {
+                    if (((inputText[index + 1] == 0xbd && inputText[index + 2] >= 0xa6 && inputText[index + 2] < 0xe6) ||
+                        (inputText[index + 1] == 0xbe && inputText[index + 2] >= 0x80 && inputText[index + 2] < 0xa0)) &&
+                        inputText[index + 3] == 0xef &&
+                        inputText[index + 4] == 0xbe)
+                    {
+                        characterByteLength = 6;
+                    }
+                    else
+                    {
+                        characterByteLength = 3;
+                    }
+                }
+                else if ((character & 0x80) != 0)
+                {
+                    if (character >= 0xc2 && character < 0xe0)
+                    {
+                        characterByteLength = 2;
+                    }
+                    else if ((character & 0xf0) == 0xe0)
+                    {
+                        characterByteLength = 3;
+                    }
+                    else if ((character & 0xf8) == 0xf0)
+                    {
+                        characterByteLength = 4;
+                    }
+                    else
+                    {
+                        isInvalid = true;
+                    }
+                }
+
+                isInvalid |= index + characterByteLength > inputText.Length;
+
+                string str = null;
+
+                if (!isInvalid)
+                {
+                    str = Encoding.UTF8.GetString(inputText.Slice(index, characterByteLength));
+
+                    foreach (char chr in str)
+                    {
+                        if (chr == '\uFFFD')
+                        {
+                            isInvalid = true;
+                            break;
+                        }
+                    }
+                }
+
+                int convertedByteLength = 1;
+
+                if (isInvalid)
+                {
+                    characterByteLength = 1;
+                    outputText[outputIndex++] = inputText[index];
+                }
+                else
+                {
+                    convertedByteLength = Encoding.UTF8.GetBytes(str.ToLowerInvariant().AsSpan(), outputText[outputIndex..]);
+                    outputIndex += convertedByteLength;
+                }
+
+                if (deltaTable.Length != 0 && convertedByteLength != 0)
+                {
+                    // Calculate how many bytes we need to advance for each converted byte to match
+                    // the character on the original text.
+                    // The official service does this as part of the conversion (to lower case) process,
+                    // but since we use .NET for that here, this is done separately.
+
+                    int distribution = characterByteLength / convertedByteLength;
+
+                    deltaTable[deltaTableIndex++] = (sbyte)(characterByteLength - distribution * convertedByteLength + distribution);
+
+                    for (int byteIndex = 1; byteIndex < convertedByteLength; byteIndex++)
+                    {
+                        deltaTable[deltaTableIndex++] = (sbyte)distribution;
+                    }
+                }
+
+                index += characterByteLength;
+            }
+
+            if (outputIndex < outputText.Length)
+            {
+                outputText[outputIndex] = 0;
+            }
+        }
+
+        protected static Span<byte> MaskText(Span<byte> text)
+        {
+            if (text.Length == 0)
+            {
+                return text;
+            }
+
+            for (int index = 0; index < text.Length; index++)
+            {
+                byte character = text[index];
+
+                if (character == 0xc1)
+                {
+                    text[index] = (byte)'*';
+                }
+                else if (character == 0xc0)
+                {
+                    if (index + 1 < text.Length && text[index + 1] == 0xc0)
+                    {
+                        text[index++] = (byte)'*';
+                        text[index] = 0;
+                    }
+                }
+                else if (character == 0 && index + 1 < text.Length)
+                {
+                    // There are two sequences of 0xFF followed by another value.
+                    // The first indicates the length of the sub-string to replace in bytes.
+                    // The second indicates the character count.
+
+                    int lengthSequenceIndex = index + 1;
+                    int byteLength = CountMaskLengthBytes(text, ref lengthSequenceIndex);
+                    int characterCount = CountMaskLengthBytes(text, ref lengthSequenceIndex);
+
+                    if (byteLength != 0)
+                    {
+                        for (int replaceIndex = 0; replaceIndex < byteLength; replaceIndex++)
+                        {
+                            text[index++] = (byte)(replaceIndex < characterCount ? '*' : '\0');
+                        }
+
+                        index--;
+                    }
+                }
+            }
+
+            // Move null-terminators to the end.
+            MoveZeroValuesToEnd(text);
+
+            // Find new length of the text.
+            int length = text.IndexOf((byte)0);
+
+            if (length >= 0)
+            {
+                return text[..length];
+            }
+
+            return text;
+        }
+
+        protected static void UpdateDeltaTable(Span<sbyte> deltaTable, ReadOnlySpan<byte> text)
+        {
+            if (text.Length == 0)
+            {
+                return;
+            }
+
+            // Update values to account for the characters that will be removed.
+            for (int index = 0; index < text.Length; index++)
+            {
+                byte character = text[index];
+
+                if (character == 0 && index + 1 < text.Length)
+                {
+                    // There are two sequences of 0xFF followed by another value.
+                    // The first indicates the length of the sub-string to replace in bytes.
+                    // The second indicates the character count.
+
+                    int lengthSequenceIndex = index + 1;
+                    int byteLength = CountMaskLengthBytes(text, ref lengthSequenceIndex);
+                    int characterCount = CountMaskLengthBytes(text, ref lengthSequenceIndex);
+
+                    if (byteLength != 0)
+                    {
+                        for (int replaceIndex = 0; replaceIndex < byteLength; replaceIndex++)
+                        {
+                            deltaTable[index++] = (sbyte)(replaceIndex < characterCount ? 1 : 0);
+                        }
+                    }
+                }
+            }
+
+            // Move zero values of the removed bytes to the end.
+            MoveZeroValuesToEnd(MemoryMarshal.Cast<sbyte, byte>(deltaTable));
+        }
+
+        private static int CountMaskLengthBytes(ReadOnlySpan<byte> text, ref int index)
+        {
+            int totalLength = 0;
+
+            for (; index < text.Length; index++)
+            {
+                int length = text[index];
+                totalLength += length;
+
+                if (length != 0xff)
+                {
+                    index++;
+                    break;
+                }
+            }
+
+            return totalLength;
+        }
+
+        private static void MoveZeroValuesToEnd(Span<byte> text)
+        {
+            for (int index = 0; index < text.Length; index++)
+            {
+                int nullCount = 0;
+
+                for (; index + nullCount < text.Length; nullCount++)
+                {
+                    byte character = text[index + nullCount];
+                    if (character != 0)
+                    {
+                        break;
+                    }
+                }
+
+                if (nullCount != 0)
+                {
+                    int fillLength = text.Length - (index + nullCount);
+
+                    text[(index + nullCount)..].CopyTo(text.Slice(index, fillLength));
+                    text.Slice(index + fillLength, nullCount).Clear();
+                }
+            }
+        }
+
+        protected static Span<byte> RemoveWordSeparators(Span<byte> output, ReadOnlySpan<byte> input, Sbv map)
+        {
+            int outputIndex = 0;
+
+            if (map.Set.BitVector.BitLength != 0)
+            {
+                for (int index = 0; index < input.Length; index++)
+                {
+                    bool isWordSeparator = false;
+
+                    for (int separatorIndex = 0; separatorIndex < _wordSeparators.Length; separatorIndex++)
+                    {
+                        ReadOnlySpan<byte> separator = _wordSeparators[separatorIndex];
+
+                        if (index + separator.Length < input.Length && input.Slice(index, separator.Length).SequenceEqual(separator))
+                        {
+                            map.Set.TurnOn(index, separator.Length);
+
+                            index += separator.Length - 1;
+                            isWordSeparator = true;
+                            break;
+                        }
+                    }
+
+                    if (!isWordSeparator)
+                    {
+                        output[outputIndex++] = input[index];
+                    }
+                }
+            }
+
+            map.Build();
+
+            return output[..outputIndex];
+        }
+
+        protected static int TrimEnd(ReadOnlySpan<byte> text, int offset)
+        {
+            for (int separatorIndex = 0; separatorIndex < _wordSeparators.Length; separatorIndex++)
+            {
+                ReadOnlySpan<byte> separator = _wordSeparators[separatorIndex];
+
+                if (offset >= separator.Length && text.Slice(offset - separator.Length, separator.Length).SequenceEqual(separator))
+                {
+                    offset -= separator.Length;
+                    separatorIndex = -1;
+                }
+            }
+
+            return offset;
+        }
+
+        protected static bool IsPrefixedByWordSeparator(ReadOnlySpan<byte> text, int offset)
+        {
+            for (int separatorIndex = 0; separatorIndex < _wordSeparators.Length; separatorIndex++)
+            {
+                ReadOnlySpan<byte> separator = _wordSeparators[separatorIndex];
+
+                if (offset >= separator.Length && text.Slice(offset - separator.Length, separator.Length).SequenceEqual(separator))
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        protected static bool IsWordSeparator(ReadOnlySpan<byte> text, int offset)
+        {
+            for (int separatorIndex = 0; separatorIndex < _wordSeparators.Length; separatorIndex++)
+            {
+                ReadOnlySpan<byte> separator = _wordSeparators[separatorIndex];
+
+                if (offset + separator.Length <= text.Length && text.Slice(offset, separator.Length).SequenceEqual(separator))
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        protected static Span<byte> RemoveWordSeparators(Span<byte> output, ReadOnlySpan<byte> input, Sbv map, AhoCorasick notSeparatorTrie)
+        {
+            int outputIndex = 0;
+
+            if (map.Set.BitVector.BitLength != 0)
+            {
+                for (int index = 0; index < input.Length;)
+                {
+                    byte character = input[index];
+                    int characterByteLength = 1;
+
+                    if ((character & 0x80) != 0)
+                    {
+                        if (character >= 0xc2 && character < 0xe0)
+                        {
+                            characterByteLength = 2;
+                        }
+                        else if ((character & 0xf0) == 0xe0)
+                        {
+                            characterByteLength = 3;
+                        }
+                        else if ((character & 0xf8) == 0xf0)
+                        {
+                            characterByteLength = 4;
+                        }
+                    }
+
+                    characterByteLength = Math.Min(characterByteLength, input.Length - index);
+
+                    bool isWordSeparator = IsWordSeparator(input.Slice(index, characterByteLength), notSeparatorTrie);
+                    if (isWordSeparator)
+                    {
+                        map.Set.TurnOn(index, characterByteLength);
+                    }
+                    else
+                    {
+                        output[outputIndex++] = input[index];
+                    }
+
+                    index += characterByteLength;
+                }
+            }
+
+            map.Build();
+
+            return output[..outputIndex];
+        }
+
+        protected static bool IsWordSeparator(ReadOnlySpan<byte> text, AhoCorasick notSeparatorTrie)
+        {
+            string str = Encoding.UTF8.GetString(text);
+
+            if (str.Length == 0)
+            {
+                return false;
+            }
+
+            char character = str[0];
+
+            switch (character)
+            {
+                case '\0':
+                case '\uD800':
+                case '\uDB7F':
+                case '\uDB80':
+                case '\uDBFF':
+                case '\uDC00':
+                case '\uDFFF':
+                    return false;
+                case '\u02E4':
+                case '\u02EC':
+                case '\u02EE':
+                case '\u0374':
+                case '\u037A':
+                case '\u0559':
+                case '\u0640':
+                case '\u06E5':
+                case '\u06E6':
+                case '\u07F4':
+                case '\u07F5':
+                case '\u07FA':
+                case '\u1C78':
+                case '\u1C79':
+                case '\u1C7A':
+                case '\u1C7B':
+                case '\u1C7C':
+                case '\uA4F8':
+                case '\uA4F9':
+                case '\uA4FA':
+                case '\uA4FB':
+                case '\uA4FC':
+                case '\uA4FD':
+                case '\uFF70':
+                case '\uFF9A':
+                case '\uFF9B':
+                    return true;
+            }
+
+            bool matched = false;
+
+            notSeparatorTrie.Match(text, MatchSimple, ref matched);
+
+            if (!matched)
+            {
+                switch (char.GetUnicodeCategory(character))
+                {
+                    case UnicodeCategory.NonSpacingMark:
+                    case UnicodeCategory.SpacingCombiningMark:
+                    case UnicodeCategory.EnclosingMark:
+                    case UnicodeCategory.SpaceSeparator:
+                    case UnicodeCategory.LineSeparator:
+                    case UnicodeCategory.ParagraphSeparator:
+                    case UnicodeCategory.Control:
+                    case UnicodeCategory.Format:
+                    case UnicodeCategory.Surrogate:
+                    case UnicodeCategory.PrivateUse:
+                    case UnicodeCategory.ConnectorPunctuation:
+                    case UnicodeCategory.DashPunctuation:
+                    case UnicodeCategory.OpenPunctuation:
+                    case UnicodeCategory.ClosePunctuation:
+                    case UnicodeCategory.InitialQuotePunctuation:
+                    case UnicodeCategory.FinalQuotePunctuation:
+                    case UnicodeCategory.OtherPunctuation:
+                    case UnicodeCategory.MathSymbol:
+                    case UnicodeCategory.CurrencySymbol:
+                        return true;
+                }
+            }
+
+            return false;
+        }
+
+        protected static int GetUtf8Length(out int characterCount, ReadOnlySpan<byte> text, int maxCharacters)
+        {
+            int index;
+
+            for (index = 0, characterCount = 0; index < text.Length && characterCount < maxCharacters; characterCount++)
+            {
+                byte character = text[index];
+                int characterByteLength;
+
+                if ((character & 0x80) != 0 || character == 0)
+                {
+                    if (character >= 0xc2 && character < 0xe0)
+                    {
+                        characterByteLength = 2;
+                    }
+                    else if ((character & 0xf0) == 0xe0)
+                    {
+                        characterByteLength = 3;
+                    }
+                    else if ((character & 0xf8) == 0xf0)
+                    {
+                        characterByteLength = 4;
+                    }
+                    else
+                    {
+                        index = 0;
+                        break;
+                    }
+                }
+                else
+                {
+                    characterByteLength = 1;
+                }
+
+                index += characterByteLength;
+            }
+
+            return index;
+        }
+
+        protected static bool MatchSimple(ReadOnlySpan<byte> text, int matchStartOffset, int matchEndOffset, int nodeId, ref bool matched)
+        {
+            matched = true;
+
+            return false;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/Sbv.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/Sbv.cs
new file mode 100644
index 000000000..d6d0bfd60
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/Sbv.cs
@@ -0,0 +1,34 @@
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    class Sbv
+    {
+        private readonly SbvSelect _sbvSelect;
+        private readonly Set _set;
+
+        public SbvSelect SbvSelect => _sbvSelect;
+        public Set Set => _set;
+
+        public Sbv()
+        {
+            _sbvSelect = new();
+            _set = new();
+        }
+
+        public Sbv(int length)
+        {
+            _sbvSelect = new();
+            _set = new(length);
+        }
+
+        public void Build()
+        {
+            _set.Build();
+            _sbvSelect.Build(_set.BitVector.Array, _set.BitVector.BitLength);
+        }
+
+        public bool Import(ref BinaryReader reader)
+        {
+            return _set.Import(ref reader) && _sbvSelect.Import(ref reader);
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/SbvRank.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/SbvRank.cs
new file mode 100644
index 000000000..c541b1f57
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/SbvRank.cs
@@ -0,0 +1,162 @@
+using System;
+using System.Numerics;
+
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    class SbvRank
+    {
+        private const int BitsPerWord = Set.BitsPerWord;
+        private const int Rank1Entries = 8;
+        private const int BitsPerRank0Entry = BitsPerWord * Rank1Entries;
+
+        private uint[] _rank0;
+        private byte[] _rank1;
+
+        public SbvRank()
+        {
+        }
+
+        public SbvRank(ReadOnlySpan<uint> bitmap, int setCapacity)
+        {
+            Build(bitmap, setCapacity);
+        }
+
+        public void Build(ReadOnlySpan<uint> bitmap, int setCapacity)
+        {
+            _rank0 = new uint[CalculateRank0Length(setCapacity)];
+            _rank1 = new byte[CalculateRank1Length(setCapacity)];
+
+            BuildRankDictionary(_rank0, _rank1, (setCapacity + BitsPerWord - 1) / BitsPerWord, bitmap);
+        }
+
+        private static void BuildRankDictionary(Span<uint> rank0, Span<byte> rank1, int length, ReadOnlySpan<uint> bitmap)
+        {
+            uint rank0Count;
+            uint rank1Count = 0;
+
+            for (int index = 0; index < length; index++)
+            {
+                if ((index % Rank1Entries) != 0)
+                {
+                    rank0Count = rank0[index / Rank1Entries];
+                }
+                else
+                {
+                    rank0[index / Rank1Entries] = rank1Count;
+                    rank0Count = rank1Count;
+                }
+
+                rank1[index] = (byte)(rank1Count - rank0Count);
+
+                rank1Count += (uint)BitOperations.PopCount(bitmap[index]);
+            }
+        }
+
+        public bool Import(ref BinaryReader reader, int setCapacity)
+        {
+            if (setCapacity == 0)
+            {
+                return true;
+            }
+
+            int rank0Length = CalculateRank0Length(setCapacity);
+            int rank1Length = CalculateRank1Length(setCapacity);
+
+            return reader.AllocateAndReadArray(ref _rank0, rank0Length) == rank0Length &&
+                reader.AllocateAndReadArray(ref _rank1, rank1Length) == rank1Length;
+        }
+
+        public int CalcRank1(int index, uint[] membershipBitmap)
+        {
+            int rank0Index = index / BitsPerRank0Entry;
+            int rank1Index = index / BitsPerWord;
+
+            uint membershipBits = membershipBitmap[rank1Index] & (uint.MaxValue >> (BitsPerWord - 1 - (index % BitsPerWord)));
+
+            return (int)_rank0[rank0Index] + _rank1[rank1Index] + BitOperations.PopCount(membershipBits);
+        }
+
+        public int CalcSelect0(int index, int length, uint[] membershipBitmap)
+        {
+            int rank0Index;
+
+            if (length > BitsPerRank0Entry)
+            {
+                int left = 0;
+                int right = (length + BitsPerRank0Entry - 1) / BitsPerRank0Entry;
+
+                while (true)
+                {
+                    int range = right - left;
+                    if (range < 0)
+                    {
+                        range++;
+                    }
+
+                    int middle = left + (range / 2);
+
+                    int foundIndex = middle * BitsPerRank0Entry - (int)_rank0[middle];
+
+                    if ((uint)foundIndex <= (uint)index)
+                    {
+                        left = middle;
+                    }
+                    else
+                    {
+                        right = middle;
+                    }
+
+                    if (right <= left + 1)
+                    {
+                        break;
+                    }
+                }
+
+                rank0Index = left;
+            }
+            else
+            {
+                rank0Index = 0;
+            }
+
+            int lengthInWords = (length + BitsPerWord - 1) / BitsPerWord;
+            int rank1WordsCount = rank0Index == (length / BitsPerRank0Entry) && (lengthInWords % Rank1Entries) != 0
+                ? lengthInWords % Rank1Entries
+                : Rank1Entries;
+
+            int baseIndex = (int)_rank0[rank0Index] + rank0Index * -BitsPerRank0Entry + index;
+            int plainIndex;
+            int count;
+            int remainingBits;
+            uint membershipBits;
+
+            for (plainIndex = rank0Index * Rank1Entries - 1, count = 0; count < rank1WordsCount; plainIndex++, count++)
+            {
+                int currentIndex = baseIndex + count * -BitsPerWord;
+
+                if (_rank1[plainIndex + 1] + currentIndex < 0)
+                {
+                    remainingBits = _rank1[plainIndex] + currentIndex + BitsPerWord;
+                    membershipBits = ~membershipBitmap[plainIndex];
+
+                    return plainIndex * BitsPerWord + SbvSelect.SelectPos(membershipBits, remainingBits);
+                }
+            }
+
+            remainingBits = _rank1[plainIndex] + baseIndex + (rank1WordsCount - 1) * -BitsPerWord;
+            membershipBits = ~membershipBitmap[plainIndex];
+
+            return plainIndex * BitsPerWord + SbvSelect.SelectPos(membershipBits, remainingBits);
+        }
+
+        private static int CalculateRank0Length(int setCapacity)
+        {
+            return (setCapacity / (BitsPerWord * Rank1Entries)) + 1;
+        }
+
+        private static int CalculateRank1Length(int setCapacity)
+        {
+            return (setCapacity / BitsPerWord) + 1;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/SbvSelect.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/SbvSelect.cs
new file mode 100644
index 000000000..54c3f8b04
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/SbvSelect.cs
@@ -0,0 +1,156 @@
+using System;
+using System.Numerics;
+
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    class SbvSelect
+    {
+        private uint[] _array;
+        private BitVector32 _bv1;
+        private BitVector32 _bv2;
+        private SbvRank _sbvRank1;
+        private SbvRank _sbvRank2;
+
+        public bool Import(ref BinaryReader reader)
+        {
+            if (!reader.Read(out int arrayLength) ||
+                reader.AllocateAndReadArray(ref _array, arrayLength) != arrayLength)
+            {
+                return false;
+            }
+
+            _bv1 = new();
+            _bv2 = new();
+            _sbvRank1 = new();
+            _sbvRank2 = new();
+
+            return _bv1.Import(ref reader) &&
+                _bv2.Import(ref reader) &&
+                _sbvRank1.Import(ref reader, _bv1.BitLength) &&
+                _sbvRank2.Import(ref reader, _bv2.BitLength);
+        }
+
+        public void Build(ReadOnlySpan<uint> bitmap, int length)
+        {
+            int lengthInWords = (length + Set.BitsPerWord - 1) / Set.BitsPerWord;
+
+            int rank0Length = 0;
+            int rank1Length = 0;
+
+            if (lengthInWords != 0)
+            {
+                for (int index = 0; index < bitmap.Length; index++)
+                {
+                    uint value = bitmap[index];
+
+                    if (value != 0)
+                    {
+                        rank0Length++;
+                        rank1Length += BitOperations.PopCount(value);
+                    }
+                }
+            }
+
+            _bv1 = new(rank0Length);
+            _bv2 = new(rank1Length);
+            _array = new uint[rank0Length];
+
+            bool setSequence = false;
+            int arrayIndex = 0;
+            uint unsetCount = 0;
+            rank0Length = 0;
+            rank1Length = 0;
+
+            if (lengthInWords != 0)
+            {
+                for (int index = 0; index < bitmap.Length; index++)
+                {
+                    uint value = bitmap[index];
+
+                    if (value != 0)
+                    {
+                        if (!setSequence)
+                        {
+                            _bv1.TurnOn(rank0Length);
+                            _array[arrayIndex++] = unsetCount;
+                            setSequence = true;
+                        }
+
+                        _bv2.TurnOn(rank1Length);
+
+                        rank0Length++;
+                        rank1Length += BitOperations.PopCount(value);
+                    }
+                    else
+                    {
+                        unsetCount++;
+                        setSequence = false;
+                    }
+                }
+            }
+
+            _sbvRank1 = new(_bv1.Array, _bv1.BitLength);
+            _sbvRank2 = new(_bv2.Array, _bv2.BitLength);
+        }
+
+        public int Select(Set set, int index)
+        {
+            if (index < _bv2.BitLength)
+            {
+                int rank1PlainIndex = _sbvRank2.CalcRank1(index, _bv2.Array);
+                int rank0PlainIndex = _sbvRank1.CalcRank1(rank1PlainIndex - 1, _bv1.Array);
+
+                int value = (int)_array[rank0PlainIndex - 1] + (rank1PlainIndex - 1);
+
+                int baseBitIndex = 0;
+
+                if (value != 0)
+                {
+                    baseBitIndex = value * 32;
+
+                    int setBvLength = set.BitVector.BitLength;
+                    int bitIndexBounded = baseBitIndex - 1;
+
+                    if (bitIndexBounded >= setBvLength)
+                    {
+                        bitIndexBounded = setBvLength - 1;
+                    }
+
+                    index -= set.SbvRank.CalcRank1(bitIndexBounded, set.BitVector.Array);
+                }
+
+                return SelectPos(set.BitVector.Array[value], index) + baseBitIndex;
+            }
+
+            return -1;
+        }
+
+        public static int SelectPos(uint membershipBits, int bitIndex)
+        {
+            // Skips "bitIndex" set bits, and returns the bit index of the next set bit.
+            // If there is no set bit after skipping the specified amount, returns 32.
+
+            int bit;
+            int bitCount = bitIndex;
+
+            for (bit = 0; bit < sizeof(uint) * 8;)
+            {
+                if (((membershipBits >> bit) & 1) != 0)
+                {
+                    if (bitCount-- == 0)
+                    {
+                        break;
+                    }
+
+                    bit++;
+                }
+                else
+                {
+                    bit += BitOperations.TrailingZeroCount(membershipBits >> bit);
+                }
+            }
+
+            return bit;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/Set.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/Set.cs
new file mode 100644
index 000000000..559b78515
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/Set.cs
@@ -0,0 +1,73 @@
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    class Set
+    {
+        public const int BitsPerWord = 32;
+
+        private readonly BitVector32 _bitVector;
+        private readonly SbvRank _sbvRank;
+
+        public BitVector32 BitVector => _bitVector;
+        public SbvRank SbvRank => _sbvRank;
+
+        public Set()
+        {
+            _bitVector = new();
+            _sbvRank = new();
+        }
+
+        public Set(int length)
+        {
+            _bitVector = new(length);
+            _sbvRank = new();
+        }
+
+        public void Build()
+        {
+            _sbvRank.Build(_bitVector.Array, _bitVector.BitLength);
+        }
+
+        public bool Import(ref BinaryReader reader)
+        {
+            return _bitVector.Import(ref reader) && _sbvRank.Import(ref reader, _bitVector.BitLength);
+        }
+
+        public bool Has(int index)
+        {
+            return _bitVector.Has(index);
+        }
+
+        public bool TurnOn(int index, int count)
+        {
+            return _bitVector.TurnOn(index, count);
+        }
+
+        public bool TurnOn(int index)
+        {
+            return _bitVector.TurnOn(index);
+        }
+
+        public int Rank1(int index)
+        {
+            if ((uint)index >= (uint)_bitVector.BitLength)
+            {
+                index = _bitVector.BitLength - 1;
+            }
+
+            return _sbvRank.CalcRank1(index, _bitVector.Array);
+        }
+
+        public int Select0(int index)
+        {
+            int length = _bitVector.BitLength;
+            int rankIndex = _sbvRank.CalcRank1(length - 1, _bitVector.Array);
+
+            if ((uint)index < (uint)(length - rankIndex))
+            {
+                return _sbvRank.CalcSelect0(index, length, _bitVector.Array);
+            }
+
+            return -1;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/SimilarFormTable.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/SimilarFormTable.cs
new file mode 100644
index 000000000..7999e6ca1
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/SimilarFormTable.cs
@@ -0,0 +1,132 @@
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    class SimilarFormTable
+    {
+        private int _similarTableStringLength;
+        private int _canonicalTableStringLength;
+        private int _count;
+        private byte[][] _similarTable;
+        private byte[][] _canonicalTable;
+
+        public bool Import(ref BinaryReader reader)
+        {
+            if (!reader.Read(out _similarTableStringLength) ||
+                !reader.Read(out _canonicalTableStringLength) ||
+                !reader.Read(out _count))
+            {
+                return false;
+            }
+
+            _similarTable = new byte[_count][];
+            _canonicalTable = new byte[_count][];
+
+            if (_count < 1)
+            {
+                return true;
+            }
+
+            for (int tableIndex = 0; tableIndex < _count; tableIndex++)
+            {
+                if (reader.AllocateAndReadArray(ref _similarTable[tableIndex], _similarTableStringLength) != _similarTableStringLength ||
+                    reader.AllocateAndReadArray(ref _canonicalTable[tableIndex], _canonicalTableStringLength) != _canonicalTableStringLength)
+                {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        public ReadOnlySpan<byte> FindCanonicalString(ReadOnlySpan<byte> similarFormString)
+        {
+            int lowerBound = 0;
+            int upperBound = _count;
+
+            for (int charIndex = 0; charIndex < similarFormString.Length; charIndex++)
+            {
+                byte character = similarFormString[charIndex];
+
+                int newLowerBound = GetLowerBound(character, charIndex, lowerBound - 1, upperBound - 1);
+                if (newLowerBound < 0 || _similarTable[newLowerBound][charIndex] != character)
+                {
+                    return ReadOnlySpan<byte>.Empty;
+                }
+
+                int newUpperBound = GetUpperBound(character, charIndex, lowerBound - 1, upperBound - 1);
+                if (newUpperBound < 0)
+                {
+                    newUpperBound = upperBound;
+                }
+
+                lowerBound = newLowerBound;
+                upperBound = newUpperBound;
+            }
+
+            return _canonicalTable[lowerBound];
+        }
+
+        private int GetLowerBound(byte character, int charIndex, int left, int right)
+        {
+            while (right - left > 1)
+            {
+                int range = right + left;
+
+                if (range < 0)
+                {
+                    range++;
+                }
+
+                int middle = range / 2;
+
+                if (character <= _similarTable[middle][charIndex])
+                {
+                    right = middle;
+                }
+                else
+                {
+                    left = middle;
+                }
+            }
+
+            if (_similarTable[right][charIndex] < character)
+            {
+                return -1;
+            }
+
+            return right;
+        }
+
+        private int GetUpperBound(byte character, int charIndex, int left, int right)
+        {
+            while (right - left > 1)
+            {
+                int range = right + left;
+
+                if (range < 0)
+                {
+                    range++;
+                }
+
+                int middle = range / 2;
+
+                if (_similarTable[middle][charIndex] <= character)
+                {
+                    left = middle;
+                }
+                else
+                {
+                    right = middle;
+                }
+            }
+
+            if (_similarTable[right][charIndex] <= character)
+            {
+                return -1;
+            }
+
+            return right;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/SparseSet.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/SparseSet.cs
new file mode 100644
index 000000000..6690203df
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/SparseSet.cs
@@ -0,0 +1,125 @@
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    class SparseSet
+    {
+        private const int BitsPerWord = Set.BitsPerWord;
+
+        private ulong _rangeValuesCount;
+        private ulong _rangeStartValue;
+        private ulong _rangeEndValue;
+        private uint _count;
+        private uint _bitfieldLength;
+        private uint[] _bitfields;
+        private readonly Sbv _sbv = new();
+
+        public ulong RangeValuesCount => _rangeValuesCount;
+        public ulong RangeEndValue => _rangeEndValue;
+
+        public bool Import(ref BinaryReader reader)
+        {
+            if (!reader.Read(out _rangeValuesCount) ||
+                !reader.Read(out _rangeStartValue) ||
+                !reader.Read(out _rangeEndValue) ||
+                !reader.Read(out _count) ||
+                !reader.Read(out _bitfieldLength) ||
+                !reader.Read(out int arrayLength) ||
+                reader.AllocateAndReadArray(ref _bitfields, arrayLength) != arrayLength)
+            {
+                return false;
+            }
+
+            return _sbv.Import(ref reader);
+        }
+
+        public bool Has(long index)
+        {
+            int plainIndex = Rank1(index);
+
+            return plainIndex != 0 && Select1Ex(plainIndex - 1) == index;
+        }
+
+        public int Rank1(long index)
+        {
+            uint count = _count;
+
+            if ((ulong)index < _rangeStartValue || count == 0)
+            {
+                return 0;
+            }
+
+            if (_rangeStartValue == (ulong)index || count < 3)
+            {
+                return 1;
+            }
+
+            if (_rangeEndValue <= (ulong)index)
+            {
+                return (int)count;
+            }
+
+            int left = 0;
+            int right = (int)count - 1;
+
+            while (true)
+            {
+                int range = right - left;
+                if (range < 0)
+                {
+                    range++;
+                }
+
+                int middle = left + (range / 2);
+
+                long foundIndex = Select1Ex(middle);
+
+                if ((ulong)foundIndex <= (ulong)index)
+                {
+                    left = middle;
+                }
+                else
+                {
+                    right = middle;
+                }
+
+                if (right <= left + 1)
+                {
+                    break;
+                }
+            }
+
+            return left + 1;
+        }
+
+        public int Select1(int index)
+        {
+            return (int)Select1Ex(index);
+        }
+
+        public long Select1Ex(int index)
+        {
+            if ((uint)index >= _count)
+            {
+                return -1L;
+            }
+
+            int indexOffset = _sbv.SbvSelect.Select(_sbv.Set, index);
+            int bitfieldLength = (int)_bitfieldLength;
+
+            int currentBitIndex = index * bitfieldLength;
+            int wordIndex = currentBitIndex / BitsPerWord;
+            int wordBitOffset = currentBitIndex % BitsPerWord;
+
+            ulong value = _bitfields[wordIndex];
+
+            if (wordBitOffset + bitfieldLength > BitsPerWord)
+            {
+                value |= (ulong)_bitfields[wordIndex + 1] << 32;
+            }
+
+            value >>= wordBitOffset;
+            value &= uint.MaxValue >> (BitsPerWord - bitfieldLength);
+
+            return ((indexOffset - (uint)index) << bitfieldLength) + (int)value;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/Utf8ParseResult.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/Utf8ParseResult.cs
new file mode 100644
index 000000000..bf065f86a
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/Utf8ParseResult.cs
@@ -0,0 +1,27 @@
+using Ryujinx.Horizon.Common;
+
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    enum Utf8ParseResult
+    {
+        Success = 0,
+        InvalidCharacter = 2,
+        InvalidPointer = 0x16,
+        InvalidSize = 0x22,
+        InvalidString = 0x54,
+    }
+
+    static class Utf8ParseResultExtensions
+    {
+        public static Result ToHorizonResult(this Utf8ParseResult result)
+        {
+            return result switch
+            {
+                Utf8ParseResult.Success => Result.Success,
+                Utf8ParseResult.InvalidSize => NgcResult.InvalidSize,
+                Utf8ParseResult.InvalidString => NgcResult.InvalidUtf8Encoding,
+                _ => NgcResult.InvalidPointer,
+            };
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/Utf8Text.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/Utf8Text.cs
new file mode 100644
index 000000000..47f780494
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/Utf8Text.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Text;
+
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    readonly struct Utf8Text
+    {
+        private readonly byte[] _text;
+        private readonly int[] _charOffsets;
+
+        public int CharacterCount => _charOffsets.Length - 1;
+
+        public Utf8Text()
+        {
+            _text = Array.Empty<byte>();
+            _charOffsets = Array.Empty<int>();
+        }
+
+        public Utf8Text(byte[] text)
+        {
+            _text = text;
+
+            UTF8Encoding encoding = new(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
+
+            string str = encoding.GetString(text);
+
+            _charOffsets = new int[str.Length + 1];
+
+            int offset = 0;
+
+            for (int index = 0; index < str.Length; index++)
+            {
+                _charOffsets[index] = offset;
+                offset += encoding.GetByteCount(str.AsSpan().Slice(index, 1));
+            }
+
+            _charOffsets[str.Length] = offset;
+        }
+
+        public Utf8Text(ReadOnlySpan<byte> text) : this(text.ToArray())
+        {
+        }
+
+        public static Utf8ParseResult Create(out Utf8Text utf8Text, ReadOnlySpan<byte> text)
+        {
+            try
+            {
+                utf8Text = new(text);
+            }
+            catch (ArgumentException)
+            {
+                utf8Text = default;
+
+                return Utf8ParseResult.InvalidCharacter;
+            }
+
+            return Utf8ParseResult.Success;
+        }
+
+        public ReadOnlySpan<byte> AsSubstring(int startCharIndex, int endCharIndex)
+        {
+            int startOffset = _charOffsets[startCharIndex];
+            int endOffset = _charOffsets[endCharIndex];
+
+            return _text.AsSpan()[startOffset..endOffset];
+        }
+
+        public Utf8Text AppendNullTerminated(ReadOnlySpan<byte> toAppend)
+        {
+            int length = toAppend.IndexOf((byte)0);
+            if (length >= 0)
+            {
+                toAppend = toAppend[..length];
+            }
+
+            return Append(toAppend);
+        }
+
+        public Utf8Text Append(ReadOnlySpan<byte> toAppend)
+        {
+            byte[] combined = new byte[_text.Length + toAppend.Length];
+
+            _text.AsSpan().CopyTo(combined.AsSpan()[.._text.Length]);
+            toAppend.CopyTo(combined.AsSpan()[_text.Length..]);
+
+            return new(combined);
+        }
+
+        public void CopyTo(Span<byte> destination)
+        {
+            _text.CopyTo(destination[.._text.Length]);
+
+            if (destination.Length > _text.Length)
+            {
+                destination[_text.Length] = 0;
+            }
+        }
+
+        public ReadOnlySpan<byte> AsSpan()
+        {
+            return _text;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/Utf8Util.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/Utf8Util.cs
new file mode 100644
index 000000000..1bb543ba1
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/Utf8Util.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Text;
+
+namespace Ryujinx.Horizon.Sdk.Ngc.Detail
+{
+    static class Utf8Util
+    {
+        public static Utf8ParseResult NormalizeFormKC(Span<byte> output, ReadOnlySpan<byte> input)
+        {
+            int length = input.IndexOf((byte)0);
+            if (length >= 0)
+            {
+                input = input[..length];
+            }
+
+            UTF8Encoding encoding = new(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
+
+            string text;
+
+            try
+            {
+                text = encoding.GetString(input);
+            }
+            catch (ArgumentException)
+            {
+                return Utf8ParseResult.InvalidCharacter;
+            }
+
+            string normalizedText = text.Normalize(NormalizationForm.FormKC);
+
+            int outputIndex = Encoding.UTF8.GetBytes(normalizedText, output);
+
+            if (outputIndex < output.Length)
+            {
+                output[outputIndex] = 0;
+            }
+
+            return Utf8ParseResult.Success;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/INgcService.cs b/src/Ryujinx.Horizon/Sdk/Ngc/INgcService.cs
new file mode 100644
index 000000000..90f07822d
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/INgcService.cs
@@ -0,0 +1,14 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Sf;
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Ngc
+{
+    interface INgcService : IServiceObject
+    {
+        Result GetContentVersion(out uint version);
+        Result Check(out uint checkMask, ReadOnlySpan<byte> text, uint regionMask, ProfanityFilterOption option);
+        Result Mask(out int maskedWordsCount, Span<byte> filteredText, ReadOnlySpan<byte> text, uint regionMask, ProfanityFilterOption option);
+        Result Reload();
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/MaskMode.cs b/src/Ryujinx.Horizon/Sdk/Ngc/MaskMode.cs
new file mode 100644
index 000000000..da0a4e6f6
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/MaskMode.cs
@@ -0,0 +1,8 @@
+namespace Ryujinx.Horizon.Sdk.Ngc
+{
+    enum MaskMode
+    {
+        Overwrite = 0,
+        ReplaceByOneCharacter = 1,
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/NgcResult.cs b/src/Ryujinx.Horizon/Sdk/Ngc/NgcResult.cs
new file mode 100644
index 000000000..c53687fe8
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/NgcResult.cs
@@ -0,0 +1,16 @@
+using Ryujinx.Horizon.Common;
+
+namespace Ryujinx.Horizon.Sdk.Ngc
+{
+    static class NgcResult
+    {
+        private const int ModuleId = 146;
+
+        public static Result InvalidPointer => new(ModuleId, 3);
+        public static Result InvalidSize => new(ModuleId, 4);
+        public static Result InvalidUtf8Encoding => new(ModuleId, 5);
+        public static Result AllocationFailed => new(ModuleId, 101);
+        public static Result DataAccessError => new(ModuleId, 102);
+        public static Result GenericUtf8Error => new(ModuleId, 103);
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/ProfanityFilterFlags.cs b/src/Ryujinx.Horizon/Sdk/Ngc/ProfanityFilterFlags.cs
new file mode 100644
index 000000000..19542c006
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/ProfanityFilterFlags.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Ngc
+{
+    [Flags]
+    enum ProfanityFilterFlags
+    {
+        None = 0,
+        MatchNormalizedFormKC = 1 << 0,
+        MatchSimilarForm = 1 << 1,
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/ProfanityFilterOption.cs b/src/Ryujinx.Horizon/Sdk/Ngc/ProfanityFilterOption.cs
new file mode 100644
index 000000000..4a2ab715e
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/ProfanityFilterOption.cs
@@ -0,0 +1,23 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Ngc
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x14, Pack = 0x4)]
+    readonly struct ProfanityFilterOption
+    {
+        public readonly SkipMode SkipAtSignCheck;
+        public readonly MaskMode MaskMode;
+        public readonly ProfanityFilterFlags Flags;
+        public readonly uint SystemRegionMask;
+        public readonly uint Reserved;
+
+        public ProfanityFilterOption(SkipMode skipAtSignCheck, MaskMode maskMode, ProfanityFilterFlags flags, uint systemRegionMask)
+        {
+            SkipAtSignCheck = skipAtSignCheck;
+            MaskMode = maskMode;
+            Flags = flags;
+            SystemRegionMask = systemRegionMask;
+            Reserved = 0;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/SkipMode.cs b/src/Ryujinx.Horizon/Sdk/Ngc/SkipMode.cs
new file mode 100644
index 000000000..69ab48eb9
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ngc/SkipMode.cs
@@ -0,0 +1,8 @@
+namespace Ryujinx.Horizon.Sdk.Ngc
+{
+    enum SkipMode
+    {
+        DoNotSkip,
+        SkipAtSignCheck,
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ovln/IReceiverService.cs b/src/Ryujinx.Horizon/Sdk/Ovln/IReceiverService.cs
new file mode 100644
index 000000000..f59e8002d
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ovln/IReceiverService.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Ovln
+{
+    interface IReceiverService : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ovln/ISenderService.cs b/src/Ryujinx.Horizon/Sdk/Ovln/ISenderService.cs
new file mode 100644
index 000000000..93323ba50
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Ovln/ISenderService.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Ovln
+{
+    interface ISenderService : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Psc/IPmControl.cs b/src/Ryujinx.Horizon/Sdk/Psc/IPmControl.cs
new file mode 100644
index 000000000..6a71d6842
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Psc/IPmControl.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Psc
+{
+    interface IPmControl : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Psc/IPmService.cs b/src/Ryujinx.Horizon/Sdk/Psc/IPmService.cs
new file mode 100644
index 000000000..c58665818
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Psc/IPmService.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Psc
+{
+    interface IPmService : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Psc/IPmStateLock.cs b/src/Ryujinx.Horizon/Sdk/Psc/IPmStateLock.cs
new file mode 100644
index 000000000..41ead492e
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Psc/IPmStateLock.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Psc
+{
+    interface IPmStateLock : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Sf/Hipc/ServerManager.cs b/src/Ryujinx.Horizon/Sdk/Sf/Hipc/ServerManager.cs
index 9ac2a337e..65a433038 100644
--- a/src/Ryujinx.Horizon/Sdk/Sf/Hipc/ServerManager.cs
+++ b/src/Ryujinx.Horizon/Sdk/Sf/Hipc/ServerManager.cs
@@ -31,7 +31,10 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
 
             if (allocator != null)
             {
-                _pointerBuffersBaseAddress = allocator.Allocate((ulong)maxSessions * (ulong)options.PointerBufferSize);
+                if (options.PointerBufferSize != 0)
+                {
+                    _pointerBuffersBaseAddress = allocator.Allocate((ulong)maxSessions * (ulong)options.PointerBufferSize);
+                }
 
                 if (options.CanDeferInvokeRequest)
                 {
diff --git a/src/Ryujinx.Horizon/Sdk/Sm/ServiceName.cs b/src/Ryujinx.Horizon/Sdk/Sm/ServiceName.cs
index f90d39c22..1233fad8a 100644
--- a/src/Ryujinx.Horizon/Sdk/Sm/ServiceName.cs
+++ b/src/Ryujinx.Horizon/Sdk/Sm/ServiceName.cs
@@ -1,10 +1,11 @@
 using System;
 using System.Runtime.InteropServices;
+using System.Text;
 
 namespace Ryujinx.Horizon.Sdk.Sm
 {
     [StructLayout(LayoutKind.Sequential, Pack = 1)]
-    readonly struct ServiceName
+    public readonly struct ServiceName
     {
         public static ServiceName Invalid { get; } = new(0);
 
@@ -78,7 +79,7 @@ namespace Ryujinx.Horizon.Sdk.Sm
 
         public override string ToString()
         {
-            string name = string.Empty;
+            StringBuilder nameBuilder = new();
 
             for (int index = 0; index < sizeof(ulong); index++)
             {
@@ -89,10 +90,10 @@ namespace Ryujinx.Horizon.Sdk.Sm
                     break;
                 }
 
-                name += (char)character;
+                nameBuilder.Append((char)character);
             }
 
-            return name;
+            return nameBuilder.ToString();
         }
     }
 }
diff --git a/src/Ryujinx.Horizon/Sdk/Sm/SmApi.cs b/src/Ryujinx.Horizon/Sdk/Sm/SmApi.cs
index 3e5635bf1..1ab400bde 100644
--- a/src/Ryujinx.Horizon/Sdk/Sm/SmApi.cs
+++ b/src/Ryujinx.Horizon/Sdk/Sm/SmApi.cs
@@ -5,7 +5,7 @@ using System;
 
 namespace Ryujinx.Horizon.Sdk.Sm
 {
-    class SmApi
+    public class SmApi : IDisposable
     {
         private const string SmName = "sm:";
 
@@ -109,5 +109,17 @@ namespace Ryujinx.Horizon.Sdk.Sm
 
             return ServiceUtil.SendRequest(out _, _portHandle, 4, sendPid: true, data);
         }
+
+        public void Dispose()
+        {
+            if (_portHandle != 0)
+            {
+                HorizonStatic.Syscall.CloseHandle(_portHandle);
+
+                _portHandle = 0;
+            }
+
+            GC.SuppressFinalize(this);
+        }
     }
 }
diff --git a/src/Ryujinx.Horizon/Sdk/Srepo/ISrepoService.cs b/src/Ryujinx.Horizon/Sdk/Srepo/ISrepoService.cs
new file mode 100644
index 000000000..9a1f4ba4f
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Srepo/ISrepoService.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Srepo
+{
+    interface ISrepoService : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Usb/IClientRootSession.cs b/src/Ryujinx.Horizon/Sdk/Usb/IClientRootSession.cs
new file mode 100644
index 000000000..4975ad6b5
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Usb/IClientRootSession.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Usb
+{
+    interface IClientRootSession : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Usb/IDsRootSession.cs b/src/Ryujinx.Horizon/Sdk/Usb/IDsRootSession.cs
new file mode 100644
index 000000000..32d7aba6c
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Usb/IDsRootSession.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Usb
+{
+    interface IDsRootSession : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Usb/IPdCradleManager.cs b/src/Ryujinx.Horizon/Sdk/Usb/IPdCradleManager.cs
new file mode 100644
index 000000000..0d3865114
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Usb/IPdCradleManager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Usb
+{
+    interface IPdCradleManager : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Usb/IPdManager.cs b/src/Ryujinx.Horizon/Sdk/Usb/IPdManager.cs
new file mode 100644
index 000000000..d63821516
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Usb/IPdManager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Usb
+{
+    interface IPdManager : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Usb/IPdManufactureManager.cs b/src/Ryujinx.Horizon/Sdk/Usb/IPdManufactureManager.cs
new file mode 100644
index 000000000..18bac3ea4
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Usb/IPdManufactureManager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Usb
+{
+    interface IPdManufactureManager : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Usb/IPmObserverService.cs b/src/Ryujinx.Horizon/Sdk/Usb/IPmObserverService.cs
new file mode 100644
index 000000000..ef4cc65af
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Usb/IPmObserverService.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Usb
+{
+    interface IPmObserverService : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Usb/IPmService.cs b/src/Ryujinx.Horizon/Sdk/Usb/IPmService.cs
new file mode 100644
index 000000000..b8d177bd0
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Usb/IPmService.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Usb
+{
+    interface IPmService : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Usb/IQdbManager.cs b/src/Ryujinx.Horizon/Sdk/Usb/IQdbManager.cs
new file mode 100644
index 000000000..a8f61f058
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Usb/IQdbManager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Usb
+{
+    interface IQdbManager : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Wlan/IDetectManager.cs b/src/Ryujinx.Horizon/Sdk/Wlan/IDetectManager.cs
new file mode 100644
index 000000000..2629ed6ef
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Wlan/IDetectManager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Wlan
+{
+    interface IDetectManager : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Wlan/IGeneralServiceCreator.cs b/src/Ryujinx.Horizon/Sdk/Wlan/IGeneralServiceCreator.cs
new file mode 100644
index 000000000..fa7704e79
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Wlan/IGeneralServiceCreator.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Wlan
+{
+    interface IGeneralServiceCreator : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Wlan/IInfraManager.cs b/src/Ryujinx.Horizon/Sdk/Wlan/IInfraManager.cs
new file mode 100644
index 000000000..6739e1686
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Wlan/IInfraManager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Wlan
+{
+    interface IInfraManager : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Wlan/ILocalGetActionFrame.cs b/src/Ryujinx.Horizon/Sdk/Wlan/ILocalGetActionFrame.cs
new file mode 100644
index 000000000..171ddba79
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Wlan/ILocalGetActionFrame.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Wlan
+{
+    interface ILocalGetActionFrame : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Wlan/ILocalGetFrame.cs b/src/Ryujinx.Horizon/Sdk/Wlan/ILocalGetFrame.cs
new file mode 100644
index 000000000..0a8d587ed
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Wlan/ILocalGetFrame.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Wlan
+{
+    interface ILocalGetFrame : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Wlan/ILocalManager.cs b/src/Ryujinx.Horizon/Sdk/Wlan/ILocalManager.cs
new file mode 100644
index 000000000..cba9eb918
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Wlan/ILocalManager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Wlan
+{
+    interface ILocalManager : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Wlan/IPrivateServiceCreator.cs b/src/Ryujinx.Horizon/Sdk/Wlan/IPrivateServiceCreator.cs
new file mode 100644
index 000000000..5ab2e0eef
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Wlan/IPrivateServiceCreator.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Wlan
+{
+    interface IPrivateServiceCreator : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Wlan/ISfDriverServiceCreator.cs b/src/Ryujinx.Horizon/Sdk/Wlan/ISfDriverServiceCreator.cs
new file mode 100644
index 000000000..78d32e6ae
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Wlan/ISfDriverServiceCreator.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Wlan
+{
+    interface ISfDriverServiceCreator : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Wlan/ISocketGetFrame.cs b/src/Ryujinx.Horizon/Sdk/Wlan/ISocketGetFrame.cs
new file mode 100644
index 000000000..c6d3e0b9a
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Wlan/ISocketGetFrame.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Wlan
+{
+    interface ISocketGetFrame : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Wlan/ISocketManager.cs b/src/Ryujinx.Horizon/Sdk/Wlan/ISocketManager.cs
new file mode 100644
index 000000000..dcdfef48c
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Wlan/ISocketManager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Wlan
+{
+    interface ISocketManager : IServiceObject
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/ServiceTable.cs b/src/Ryujinx.Horizon/ServiceTable.cs
index d47f91bf9..c79328a96 100644
--- a/src/Ryujinx.Horizon/ServiceTable.cs
+++ b/src/Ryujinx.Horizon/ServiceTable.cs
@@ -1,6 +1,16 @@
 using Ryujinx.Horizon.Bcat;
+using Ryujinx.Horizon.Hshl;
+using Ryujinx.Horizon.Ins;
+using Ryujinx.Horizon.Lbl;
 using Ryujinx.Horizon.LogManager;
+using Ryujinx.Horizon.MmNv;
+using Ryujinx.Horizon.Ngc;
+using Ryujinx.Horizon.Ovln;
 using Ryujinx.Horizon.Prepo;
+using Ryujinx.Horizon.Psc;
+using Ryujinx.Horizon.Srepo;
+using Ryujinx.Horizon.Usb;
+using Ryujinx.Horizon.Wlan;
 using System.Collections.Generic;
 using System.Threading;
 
@@ -22,9 +32,19 @@ namespace Ryujinx.Horizon
                 entries.Add(new ServiceEntry(T.Main, this, options));
             }
 
-            RegisterService<LmMain>();
-            RegisterService<PrepoMain>();
             RegisterService<BcatMain>();
+            RegisterService<HshlMain>();
+            RegisterService<InsMain>();
+            RegisterService<LblMain>();
+            RegisterService<LmMain>();
+            RegisterService<MmNvMain>();
+            RegisterService<NgcMain>();
+            RegisterService<OvlnMain>();
+            RegisterService<PrepoMain>();
+            RegisterService<PscMain>();
+            RegisterService<SrepoMain>();
+            RegisterService<UsbMain>();
+            RegisterService<WlanMain>();
 
             _totalServices = entries.Count;
 
diff --git a/src/Ryujinx.Horizon/Srepo/Ipc/SrepoService.cs b/src/Ryujinx.Horizon/Srepo/Ipc/SrepoService.cs
new file mode 100644
index 000000000..501eb5fed
--- /dev/null
+++ b/src/Ryujinx.Horizon/Srepo/Ipc/SrepoService.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Srepo;
+
+namespace Ryujinx.Horizon.Srepo.Ipc
+{
+    partial class SrepoService : ISrepoService
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Srepo/SrepoIpcServer.cs b/src/Ryujinx.Horizon/Srepo/SrepoIpcServer.cs
new file mode 100644
index 000000000..a971f97b8
--- /dev/null
+++ b/src/Ryujinx.Horizon/Srepo/SrepoIpcServer.cs
@@ -0,0 +1,46 @@
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using Ryujinx.Horizon.Sdk.Sm;
+using Ryujinx.Horizon.Srepo.Ipc;
+
+namespace Ryujinx.Horizon.Srepo
+{
+    class SrepoIpcServer
+    {
+        private const int SrepoAMaxSessionsCount = 2;
+        private const int SrepoUMaxSessionsCount = 30;
+        private const int TotalMaxSessionsCount = SrepoAMaxSessionsCount + SrepoUMaxSessionsCount;
+
+        private const int PointerBufferSize = 0x80;
+        private const int MaxDomains = 32;
+        private const int MaxDomainObjects = 192;
+        private const int MaxPortsCount = 2;
+
+        private static readonly ManagerOptions _options = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
+
+        private SmApi _sm;
+        private ServerManager _serverManager;
+
+        public void Initialize()
+        {
+            HeapAllocator allocator = new();
+
+            _sm = new SmApi();
+            _sm.Initialize().AbortOnFailure();
+
+            _serverManager = new ServerManager(allocator, _sm, MaxPortsCount, _options, TotalMaxSessionsCount);
+
+            _serverManager.RegisterObjectForServer(new SrepoService(), ServiceName.Encode("srepo:a"), SrepoAMaxSessionsCount); // 5.0.0+
+            _serverManager.RegisterObjectForServer(new SrepoService(), ServiceName.Encode("srepo:u"), SrepoUMaxSessionsCount); // 5.0.0+
+        }
+
+        public void ServiceRequests()
+        {
+            _serverManager.ServiceRequests();
+        }
+
+        public void Shutdown()
+        {
+            _serverManager.Dispose();
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Srepo/SrepoMain.cs b/src/Ryujinx.Horizon/Srepo/SrepoMain.cs
new file mode 100644
index 000000000..78d813ac9
--- /dev/null
+++ b/src/Ryujinx.Horizon/Srepo/SrepoMain.cs
@@ -0,0 +1,17 @@
+namespace Ryujinx.Horizon.Srepo
+{
+    class SrepoMain : IService
+    {
+        public static void Main(ServiceTable serviceTable)
+        {
+            SrepoIpcServer ipcServer = new();
+
+            ipcServer.Initialize();
+
+            serviceTable.SignalServiceReady();
+
+            ipcServer.ServiceRequests();
+            ipcServer.Shutdown();
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Usb/Ipc/ClientRootSession.cs b/src/Ryujinx.Horizon/Usb/Ipc/ClientRootSession.cs
new file mode 100644
index 000000000..2167ebcad
--- /dev/null
+++ b/src/Ryujinx.Horizon/Usb/Ipc/ClientRootSession.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Usb;
+
+namespace Ryujinx.Horizon.Usb.Ipc
+{
+    partial class ClientRootSession : IClientRootSession
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Usb/Ipc/DsRootSession.cs b/src/Ryujinx.Horizon/Usb/Ipc/DsRootSession.cs
new file mode 100644
index 000000000..8a84537f8
--- /dev/null
+++ b/src/Ryujinx.Horizon/Usb/Ipc/DsRootSession.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Usb;
+
+namespace Ryujinx.Horizon.Usb.Ipc
+{
+    partial class DsRootSession : IDsRootSession
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Usb/Ipc/PdCradleManager.cs b/src/Ryujinx.Horizon/Usb/Ipc/PdCradleManager.cs
new file mode 100644
index 000000000..27e1c4e37
--- /dev/null
+++ b/src/Ryujinx.Horizon/Usb/Ipc/PdCradleManager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Usb;
+
+namespace Ryujinx.Horizon.Usb.Ipc
+{
+    partial class PdCradleManager : IPdCradleManager
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Usb/Ipc/PdManager.cs b/src/Ryujinx.Horizon/Usb/Ipc/PdManager.cs
new file mode 100644
index 000000000..c501e3f20
--- /dev/null
+++ b/src/Ryujinx.Horizon/Usb/Ipc/PdManager.cs
@@ -0,0 +1,9 @@
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using Ryujinx.Horizon.Sdk.Usb;
+
+namespace Ryujinx.Horizon.Usb.Ipc
+{
+    partial class PdManager : IPdManager
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Usb/Ipc/PdManufactureManager.cs b/src/Ryujinx.Horizon/Usb/Ipc/PdManufactureManager.cs
new file mode 100644
index 000000000..04f78b9c2
--- /dev/null
+++ b/src/Ryujinx.Horizon/Usb/Ipc/PdManufactureManager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Usb;
+
+namespace Ryujinx.Horizon.Usb.Ipc
+{
+    partial class PdManufactureManager : IPdManufactureManager
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Usb/Ipc/PmObserverService.cs b/src/Ryujinx.Horizon/Usb/Ipc/PmObserverService.cs
new file mode 100644
index 000000000..e2edf4cb9
--- /dev/null
+++ b/src/Ryujinx.Horizon/Usb/Ipc/PmObserverService.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Usb;
+
+namespace Ryujinx.Horizon.Usb.Ipc
+{
+    partial class PmObserverService : IPmObserverService
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Usb/Ipc/PmService.cs b/src/Ryujinx.Horizon/Usb/Ipc/PmService.cs
new file mode 100644
index 000000000..625aaa497
--- /dev/null
+++ b/src/Ryujinx.Horizon/Usb/Ipc/PmService.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Usb;
+
+namespace Ryujinx.Horizon.Usb.Ipc
+{
+    partial class PmService : IPmService
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Usb/Ipc/QdbManager.cs b/src/Ryujinx.Horizon/Usb/Ipc/QdbManager.cs
new file mode 100644
index 000000000..1421142fb
--- /dev/null
+++ b/src/Ryujinx.Horizon/Usb/Ipc/QdbManager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Usb;
+
+namespace Ryujinx.Horizon.Usb.Ipc
+{
+    partial class QdbManager : IQdbManager
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Usb/UsbIpcServer.cs b/src/Ryujinx.Horizon/Usb/UsbIpcServer.cs
new file mode 100644
index 000000000..a9158b507
--- /dev/null
+++ b/src/Ryujinx.Horizon/Usb/UsbIpcServer.cs
@@ -0,0 +1,71 @@
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using Ryujinx.Horizon.Sdk.Sm;
+using Ryujinx.Horizon.Usb.Ipc;
+
+namespace Ryujinx.Horizon.Usb
+{
+    class UsbIpcServer
+    {
+        private const int UsbDsMaxSessionsCount = 4;
+        private const int UsbHsMaxSessionsCount = 20;
+        private const int UsbHsAMaxSessionsCount = 3;
+        private const int UsbObsvMaxSessionsCount = 2;
+        private const int UsbPdMaxSessionsCount = 6;
+        private const int UsbPdCMaxSessionsCount = 4;
+        private const int UsbPdMMaxSessionsCount = 1;
+        private const int UsbPmMaxSessionsCount = 5;
+        private const int UsbQdbMaxSessionsCount = 4;
+        private const int TotalMaxSessionsCount =
+            UsbDsMaxSessionsCount +
+            UsbHsMaxSessionsCount +
+            UsbHsAMaxSessionsCount +
+            UsbObsvMaxSessionsCount +
+            UsbPdMaxSessionsCount +
+            UsbPdCMaxSessionsCount +
+            UsbPdMMaxSessionsCount +
+            UsbPmMaxSessionsCount +
+            UsbQdbMaxSessionsCount;
+
+        private const int PointerBufferSize = 0;
+        private const int MaxDomains = 0;
+        private const int MaxDomainObjects = 0;
+        private const int MaxPortsCount = 9;
+
+        private static readonly ManagerOptions _options = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
+
+        private SmApi _sm;
+        private ServerManager _serverManager;
+
+        public void Initialize()
+        {
+            HeapAllocator allocator = new();
+
+            _sm = new SmApi();
+            _sm.Initialize().AbortOnFailure();
+
+            _serverManager = new ServerManager(allocator, _sm, MaxPortsCount, _options, TotalMaxSessionsCount);
+
+#pragma warning disable IDE0055 // Disable formatting
+            _serverManager.RegisterObjectForServer(new DsRootSession(),        ServiceName.Encode("usb:ds"),   UsbDsMaxSessionsCount);
+            _serverManager.RegisterObjectForServer(new ClientRootSession(),    ServiceName.Encode("usb:hs"),   UsbHsMaxSessionsCount);
+            _serverManager.RegisterObjectForServer(new ClientRootSession(),    ServiceName.Encode("usb:hs:a"), UsbHsAMaxSessionsCount);  // 7.0.0+
+            _serverManager.RegisterObjectForServer(new PmObserverService(),    ServiceName.Encode("usb:obsv"), UsbObsvMaxSessionsCount); // 8.0.0+
+            _serverManager.RegisterObjectForServer(new PdManager(),            ServiceName.Encode("usb:pd"),   UsbPdMaxSessionsCount);
+            _serverManager.RegisterObjectForServer(new PdCradleManager(),      ServiceName.Encode("usb:pd:c"), UsbPdCMaxSessionsCount);
+            _serverManager.RegisterObjectForServer(new PdManufactureManager(), ServiceName.Encode("usb:pd:m"), UsbPdMMaxSessionsCount);  // 1.0.0
+            _serverManager.RegisterObjectForServer(new PmService(),            ServiceName.Encode("usb:pm"),   UsbPmMaxSessionsCount);
+            _serverManager.RegisterObjectForServer(new QdbManager(),           ServiceName.Encode("usb:qdb"),  UsbQdbMaxSessionsCount);  // 7.0.0+
+#pragma warning restore IDE0055
+        }
+
+        public void ServiceRequests()
+        {
+            _serverManager.ServiceRequests();
+        }
+
+        public void Shutdown()
+        {
+            _serverManager.Dispose();
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Usb/UsbMain.cs b/src/Ryujinx.Horizon/Usb/UsbMain.cs
new file mode 100644
index 000000000..c54b39a65
--- /dev/null
+++ b/src/Ryujinx.Horizon/Usb/UsbMain.cs
@@ -0,0 +1,17 @@
+namespace Ryujinx.Horizon.Usb
+{
+    class UsbMain : IService
+    {
+        public static void Main(ServiceTable serviceTable)
+        {
+            UsbIpcServer ipcServer = new();
+
+            ipcServer.Initialize();
+
+            serviceTable.SignalServiceReady();
+
+            ipcServer.ServiceRequests();
+            ipcServer.Shutdown();
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Wlan/Ipc/DetectManager.cs b/src/Ryujinx.Horizon/Wlan/Ipc/DetectManager.cs
new file mode 100644
index 000000000..595592f77
--- /dev/null
+++ b/src/Ryujinx.Horizon/Wlan/Ipc/DetectManager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Wlan;
+
+namespace Ryujinx.Horizon.Wlan.Ipc
+{
+    partial class DetectManager : IDetectManager
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Wlan/Ipc/GeneralServiceCreator.cs b/src/Ryujinx.Horizon/Wlan/Ipc/GeneralServiceCreator.cs
new file mode 100644
index 000000000..7f651d227
--- /dev/null
+++ b/src/Ryujinx.Horizon/Wlan/Ipc/GeneralServiceCreator.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Wlan;
+
+namespace Ryujinx.Horizon.Wlan.Ipc
+{
+    partial class GeneralServiceCreator : IGeneralServiceCreator
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Wlan/Ipc/InfraManager.cs b/src/Ryujinx.Horizon/Wlan/Ipc/InfraManager.cs
new file mode 100644
index 000000000..d69c73b5c
--- /dev/null
+++ b/src/Ryujinx.Horizon/Wlan/Ipc/InfraManager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Wlan;
+
+namespace Ryujinx.Horizon.Wlan.Ipc
+{
+    partial class InfraManager : IInfraManager
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Wlan/Ipc/LocalGetActionFrame.cs b/src/Ryujinx.Horizon/Wlan/Ipc/LocalGetActionFrame.cs
new file mode 100644
index 000000000..8458655ed
--- /dev/null
+++ b/src/Ryujinx.Horizon/Wlan/Ipc/LocalGetActionFrame.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Wlan;
+
+namespace Ryujinx.Horizon.Wlan.Ipc
+{
+    partial class LocalGetActionFrame : ILocalGetActionFrame
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Wlan/Ipc/LocalGetFrame.cs b/src/Ryujinx.Horizon/Wlan/Ipc/LocalGetFrame.cs
new file mode 100644
index 000000000..5ce8724db
--- /dev/null
+++ b/src/Ryujinx.Horizon/Wlan/Ipc/LocalGetFrame.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Wlan;
+
+namespace Ryujinx.Horizon.Wlan.Ipc
+{
+    partial class LocalGetFrame : ILocalGetFrame
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Wlan/Ipc/LocalManager.cs b/src/Ryujinx.Horizon/Wlan/Ipc/LocalManager.cs
new file mode 100644
index 000000000..789ac1903
--- /dev/null
+++ b/src/Ryujinx.Horizon/Wlan/Ipc/LocalManager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Wlan;
+
+namespace Ryujinx.Horizon.Wlan.Ipc
+{
+    partial class LocalManager : ILocalManager
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Wlan/Ipc/PrivateServiceCreator.cs b/src/Ryujinx.Horizon/Wlan/Ipc/PrivateServiceCreator.cs
new file mode 100644
index 000000000..c25723f91
--- /dev/null
+++ b/src/Ryujinx.Horizon/Wlan/Ipc/PrivateServiceCreator.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Wlan;
+
+namespace Ryujinx.Horizon.Wlan.Ipc
+{
+    partial class PrivateServiceCreator : IPrivateServiceCreator
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Wlan/Ipc/SfDriverServiceCreator.cs b/src/Ryujinx.Horizon/Wlan/Ipc/SfDriverServiceCreator.cs
new file mode 100644
index 000000000..fd74024b6
--- /dev/null
+++ b/src/Ryujinx.Horizon/Wlan/Ipc/SfDriverServiceCreator.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Wlan;
+
+namespace Ryujinx.Horizon.Wlan.Ipc
+{
+    partial class SfDriverServiceCreator : ISfDriverServiceCreator
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Wlan/Ipc/SocketGetFrame.cs b/src/Ryujinx.Horizon/Wlan/Ipc/SocketGetFrame.cs
new file mode 100644
index 000000000..5c1663f68
--- /dev/null
+++ b/src/Ryujinx.Horizon/Wlan/Ipc/SocketGetFrame.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Wlan;
+
+namespace Ryujinx.Horizon.Wlan.Ipc
+{
+    partial class SocketGetFrame : ISocketGetFrame
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Wlan/Ipc/SocketManager.cs b/src/Ryujinx.Horizon/Wlan/Ipc/SocketManager.cs
new file mode 100644
index 000000000..a112e7452
--- /dev/null
+++ b/src/Ryujinx.Horizon/Wlan/Ipc/SocketManager.cs
@@ -0,0 +1,8 @@
+using Ryujinx.Horizon.Sdk.Wlan;
+
+namespace Ryujinx.Horizon.Wlan.Ipc
+{
+    partial class SocketManager : ISocketManager
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Wlan/WlanIpcServer.cs b/src/Ryujinx.Horizon/Wlan/WlanIpcServer.cs
new file mode 100644
index 000000000..eb13f028c
--- /dev/null
+++ b/src/Ryujinx.Horizon/Wlan/WlanIpcServer.cs
@@ -0,0 +1,59 @@
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using Ryujinx.Horizon.Sdk.Sm;
+using Ryujinx.Horizon.Wlan.Ipc;
+
+namespace Ryujinx.Horizon.Wlan
+{
+    class WlanIpcServer
+    {
+        private const int WlanOtherMaxSessionsCount = 10;
+        private const int WlanDtcMaxSessionsCount = 4;
+        private const int WlanMaxSessionsCount = 30;
+        private const int WlanNdMaxSessionsCount = 5;
+        private const int WlanPMaxSessionsCount = 30;
+        private const int TotalMaxSessionsCount = WlanDtcMaxSessionsCount + WlanMaxSessionsCount + WlanNdMaxSessionsCount + WlanPMaxSessionsCount + WlanOtherMaxSessionsCount * 6;
+
+        private const int PointerBufferSize = 0x1000;
+        private const int MaxDomains = 16;
+        private const int MaxDomainObjects = 10;
+        private const int MaxPortsCount = 10;
+
+        private static readonly ManagerOptions _options = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
+
+        private SmApi _sm;
+        private ServerManager _serverManager;
+
+        public void Initialize()
+        {
+            HeapAllocator allocator = new();
+
+            _sm = new SmApi();
+            _sm.Initialize().AbortOnFailure();
+
+            _serverManager = new ServerManager(allocator, _sm, MaxPortsCount, _options, TotalMaxSessionsCount);
+
+#pragma warning disable IDE0055 // Disable formatting
+            _serverManager.RegisterObjectForServer(new GeneralServiceCreator(),  ServiceName.Encode("wlan"),     WlanMaxSessionsCount);      // 15.0.0+
+            _serverManager.RegisterObjectForServer(new DetectManager(),          ServiceName.Encode("wlan:dtc"), WlanDtcMaxSessionsCount);   // 6.0.0-14.1.2
+            _serverManager.RegisterObjectForServer(new InfraManager(),           ServiceName.Encode("wlan:inf"), WlanOtherMaxSessionsCount); // 1.0.0-14.1.2
+            _serverManager.RegisterObjectForServer(new LocalManager(),           ServiceName.Encode("wlan:lcl"), WlanOtherMaxSessionsCount); // 1.0.0-14.1.2
+            _serverManager.RegisterObjectForServer(new LocalGetFrame(),          ServiceName.Encode("wlan:lg"),  WlanOtherMaxSessionsCount); // 1.0.0-14.1.2
+            _serverManager.RegisterObjectForServer(new LocalGetActionFrame(),    ServiceName.Encode("wlan:lga"), WlanOtherMaxSessionsCount); // 1.0.0-14.1.2
+            _serverManager.RegisterObjectForServer(new SfDriverServiceCreator(), ServiceName.Encode("wlan:nd"),  WlanNdMaxSessionsCount);    // 15.0.0+
+            _serverManager.RegisterObjectForServer(new PrivateServiceCreator(),  ServiceName.Encode("wlan:p"),   WlanPMaxSessionsCount);     // 15.0.0+
+            _serverManager.RegisterObjectForServer(new SocketGetFrame(),         ServiceName.Encode("wlan:sg"),  WlanOtherMaxSessionsCount); // 1.0.0-14.1.2
+            _serverManager.RegisterObjectForServer(new SocketManager(),          ServiceName.Encode("wlan:soc"), WlanOtherMaxSessionsCount); // 1.0.0-14.1.2
+#pragma warning restore IDE0055
+        }
+
+        public void ServiceRequests()
+        {
+            _serverManager.ServiceRequests();
+        }
+
+        public void Shutdown()
+        {
+            _serverManager.Dispose();
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Wlan/WlanMain.cs b/src/Ryujinx.Horizon/Wlan/WlanMain.cs
new file mode 100644
index 000000000..1381a8cb0
--- /dev/null
+++ b/src/Ryujinx.Horizon/Wlan/WlanMain.cs
@@ -0,0 +1,17 @@
+namespace Ryujinx.Horizon.Wlan
+{
+    class WlanMain : IService
+    {
+        public static void Main(ServiceTable serviceTable)
+        {
+            WlanIpcServer ipcServer = new();
+
+            ipcServer.Initialize();
+
+            serviceTable.SignalServiceReady();
+
+            ipcServer.ServiceRequests();
+            ipcServer.Shutdown();
+        }
+    }
+}
diff --git a/src/Ryujinx.ShaderTools/Program.cs b/src/Ryujinx.ShaderTools/Program.cs
index 55ff12bea..4211ab491 100644
--- a/src/Ryujinx.ShaderTools/Program.cs
+++ b/src/Ryujinx.ShaderTools/Program.cs
@@ -29,6 +29,12 @@ namespace Ryujinx.ShaderTools
             [Option("compute", Required = false, Default = false, HelpText = "Indicate that the shader is a compute shader.")]
             public bool Compute { get; set; }
 
+            [Option("vertex-as-compute", Required = false, Default = false, HelpText = "Indicate that the shader is a vertex shader and should be converted to compute.")]
+            public bool VertexAsCompute { get; set; }
+
+            [Option("vertex-passthrough", Required = false, Default = false, HelpText = "Indicate that the shader is a vertex passthrough shader for compute output.")]
+            public bool VertexPassthrough { get; set; }
+
             [Option("target-language", Required = false, Default = TargetLanguage.Glsl, HelpText = "Indicate the target shader language to use.")]
             public TargetLanguage TargetLanguage { get; set; }
 
@@ -54,8 +60,18 @@ namespace Ryujinx.ShaderTools
             byte[] data = File.ReadAllBytes(options.InputPath);
 
             TranslationOptions translationOptions = new(options.TargetLanguage, options.TargetApi, flags);
+            TranslatorContext translatorContext = Translator.CreateContext(0, new GpuAccessor(data), translationOptions);
 
-            ShaderProgram program = Translator.CreateContext(0, new GpuAccessor(data), translationOptions).Translate();
+            ShaderProgram program;
+
+            if (options.VertexPassthrough)
+            {
+                program = translatorContext.GenerateVertexPassthroughForCompute();
+            }
+            else
+            {
+                program = translatorContext.Translate(options.VertexAsCompute);
+            }
 
             if (options.OutputPath == null)
             {
diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs
index 37a02679b..a1bbea4d7 100644
--- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs
+++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs
@@ -1,5 +1,6 @@
 using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Configuration.Hid;
+using Ryujinx.Common.Configuration.Multiplayer;
 using Ryujinx.Common.Logging;
 using Ryujinx.Common.Utilities;
 using Ryujinx.Ui.Common.Configuration.System;
@@ -365,6 +366,11 @@ namespace Ryujinx.Ui.Common.Configuration
         /// </summary>
         public string PreferredGpu { get; set; }
 
+        /// <summary>
+        /// Multiplayer Mode
+        /// </summary>
+        public MultiplayerMode MultiplayerMode { get; set; }
+
         /// <summary>
         /// GUID for the network interface used by LAN (or 0 for default)
         /// </summary>
diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs
index 8440eca3e..bdb08e55b 100644
--- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs
+++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs
@@ -3,6 +3,7 @@ using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Configuration.Hid;
 using Ryujinx.Common.Configuration.Hid.Controller;
 using Ryujinx.Common.Configuration.Hid.Keyboard;
+using Ryujinx.Common.Configuration.Multiplayer;
 using Ryujinx.Common.Logging;
 using Ryujinx.Ui.Common.Configuration.System;
 using Ryujinx.Ui.Common.Configuration.Ui;
@@ -568,9 +569,15 @@ namespace Ryujinx.Ui.Common.Configuration
             /// </summary>
             public ReactiveObject<string> LanInterfaceId { get; private set; }
 
+            /// <summary>
+            /// Multiplayer Mode
+            /// </summary>
+            public ReactiveObject<MultiplayerMode> Mode { get; private set; }
+
             public MultiplayerSection()
             {
                 LanInterfaceId = new ReactiveObject<string>();
+                Mode = new ReactiveObject<MultiplayerMode>();
             }
         }
 
@@ -749,6 +756,7 @@ namespace Ryujinx.Ui.Common.Configuration
                 GraphicsBackend = Graphics.GraphicsBackend,
                 PreferredGpu = Graphics.PreferredGpu,
                 MultiplayerLanInterfaceId = Multiplayer.LanInterfaceId,
+                MultiplayerMode = Multiplayer.Mode,
             };
 
             return configurationFile;
@@ -804,6 +812,7 @@ namespace Ryujinx.Ui.Common.Configuration
             System.IgnoreMissingServices.Value = false;
             System.UseHypervisor.Value = true;
             Multiplayer.LanInterfaceId.Value = "0";
+            Multiplayer.Mode.Value = MultiplayerMode.Disabled;
             Ui.GuiColumns.FavColumn.Value = true;
             Ui.GuiColumns.IconColumn.Value = true;
             Ui.GuiColumns.AppColumn.Value = true;
@@ -1012,6 +1021,8 @@ namespace Ryujinx.Ui.Common.Configuration
                 configurationFileUpdated = true;
             }
 
+            // configurationFileFormat.Version == 13 -> LDN1
+
             if (configurationFileFormat.Version < 14)
             {
                 Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 14.");
@@ -1048,6 +1059,8 @@ namespace Ryujinx.Ui.Common.Configuration
                 configurationFileUpdated = true;
             }
 
+            // configurationFileFormat.Version == 19 -> LDN2
+
             if (configurationFileFormat.Version < 20)
             {
                 Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 20.");
@@ -1057,6 +1070,18 @@ namespace Ryujinx.Ui.Common.Configuration
                 configurationFileUpdated = true;
             }
 
+            if (configurationFileFormat.Version < 21)
+            {
+                Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 21.");
+
+                // Initialize network config.
+
+                configurationFileFormat.MultiplayerMode = MultiplayerMode.Disabled;
+                configurationFileFormat.MultiplayerLanInterfaceId = "0";
+
+                configurationFileUpdated = true;
+            }
+
             if (configurationFileFormat.Version < 22)
             {
                 Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 22.");
@@ -1520,6 +1545,7 @@ namespace Ryujinx.Ui.Common.Configuration
             }
 
             Multiplayer.LanInterfaceId.Value = configurationFileFormat.MultiplayerLanInterfaceId;
+            Multiplayer.Mode.Value = configurationFileFormat.MultiplayerMode;
 
             if (configurationFileUpdated)
             {
diff --git a/src/Ryujinx.Ui.LocaleGenerator/LocaleGenerator.cs b/src/Ryujinx.Ui.LocaleGenerator/LocaleGenerator.cs
index 6fb19fae3..d1b9131d0 100644
--- a/src/Ryujinx.Ui.LocaleGenerator/LocaleGenerator.cs
+++ b/src/Ryujinx.Ui.LocaleGenerator/LocaleGenerator.cs
@@ -1,5 +1,6 @@
 using Microsoft.CodeAnalysis;
 using System.Linq;
+using System.Text;
 
 namespace Ryujinx.Ui.LocaleGenerator
 {
@@ -15,15 +16,17 @@ namespace Ryujinx.Ui.LocaleGenerator
             context.RegisterSourceOutput(contents, (spc, content) =>
             {
                 var lines = content.Split('\n').Where(x => x.Trim().StartsWith("\"")).Select(x => x.Split(':')[0].Trim().Replace("\"", ""));
-                string enumSource = "namespace Ryujinx.Ava.Common.Locale;\n";
-                enumSource += "internal enum LocaleKeys\n{\n";
+                StringBuilder enumSourceBuilder = new();
+                enumSourceBuilder.AppendLine("namespace Ryujinx.Ava.Common.Locale;");
+                enumSourceBuilder.AppendLine("internal enum LocaleKeys");
+                enumSourceBuilder.AppendLine("{");
                 foreach (var line in lines)
                 {
-                    enumSource += $"    {line},\n";
+                    enumSourceBuilder.AppendLine($"    {line},");
                 }
-                enumSource += "}\n";
+                enumSourceBuilder.AppendLine("}");
 
-                spc.AddSource("LocaleKeys", enumSource);
+                spc.AddSource("LocaleKeys", enumSourceBuilder.ToString());
             });
         }
     }
diff --git a/src/Ryujinx/Ui/Applet/SwkbdAppletDialog.cs b/src/Ryujinx/Ui/Applet/SwkbdAppletDialog.cs
index 1ed08250f..c1f3d77c1 100644
--- a/src/Ryujinx/Ui/Applet/SwkbdAppletDialog.cs
+++ b/src/Ryujinx/Ui/Applet/SwkbdAppletDialog.cs
@@ -90,9 +90,9 @@ namespace Ryujinx.Ui.Applet
 
             switch (mode)
             {
-                case KeyboardMode.NumbersOnly:
-                    _validationInfoText += "<i>Must be numbers only.</i>";
-                    _checkInput = text => text.All(char.IsDigit);
+                case KeyboardMode.Numeric:
+                    _validationInfoText += "<i>Must be 0-9 or '.' only.</i>";
+                    _checkInput = text => text.All(NumericCharacterValidation.IsNumeric);
                     break;
                 case KeyboardMode.Alphabet:
                     _validationInfoText += "<i>Must be non CJK-characters only.</i>";
diff --git a/src/Ryujinx/Ui/MainWindow.cs b/src/Ryujinx/Ui/MainWindow.cs
index 5182dfcf8..0d6cab67b 100644
--- a/src/Ryujinx/Ui/MainWindow.cs
+++ b/src/Ryujinx/Ui/MainWindow.cs
@@ -13,6 +13,7 @@ using Ryujinx.Audio.Backends.SoundIo;
 using Ryujinx.Audio.Integration;
 using Ryujinx.Common;
 using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Configuration.Multiplayer;
 using Ryujinx.Common.Logging;
 using Ryujinx.Common.SystemInterop;
 using Ryujinx.Cpu;
@@ -207,6 +208,9 @@ namespace Ryujinx.Ui
             ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState;
             ConfigurationState.Instance.System.AudioVolume.Event += UpdateAudioVolumeState;
 
+            ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerMode;
+            ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateMultiplayerLanInterfaceId;
+
             if (ConfigurationState.Instance.Ui.StartFullscreen)
             {
                 _startFullScreen.Active = true;
@@ -331,6 +335,22 @@ namespace Ryujinx.Ui
             InputManager = new InputManager(new GTK3KeyboardDriver(this), new SDL2GamepadDriver());
         }
 
+        private void UpdateMultiplayerLanInterfaceId(object sender, ReactiveEventArgs<string> args)
+        {
+            if (_emulationContext != null)
+            {
+                _emulationContext.Configuration.MultiplayerLanInterfaceId = args.NewValue;
+            }
+        }
+
+        private void UpdateMultiplayerMode(object sender, ReactiveEventArgs<MultiplayerMode> args)
+        {
+            if (_emulationContext != null)
+            {
+                _emulationContext.Configuration.MultiplayerMode = args.NewValue;
+            }
+        }
+
         private void UpdateIgnoreMissingServicesState(object sender, ReactiveEventArgs<bool> args)
         {
             if (_emulationContext != null)
@@ -649,7 +669,8 @@ namespace Ryujinx.Ui
                 ConfigurationState.Instance.Graphics.AspectRatio,
                 ConfigurationState.Instance.System.AudioVolume,
                 ConfigurationState.Instance.System.UseHypervisor,
-                ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value);
+                ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value,
+                ConfigurationState.Instance.Multiplayer.Mode);
 
             _emulationContext = new HLE.Switch(configuration);
         }
diff --git a/src/Ryujinx/Ui/Windows/SettingsWindow.cs b/src/Ryujinx/Ui/Windows/SettingsWindow.cs
index f269d4125..ecd712e8e 100644
--- a/src/Ryujinx/Ui/Windows/SettingsWindow.cs
+++ b/src/Ryujinx/Ui/Windows/SettingsWindow.cs
@@ -5,6 +5,7 @@ using Ryujinx.Audio.Backends.SDL2;
 using Ryujinx.Audio.Backends.SoundIo;
 using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Configuration.Hid;
+using Ryujinx.Common.Configuration.Multiplayer;
 using Ryujinx.Common.GraphicsDriver;
 using Ryujinx.HLE.FileSystem;
 using Ryujinx.HLE.HOS.Services.Time.TimeZone;
@@ -18,6 +19,7 @@ using System.Globalization;
 using System.IO;
 using System.Net.NetworkInformation;
 using System.Reflection;
+using System.Text.RegularExpressions;
 using System.Threading.Tasks;
 using GUI = Gtk.Builder.ObjectAttribute;
 
@@ -88,6 +90,7 @@ namespace Ryujinx.Ui.Windows
         [GUI] Adjustment _systemTimeHourSpinAdjustment;
         [GUI] Adjustment _systemTimeMinuteSpinAdjustment;
         [GUI] ComboBoxText _multiLanSelect;
+        [GUI] ComboBoxText _multiModeSelect;
         [GUI] CheckButton _custThemeToggle;
         [GUI] Entry _custThemePath;
         [GUI] ToggleButton _browseThemePath;
@@ -367,6 +370,7 @@ namespace Ryujinx.Ui.Windows
             _graphicsBackend.Changed += (sender, e) => UpdatePreferredGpuComboBox();
             PopulateNetworkInterfaces();
             _multiLanSelect.SetActiveId(ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value);
+            _multiModeSelect.SetActiveId(ConfigurationState.Instance.Multiplayer.Mode.Value.ToString());
 
             _custThemePath.Buffer.Text = ConfigurationState.Instance.Ui.CustomThemePath;
             _resScaleText.Buffer.Text = ConfigurationState.Instance.Graphics.ResScaleCustom.Value.ToString();
@@ -665,6 +669,9 @@ namespace Ryujinx.Ui.Windows
 
             _previousVolumeLevel = ConfigurationState.Instance.System.AudioVolume.Value;
 
+            ConfigurationState.Instance.Multiplayer.Mode.Value = Enum.Parse<MultiplayerMode>(_multiModeSelect.ActiveId);
+            ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value = _multiLanSelect.ActiveId;
+
             if (_audioBackendSelect.GetActiveIter(out TreeIter activeIter))
             {
                 ConfigurationState.Instance.System.AudioBackend.Value = (AudioBackend)_audioBackendStore.GetValue(activeIter, 1);
diff --git a/src/Ryujinx/Ui/Windows/SettingsWindow.glade b/src/Ryujinx/Ui/Windows/SettingsWindow.glade
index 6ae0adc5a..c6d7e3b09 100644
--- a/src/Ryujinx/Ui/Windows/SettingsWindow.glade
+++ b/src/Ryujinx/Ui/Windows/SettingsWindow.glade
@@ -2951,6 +2951,96 @@
                         <property name="margin-right">10</property>
                         <property name="margin-top">5</property>
                         <property name="orientation">vertical</property>
+                        <child>
+                          <object class="GtkBox" id="CatMultiplayer">
+                            <property name="visible">True</property>
+                            <property name="can-focus">False</property>
+                            <property name="valign">start</property>
+                            <property name="margin-left">5</property>
+                            <property name="margin-right">5</property>
+                            <property name="orientation">vertical</property>
+                            <child>
+                              <object class="GtkLabel">
+                                <property name="visible">True</property>
+                                <property name="can-focus">False</property>
+                                <property name="halign">start</property>
+                                <property name="margin-bottom">5</property>
+                                <property name="label" translatable="yes">Multiplayer</property>
+                                <attributes>
+                                  <attribute name="weight" value="bold"/>
+                                </attributes>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">True</property>
+                                <property name="position">0</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkBox" id="MultiplayerOptions">
+                                <property name="visible">True</property>
+                                <property name="can-focus">False</property>
+                                <property name="valign">start</property>
+                                <property name="margin-left">10</property>
+                                <property name="margin-right">10</property>
+                                <property name="orientation">vertical</property>
+                                <child>
+                                  <object class="GtkBox" id="ModeBox">
+                                    <property name="visible">True</property>
+                                    <property name="can-focus">False</property>
+                                    <child>
+                                      <object class="GtkLabel">
+                                        <property name="visible">True</property>
+                                        <property name="can-focus">False</property>
+                                        <property name="tooltip-text" translatable="yes">Change Multiplayer Mode</property>
+                                        <property name="halign">end</property>
+                                        <property name="label" translatable="yes">Mode:</property>
+                                      </object>
+                                      <packing>
+                                        <property name="expand">False</property>
+                                        <property name="fill">True</property>
+                                        <property name="padding">5</property>
+                                        <property name="position">0</property>
+                                      </packing>
+                                    </child>
+                                    <child>
+                                      <object class="GtkComboBoxText" id="_multiModeSelect">
+                                        <property name="visible">True</property>
+                                        <property name="can-focus">False</property>
+                                        <property name="tooltip-text" translatable="yes">Change Multiplayer Mode</property>
+                                        <property name="active-id">Disabled</property>
+                                        <items>
+                                          <item id="Disabled" translatable="yes">Disabled</item>
+                                        </items>
+                                      </object>
+                                      <packing>
+                                        <property name="expand">False</property>
+                                        <property name="fill">True</property>
+                                        <property name="position">1</property>
+                                      </packing>
+                                    </child>
+                                  </object>
+                                  <packing>
+                                    <property name="expand">False</property>
+                                    <property name="fill">True</property>
+                                    <property name="position">3</property>
+                                  </packing>
+                                </child>
+                              </object>
+                              <packing>
+                                <property name="expand">True</property>
+                                <property name="fill">True</property>
+                                <property name="position">2</property>
+                              </packing>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="padding">5</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
                         <child>
                           <object class="GtkBox" id="CatLAN">
                             <property name="visible">True</property>