Merge branch 'master' into OpenGLSpirv

This commit is contained in:
sunshineinabox 2023-10-13 21:41:30 -07:00 committed by GitHub
commit 146a8990f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
455 changed files with 15786 additions and 2656 deletions

View file

@ -29,4 +29,4 @@ infra:
- TSRBerry - TSRBerry
default: default:
- @developers - '@developers'

View file

@ -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))

View file

@ -35,7 +35,7 @@ jobs:
fail-fast: false fail-fast: false
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-dotnet@v3 - uses: actions/setup-dotnet@v3
with: with:
@ -108,7 +108,7 @@ jobs:
configuration: [ Debug, Release ] configuration: [ Debug, Release ]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-dotnet@v3 - uses: actions/setup-dotnet@v3
with: with:
@ -135,9 +135,13 @@ jobs:
id: git_short_hash id: git_short_hash
run: echo "result=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT run: echo "result=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
- name: Publish macOS - name: Publish macOS Ryujinx.Ava
run: | 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 - name: Upload Ryujinx.Ava artifact
uses: actions/upload-artifact@v3 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 name: ava-ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-macos_universal
path: "publish_ava/*.tar.gz" path: "publish_ava/*.tar.gz"
if: github.event_name == 'pull_request' 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'

View file

@ -23,7 +23,7 @@ jobs:
format: format:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0

View file

@ -24,7 +24,7 @@ jobs:
RYUJINX_VERSION: "${{ inputs.ryujinx_version }}" RYUJINX_VERSION: "${{ inputs.ryujinx_version }}"
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
path: Ryujinx path: Ryujinx
@ -38,7 +38,7 @@ jobs:
run: | run: |
echo "git_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT echo "git_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
repository: flathub/org.ryujinx.Ryujinx repository: flathub/org.ryujinx.Ryujinx
token: ${{ secrets.RYUJINX_BOT_PAT }} token: ${{ secrets.RYUJINX_BOT_PAT }}

View file

@ -12,14 +12,24 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# Grab sources to get update_reviewers.py and reviewers.yml # Grab sources to get latest labeler.yml
- name: Fetch sources - name: Fetch sources
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
# Ensure we pin the source origin as pull_request_target run under forks. # Ensure we pin the source origin as pull_request_target run under forks.
fetch-depth: 0 fetch-depth: 0
repository: Ryujinx/Ryujinx repository: Ryujinx/Ryujinx
ref: master 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 - name: Update labels based on changes
uses: actions/labeler@v4 uses: actions/labeler@v4
@ -27,11 +37,11 @@ jobs:
sync-labels: true sync-labels: true
dot: true dot: true
- run: pip3 install PyGithub
- name: Assign reviewers - name: Assign reviewers
run: | 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 shell: bash
env: env:
MAKO_APP_ID: ${{ secrets.MAKO_APP_ID }}
MAKO_PRIVATE_KEY: ${{ secrets.MAKO_PRIVATE_KEY }} MAKO_PRIVATE_KEY: ${{ secrets.MAKO_PRIVATE_KEY }}
MAKO_INSTALLATION_ID: ${{ secrets.MAKO_INSTALLATION_ID }}

View file

@ -62,7 +62,7 @@ jobs:
DOTNET_RUNTIME_IDENTIFIER: win10-x64 DOTNET_RUNTIME_IDENTIFIER: win10-x64
RELEASE_ZIP_OS_NAME: win_x64 RELEASE_ZIP_OS_NAME: win_x64
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-dotnet@v3 - uses: actions/setup-dotnet@v3
with: with:
@ -150,7 +150,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: ${{ fromJSON(vars.JOB_TIMEOUT) }} timeout-minutes: ${{ fromJSON(vars.JOB_TIMEOUT) }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-dotnet@v3 - uses: actions/setup-dotnet@v3
with: 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 sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
shell: bash shell: bash
- name: Publish macOS - name: Publish macOS Ryujinx.Ava
run: | 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 - name: Pushing new release
uses: ncipollo/release-action@v1 uses: ncipollo/release-action@v1
with: with:
name: ${{ steps.version_info.outputs.build_version }} 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 }} 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)." body: "For more information about this release please check out the official [Changelog](https://github.com/Ryujinx/Ryujinx/wiki/Changelog)."
omitBodyDuringUpdate: true omitBodyDuringUpdate: true

147
CONTRIBUTING.md Normal file
View file

@ -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.

View file

@ -3,25 +3,25 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageVersion Include="Avalonia" Version="11.0.3" /> <PackageVersion Include="Avalonia" Version="11.0.4" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.0.3" /> <PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.0.4" />
<PackageVersion Include="Avalonia.Desktop" Version="11.0.3" /> <PackageVersion Include="Avalonia.Desktop" Version="11.0.4" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.0.3" /> <PackageVersion Include="Avalonia.Diagnostics" Version="11.0.4" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.0.3" /> <PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.0.4" />
<PackageVersion Include="Avalonia.Svg" Version="11.0.0" /> <PackageVersion Include="Avalonia.Svg" Version="11.0.0.2" />
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0" /> <PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0.2" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" /> <PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="Concentus" Version="1.1.7" /> <PackageVersion Include="Concentus" Version="1.1.7" />
<PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" /> <PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageVersion Include="DynamicData" Version="7.14.2" /> <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" Version="1.1.1" />
<PackageVersion Include="GtkSharp.Dependencies.osx" Version="0.0.5" /> <PackageVersion Include="GtkSharp.Dependencies.osx" Version="0.0.5" />
<PackageVersion Include="jp2masa.Avalonia.Flexbox" Version="0.3.0-beta.4" /> <PackageVersion Include="jp2masa.Avalonia.Flexbox" Version="0.3.0-beta.4" />
<PackageVersion Include="LibHac" Version="0.18.0" /> <PackageVersion Include="LibHac" Version="0.18.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" /> <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.6.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.3" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" /> <PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" /> <PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
<PackageVersion Include="NUnit" Version="3.13.3" /> <PackageVersion Include="NUnit" Version="3.13.3" />
@ -44,9 +44,9 @@
<PackageVersion Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta11" /> <PackageVersion Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta11" />
<PackageVersion Include="SPB" Version="0.0.4-build28" /> <PackageVersion Include="SPB" Version="0.0.4-build28" />
<PackageVersion Include="System.Drawing.Common" Version="7.0.0" /> <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.IO.Hashing" Version="7.0.0" />
<PackageVersion Include="System.Management" Version="7.0.2" /> <PackageVersion Include="System.Management" Version="7.0.2" />
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" /> <PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -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"

40
docs/README.md Normal file
View file

@ -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

View file

@ -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.

56
docs/workflow/pr-guide.md Normal file
View file

@ -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.

View file

@ -1,6 +1,7 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Text;
namespace ARMeilleure.Diagnostics namespace ARMeilleure.Diagnostics
{ {
@ -33,7 +34,6 @@ namespace ARMeilleure.Diagnostics
public static string Get(ulong address) public static string Get(ulong address)
{ {
if (_symbols.TryGetValue(address, out string result)) if (_symbols.TryGetValue(address, out string result))
{ {
return result; return result;
@ -48,13 +48,15 @@ namespace ARMeilleure.Diagnostics
ulong diff = address - symbol.Start; ulong diff = address - symbol.Start;
ulong rem = diff % symbol.ElementSize; ulong rem = diff % symbol.ElementSize;
result = symbol.Name + "_" + diff / symbol.ElementSize; StringBuilder resultBuilder = new();
resultBuilder.Append($"{symbol.Name}_{diff / symbol.ElementSize}");
if (rem != 0) if (rem != 0)
{ {
result += "+" + rem; resultBuilder.Append($"+{rem}");
} }
result = resultBuilder.ToString();
_symbols.TryAdd(address, result); _symbols.TryAdd(address, result);
return result; return result;

View file

@ -189,7 +189,7 @@ namespace ARMeilleure.Translation
{ {
if (start.CompareTo(node.End) < 0) if (start.CompareTo(node.End) < 0)
{ {
if (overlaps.Length >= overlapCount) if (overlaps.Length <= overlapCount)
{ {
Array.Resize(ref overlaps, overlapCount + ArrayGrowthSize); Array.Resize(ref overlaps, overlapCount + ArrayGrowthSize);
} }

View file

@ -7,14 +7,14 @@ namespace ARMeilleure.Translation
internal class TranslatorCache<T> internal class TranslatorCache<T>
{ {
private readonly IntervalTree<ulong, T> _tree; private readonly IntervalTree<ulong, T> _tree;
private readonly ReaderWriterLock _treeLock; private readonly ReaderWriterLockSlim _treeLock;
public int Count => _tree.Count; public int Count => _tree.Count;
public TranslatorCache() public TranslatorCache()
{ {
_tree = new IntervalTree<ulong, T>(); _tree = new IntervalTree<ulong, T>();
_treeLock = new ReaderWriterLock(); _treeLock = new ReaderWriterLockSlim();
} }
public bool TryAdd(ulong address, ulong size, T value) 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) 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); bool result = _tree.AddOrUpdate(address, address + size, value, updateFactoryCallback);
_treeLock.ReleaseWriterLock(); _treeLock.ExitWriteLock();
return result; return result;
} }
public T GetOrAdd(ulong address, ulong size, T value) public T GetOrAdd(ulong address, ulong size, T value)
{ {
_treeLock.AcquireWriterLock(Timeout.Infinite); _treeLock.EnterWriteLock();
value = _tree.GetOrAdd(address, address + size, value); value = _tree.GetOrAdd(address, address + size, value);
_treeLock.ReleaseWriterLock(); _treeLock.ExitWriteLock();
return value; return value;
} }
public bool Remove(ulong address) public bool Remove(ulong address)
{ {
_treeLock.AcquireWriterLock(Timeout.Infinite); _treeLock.EnterWriteLock();
bool removed = _tree.Remove(address) != 0; bool removed = _tree.Remove(address) != 0;
_treeLock.ReleaseWriterLock(); _treeLock.ExitWriteLock();
return removed; return removed;
} }
public void Clear() public void Clear()
{ {
_treeLock.AcquireWriterLock(Timeout.Infinite); _treeLock.EnterWriteLock();
_tree.Clear(); _tree.Clear();
_treeLock.ReleaseWriterLock(); _treeLock.ExitWriteLock();
} }
public bool ContainsKey(ulong address) public bool ContainsKey(ulong address)
{ {
_treeLock.AcquireReaderLock(Timeout.Infinite); _treeLock.EnterReadLock();
bool result = _tree.ContainsKey(address); bool result = _tree.ContainsKey(address);
_treeLock.ReleaseReaderLock(); _treeLock.ExitReadLock();
return result; return result;
} }
public bool TryGetValue(ulong address, out T value) public bool TryGetValue(ulong address, out T value)
{ {
_treeLock.AcquireReaderLock(Timeout.Infinite); _treeLock.EnterReadLock();
bool result = _tree.TryGet(address, out value); bool result = _tree.TryGet(address, out value);
_treeLock.ReleaseReaderLock(); _treeLock.ExitReadLock();
return result; return result;
} }
public int GetOverlaps(ulong address, ulong size, ref ulong[] overlaps) 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); int count = _tree.Get(address, address + size, ref overlaps);
_treeLock.ReleaseReaderLock(); _treeLock.ExitReadLock();
return count; return count;
} }
public List<T> AsList() public List<T> AsList()
{ {
_treeLock.AcquireReaderLock(Timeout.Infinite); _treeLock.EnterReadLock();
List<T> list = _tree.AsList(); List<T> list = _tree.AsList();
_treeLock.ReleaseReaderLock(); _treeLock.ExitReadLock();
return list; return list;
} }

View file

@ -31,9 +31,18 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
public bool IsEffectEnabled { get; } public bool IsEffectEnabled { get; }
public AuxiliaryBufferCommand(uint bufferOffset, byte inputBufferOffset, byte outputBufferOffset, public AuxiliaryBufferCommand(
ref AuxiliaryBufferAddresses sendBufferInfo, bool isEnabled, uint countMax, uint bufferOffset,
CpuAddress outputBuffer, CpuAddress inputBuffer, uint updateCount, uint writeOffset, int nodeId) byte inputBufferOffset,
byte outputBufferOffset,
ref AuxiliaryBufferAddresses sendBufferInfo,
bool isEnabled,
uint countMax,
CpuAddress outputBuffer,
CpuAddress inputBuffer,
uint updateCount,
uint writeOffset,
int nodeId)
{ {
Enabled = true; Enabled = true;
NodeId = nodeId; NodeId = nodeId;

View file

@ -21,7 +21,14 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
private BiquadFilterParameter _parameter; 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; _parameter = filter;
BiquadFilterState = biquadFilterStateMemory; BiquadFilterState = biquadFilterStateMemory;

View file

@ -77,7 +77,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe void ClearBuffer(int index) 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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -89,7 +89,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe void CopyBuffer(int outputBufferIndex, int inputBufferIndex) 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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]

View file

@ -94,18 +94,18 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
float newMean = inputMovingAverage.Update(FloatingPointHelper.MeanSquare(channelInput), _parameter.InputGain); float newMean = inputMovingAverage.Update(FloatingPointHelper.MeanSquare(channelInput), _parameter.InputGain);
float y = FloatingPointHelper.Log10(newMean) * 10.0f; 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) 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; float tmpGain;
@ -118,7 +118,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
tmpGain = (y - state.Unknown10) * ((y - state.Unknown10) * -state.CompressorGainReduction); tmpGain = (y - state.Unknown10) * ((y - state.Unknown10) * -state.CompressorGainReduction);
} }
z = FloatingPointHelper.DecibelToLinearExtended(tmpGain); z = FloatingPointHelper.DecibelToLinear(tmpGain);
} }
float unknown4New = z; float unknown4New = z;

View file

@ -88,7 +88,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
float outGain = FixedPointHelper.ToFloat(Parameter.OutGain, FixedPointPrecision); float outGain = FixedPointHelper.ToFloat(Parameter.OutGain, FixedPointPrecision);
Matrix2x2 delayFeedback = new(delayFeedbackBaseGain, delayFeedbackCrossGain, Matrix2x2 delayFeedback = new(delayFeedbackBaseGain, delayFeedbackCrossGain,
delayFeedbackCrossGain, delayFeedbackBaseGain); delayFeedbackCrossGain, delayFeedbackBaseGain);
for (int i = 0; i < sampleCount; i++) for (int i = 0; i < sampleCount; i++)
{ {
@ -125,9 +125,9 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
float outGain = FixedPointHelper.ToFloat(Parameter.OutGain, FixedPointPrecision); float outGain = FixedPointHelper.ToFloat(Parameter.OutGain, FixedPointPrecision);
Matrix4x4 delayFeedback = new(delayFeedbackBaseGain, delayFeedbackCrossGain, delayFeedbackCrossGain, 0.0f, Matrix4x4 delayFeedback = new(delayFeedbackBaseGain, delayFeedbackCrossGain, delayFeedbackCrossGain, 0.0f,
delayFeedbackCrossGain, delayFeedbackBaseGain, 0.0f, delayFeedbackCrossGain, delayFeedbackCrossGain, delayFeedbackBaseGain, 0.0f, delayFeedbackCrossGain,
delayFeedbackCrossGain, 0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain, delayFeedbackCrossGain, 0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain,
0.0f, delayFeedbackCrossGain, delayFeedbackCrossGain, delayFeedbackBaseGain); 0.0f, delayFeedbackCrossGain, delayFeedbackCrossGain, delayFeedbackBaseGain);
for (int i = 0; i < sampleCount; i++) for (int i = 0; i < sampleCount; i++)
@ -172,11 +172,11 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
float outGain = FixedPointHelper.ToFloat(Parameter.OutGain, FixedPointPrecision); float outGain = FixedPointHelper.ToFloat(Parameter.OutGain, FixedPointPrecision);
Matrix6x6 delayFeedback = new(delayFeedbackBaseGain, 0.0f, delayFeedbackCrossGain, 0.0f, delayFeedbackCrossGain, 0.0f, Matrix6x6 delayFeedback = new(delayFeedbackBaseGain, 0.0f, delayFeedbackCrossGain, 0.0f, delayFeedbackCrossGain, 0.0f,
0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain, 0.0f, 0.0f, delayFeedbackCrossGain, 0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain, 0.0f, 0.0f, delayFeedbackCrossGain,
delayFeedbackCrossGain, delayFeedbackCrossGain, delayFeedbackBaseGain, 0.0f, 0.0f, 0.0f, delayFeedbackCrossGain, delayFeedbackCrossGain, delayFeedbackBaseGain, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, feedbackGain, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, feedbackGain, 0.0f, 0.0f,
delayFeedbackCrossGain, 0.0f, 0.0f, 0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain, delayFeedbackCrossGain, 0.0f, 0.0f, 0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain,
0.0f, delayFeedbackCrossGain, 0.0f, 0.0f, delayFeedbackCrossGain, delayFeedbackBaseGain); 0.0f, delayFeedbackCrossGain, 0.0f, 0.0f, delayFeedbackCrossGain, delayFeedbackBaseGain);
for (int i = 0; i < sampleCount; i++) for (int i = 0; i < sampleCount; i++)
{ {

View file

@ -28,7 +28,14 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
private LimiterParameter _parameter; 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; Enabled = true;
NodeId = nodeId; NodeId = nodeId;

View file

@ -79,53 +79,57 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ProcessReverbMono(ref ReverbState state, ReadOnlySpan<IntPtr> outputBuffers, ReadOnlySpan<IntPtr> inputBuffers, uint sampleCount) private void ProcessReverbMono(ref ReverbState state, ReadOnlySpan<IntPtr> outputBuffers, ReadOnlySpan<IntPtr> inputBuffers, uint sampleCount)
{ {
ProcessReverbGeneric(ref state, ProcessReverbGeneric(
outputBuffers, ref state,
inputBuffers, outputBuffers,
sampleCount, inputBuffers,
_outputEarlyIndicesTableMono, sampleCount,
_targetEarlyDelayLineIndicesTableMono, _outputEarlyIndicesTableMono,
_targetOutputFeedbackIndicesTableMono, _targetEarlyDelayLineIndicesTableMono,
_outputIndicesTableMono); _targetOutputFeedbackIndicesTableMono,
_outputIndicesTableMono);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ProcessReverbStereo(ref ReverbState state, ReadOnlySpan<IntPtr> outputBuffers, ReadOnlySpan<IntPtr> inputBuffers, uint sampleCount) private void ProcessReverbStereo(ref ReverbState state, ReadOnlySpan<IntPtr> outputBuffers, ReadOnlySpan<IntPtr> inputBuffers, uint sampleCount)
{ {
ProcessReverbGeneric(ref state, ProcessReverbGeneric(
outputBuffers, ref state,
inputBuffers, outputBuffers,
sampleCount, inputBuffers,
_outputEarlyIndicesTableStereo, sampleCount,
_targetEarlyDelayLineIndicesTableStereo, _outputEarlyIndicesTableStereo,
_targetOutputFeedbackIndicesTableStereo, _targetEarlyDelayLineIndicesTableStereo,
_outputIndicesTableStereo); _targetOutputFeedbackIndicesTableStereo,
_outputIndicesTableStereo);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ProcessReverbQuadraphonic(ref ReverbState state, ReadOnlySpan<IntPtr> outputBuffers, ReadOnlySpan<IntPtr> inputBuffers, uint sampleCount) private void ProcessReverbQuadraphonic(ref ReverbState state, ReadOnlySpan<IntPtr> outputBuffers, ReadOnlySpan<IntPtr> inputBuffers, uint sampleCount)
{ {
ProcessReverbGeneric(ref state, ProcessReverbGeneric(
outputBuffers, ref state,
inputBuffers, outputBuffers,
sampleCount, inputBuffers,
_outputEarlyIndicesTableQuadraphonic, sampleCount,
_targetEarlyDelayLineIndicesTableQuadraphonic, _outputEarlyIndicesTableQuadraphonic,
_targetOutputFeedbackIndicesTableQuadraphonic, _targetEarlyDelayLineIndicesTableQuadraphonic,
_outputIndicesTableQuadraphonic); _targetOutputFeedbackIndicesTableQuadraphonic,
_outputIndicesTableQuadraphonic);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ProcessReverbSurround(ref ReverbState state, ReadOnlySpan<IntPtr> outputBuffers, ReadOnlySpan<IntPtr> inputBuffers, uint sampleCount) private void ProcessReverbSurround(ref ReverbState state, ReadOnlySpan<IntPtr> outputBuffers, ReadOnlySpan<IntPtr> inputBuffers, uint sampleCount)
{ {
ProcessReverbGeneric(ref state, ProcessReverbGeneric(
outputBuffers, ref state,
inputBuffers, outputBuffers,
sampleCount, inputBuffers,
_outputEarlyIndicesTableSurround, sampleCount,
_targetEarlyDelayLineIndicesTableSurround, _outputEarlyIndicesTableSurround,
_targetOutputFeedbackIndicesTableSurround, _targetEarlyDelayLineIndicesTableSurround,
_outputIndicesTableSurround); _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) 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)

View file

@ -52,7 +52,7 @@ namespace Ryujinx.Audio.Renderer.Dsp
{ {
// NOTE: Nintendo uses an approximation of log10, we don't. // NOTE: Nintendo uses an approximation of log10, we don't.
// As such, we support the same ranges as Nintendo to avoid unexpected behaviours. // 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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -62,7 +62,8 @@ namespace Ryujinx.Audio.Renderer.Dsp
foreach (float input in inputs) foreach (float input in inputs)
{ {
res += (input * input); float normInput = input * (1f / 32768f);
res += normInput * normInput;
} }
res /= inputs.Length; res /= inputs.Length;
@ -81,19 +82,6 @@ namespace Ryujinx.Audio.Renderer.Dsp
return MathF.Pow(10.0f, db / 20.0f); 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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float DegreesToRadians(float degrees) public static float DegreesToRadians(float degrees)
{ {

View file

@ -20,6 +20,11 @@ namespace Ryujinx.Audio.Renderer.Dsp
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetBufferSize<T>(int startSampleOffset, int endSampleOffset, int offset, int count) where T : unmanaged 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>(); return GetCountToDecode(startSampleOffset, endSampleOffset, offset, count) * Unsafe.SizeOf<T>();
} }

View file

@ -3,7 +3,7 @@ using Ryujinx.Audio.Renderer.Parameter.Effect;
namespace Ryujinx.Audio.Renderer.Dsp.State namespace Ryujinx.Audio.Renderer.Dsp.State
{ {
public class CompressorState public struct CompressorState
{ {
public ExponentialMovingAverage InputMovingAverage; public ExponentialMovingAverage InputMovingAverage;
public float Unknown4; public float Unknown4;
@ -45,7 +45,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.State
CompressorGainReduction = (1.0f - ratio) / Constants.ChannelCountMax; CompressorGainReduction = (1.0f - ratio) / Constants.ChannelCountMax;
Unknown10 = threshold - 1.5f; Unknown10 = threshold - 1.5f;
Unknown14 = threshold + 1.5f; Unknown14 = threshold + 1.5f;
OutputGain = FloatingPointHelper.DecibelToLinearExtended(parameter.OutputGain + makeupGain); OutputGain = FloatingPointHelper.DecibelToLinear(parameter.OutputGain + makeupGain);
} }
} }
} }

View file

@ -4,7 +4,7 @@ using System.Runtime.CompilerServices;
namespace Ryujinx.Audio.Renderer.Dsp.State namespace Ryujinx.Audio.Renderer.Dsp.State
{ {
public class DelayState public struct DelayState
{ {
public DelayLine[] DelayLines { get; } public DelayLine[] DelayLines { get; }
public float[] LowPassZ { get; set; } public float[] LowPassZ { get; set; }
@ -53,7 +53,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.State
LowPassBaseGain = 1.0f - LowPassFeedbackGain; 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++) for (int i = 0; i < channelCount; i++)
{ {

View file

@ -4,7 +4,7 @@ using System;
namespace Ryujinx.Audio.Renderer.Dsp.State namespace Ryujinx.Audio.Renderer.Dsp.State
{ {
public class LimiterState public struct LimiterState
{ {
public ExponentialMovingAverage[] DetectorAverage; public ExponentialMovingAverage[] DetectorAverage;
public ExponentialMovingAverage[] CompressionGainAverage; public ExponentialMovingAverage[] CompressionGainAverage;

View file

@ -4,7 +4,7 @@ using System;
namespace Ryujinx.Audio.Renderer.Dsp.State 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[] _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 }; private readonly float[] _fdnDelayMaxTimes = new float[4] { 45.704f, 82.782f, 149.94f, 271.58f };

View file

@ -5,7 +5,7 @@ using System;
namespace Ryujinx.Audio.Renderer.Dsp.State namespace Ryujinx.Audio.Renderer.Dsp.State
{ {
public class ReverbState public struct ReverbState
{ {
private static readonly float[] _fdnDelayTimes = new float[20] private static readonly float[] _fdnDelayTimes = new float[20]
{ {
@ -54,7 +54,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.State
// Room // Room
0.70f, 0.68f, 0.70f, 0.68f, 0.70f, 0.68f, 0.70f, 0.68f, 0.68f, 0.68f, 0.70f, 0.68f, 0.70f, 0.68f, 0.70f, 0.68f, 0.70f, 0.68f, 0.68f, 0.68f,
// Chamber // 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 // Hall
0.50f, 0.70f, 0.70f, 0.68f, 0.50f, 0.68f, 0.68f, 0.70f, 0.68f, 0.00f, 0.50f, 0.70f, 0.70f, 0.68f, 0.50f, 0.68f, 0.68f, 0.70f, 0.68f, 0.00f,
// Cathedral // Cathedral

View file

@ -264,8 +264,8 @@ namespace Ryujinx.Audio.Renderer.Parameter
{ {
uint dataTypeSize = (uint)Unsafe.SizeOf<T>(); uint dataTypeSize = (uint)Unsafe.SizeOf<T>();
return StartSampleOffset * dataTypeSize <= Size && return (ulong)StartSampleOffset * dataTypeSize <= Size &&
EndSampleOffset * dataTypeSize <= Size; (ulong)EndSampleOffset * dataTypeSize <= Size;
} }
/// <summary> /// <summary>

View file

@ -104,7 +104,7 @@ namespace Ryujinx.Ava
{ {
"Light" => ThemeVariant.Light, "Light" => ThemeVariant.Light,
"Dark" => ThemeVariant.Dark, "Dark" => ThemeVariant.Dark,
_ => ThemeVariant.Default _ => ThemeVariant.Default,
}; };
if (enableCustomTheme) if (enableCustomTheme)

View file

@ -3,7 +3,6 @@ using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Rendering;
using Avalonia.Threading; using Avalonia.Threading;
using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem;
using Ryujinx.Audio.Backends.Dummy; using Ryujinx.Audio.Backends.Dummy;
@ -21,6 +20,7 @@ using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Multiplayer;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.SystemInterop; using Ryujinx.Common.SystemInterop;
using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.GAL;
@ -191,6 +191,7 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough; ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough;
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState; ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState;
ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState;
_gpuCancellationTokenSource = new CancellationTokenSource(); _gpuCancellationTokenSource = new CancellationTokenSource();
_gpuDoneEvent = new ManualResetEvent(false); _gpuDoneEvent = new ManualResetEvent(false);
@ -412,6 +413,11 @@ namespace Ryujinx.Ava
Device.Configuration.MultiplayerLanInterfaceId = e.NewValue; Device.Configuration.MultiplayerLanInterfaceId = e.NewValue;
} }
private void UpdateMultiplayerModeState(object sender, ReactiveEventArgs<MultiplayerMode> e)
{
Device.Configuration.MultiplayerMode = e.NewValue;
}
public void Stop() public void Stop()
{ {
_isActive = false; _isActive = false;
@ -782,7 +788,8 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.Graphics.AspectRatio, ConfigurationState.Instance.Graphics.AspectRatio,
ConfigurationState.Instance.System.AudioVolume, ConfigurationState.Instance.System.AudioVolume,
ConfigurationState.Instance.System.UseHypervisor, ConfigurationState.Instance.System.UseHypervisor,
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value); ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value,
ConfigurationState.Instance.Multiplayer.Mode);
Device = new Switch(configuration); Device = new Switch(configuration);
} }

View file

@ -546,7 +546,7 @@
"SwkbdMinCharacters": "Must be at least {0} characters long", "SwkbdMinCharacters": "Must be at least {0} characters long",
"SwkbdMinRangeCharacters": "Must be {0}-{1} characters long", "SwkbdMinRangeCharacters": "Must be {0}-{1} characters long",
"SoftwareKeyboard": "Software Keyboard", "SoftwareKeyboard": "Software Keyboard",
"SoftwareKeyboardModeNumbersOnly": "Must be numbers only", "SoftwareKeyboardModeNumeric": "Must be 0-9 or '.' only",
"SoftwareKeyboardModeAlphabet": "Must be non CJK-characters only", "SoftwareKeyboardModeAlphabet": "Must be non CJK-characters only",
"SoftwareKeyboardModeASCII": "Must be ASCII text 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.", "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", "NetworkInterfaceDefault": "Default",
"PackagingShaders": "Packaging Shaders", "PackagingShaders": "Packaging Shaders",
"AboutChangelogButton": "View Changelog on GitHub", "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"
} }

View file

@ -15,7 +15,6 @@ using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls; using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS.Services.Account.Acc;
@ -36,11 +35,9 @@ namespace Ryujinx.Ava.Common
private static HorizonClient _horizonClient; private static HorizonClient _horizonClient;
private static AccountManager _accountManager; private static AccountManager _accountManager;
private static VirtualFileSystem _virtualFileSystem; 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; _virtualFileSystem = virtualFileSystem;
_horizonClient = horizonClient; _horizonClient = horizonClient;
_accountManager = accountManager; _accountManager = accountManager;
@ -148,7 +145,7 @@ namespace Ryujinx.Ava.Common
var result = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions var result = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{ {
Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle], Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle],
AllowMultiple = false AllowMultiple = false,
}); });
if (result.Count == 0) if (result.Count == 0)

View file

@ -1,6 +1,7 @@
using Avalonia.Data; using Avalonia.Data.Core;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.MarkupExtensions; using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings;
using System; using System;
namespace Ryujinx.Ava.Common.Locale namespace Ryujinx.Ava.Common.Locale
@ -18,11 +19,20 @@ namespace Ryujinx.Ava.Common.Locale
{ {
LocaleKeys keyToUse = Key; LocaleKeys keyToUse = Key;
ReflectionBindingExtension binding = new($"[{keyToUse}]") var builder = new CompiledBindingPathBuilder();
{
Mode = BindingMode.OneWay, builder.SetRawSource(LocaleManager.Instance)
Source = 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); return binding.ProvideValue(serviceProvider);
} }

View file

@ -1,6 +1,5 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Input; using Ryujinx.Input;
using System; using System;

View file

@ -82,12 +82,9 @@ namespace Ryujinx.Modules
{ {
Logger.Error?.Print(LogClass.Application, "Failed to convert the current Ryujinx version!"); Logger.Error?.Print(LogClass.Application, "Failed to convert the current Ryujinx version!");
Dispatcher.UIThread.Post(async () => await ContentDialogHelper.CreateWarningDialog(
{ LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedMessage],
await ContentDialogHelper.CreateWarningDialog( LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedMessage],
LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
});
_running = false; _running = false;
@ -114,10 +111,9 @@ namespace Ryujinx.Modules
{ {
if (showVersionUpToDate) if (showVersionUpToDate)
{ {
Dispatcher.UIThread.Post(async () => await ContentDialogHelper.CreateUpdaterInfoDialog(
{ LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
await ContentDialogHelper.CreateUpdaterInfoDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage], ""); "");
});
} }
_running = false; _running = false;
@ -134,10 +130,9 @@ namespace Ryujinx.Modules
{ {
if (showVersionUpToDate) if (showVersionUpToDate)
{ {
Dispatcher.UIThread.Post(async () => await ContentDialogHelper.CreateUpdaterInfoDialog(
{ LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
await ContentDialogHelper.CreateUpdaterInfoDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage], ""); "");
});
} }
_running = false; _running = false;
@ -149,10 +144,8 @@ namespace Ryujinx.Modules
{ {
Logger.Error?.Print(LogClass.Application, exception.Message); 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; _running = false;
@ -167,12 +160,9 @@ namespace Ryujinx.Modules
{ {
Logger.Error?.Print(LogClass.Application, "Failed to convert the received Ryujinx version from Github!"); 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],
await ContentDialogHelper.CreateWarningDialog( LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedGithubMessage],
LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
});
_running = false; _running = false;
@ -183,10 +173,9 @@ namespace Ryujinx.Modules
{ {
if (showVersionUpToDate) if (showVersionUpToDate)
{ {
Dispatcher.UIThread.Post(async () => await ContentDialogHelper.CreateUpdaterInfoDialog(
{ LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
await ContentDialogHelper.CreateUpdaterInfoDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage], ""); "");
});
} }
_running = false; _running = false;
@ -212,7 +201,7 @@ namespace Ryujinx.Modules
_buildSize = -1; _buildSize = -1;
} }
Dispatcher.UIThread.Post(async () => await Dispatcher.UIThread.InvokeAsync(async () =>
{ {
// Show a message asking the user if they want to update // Show a message asking the user if they want to update
var shouldUpdate = await ContentDialogHelper.CreateChoiceDialog( var shouldUpdate = await ContentDialogHelper.CreateChoiceDialog(
@ -222,7 +211,7 @@ namespace Ryujinx.Modules
if (shouldUpdate) if (shouldUpdate)
{ {
UpdateRyujinx(mainWindow, _buildUrl); await UpdateRyujinx(mainWindow, _buildUrl);
} }
else else
{ {
@ -241,7 +230,7 @@ namespace Ryujinx.Modules
return result; return result;
} }
private static async void UpdateRyujinx(Window parent, string downloadUrl) private static async Task UpdateRyujinx(Window parent, string downloadUrl)
{ {
_updateSuccessful = false; _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 // Extract Update
taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterExtracting]; taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterExtracting];
taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal); taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal);
await Task.Run(() => if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
{ {
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) ExtractTarGzipFile(taskDialog, updateFile, _updateDir);
{ }
ExtractTarGzipFile(taskDialog, updateFile, _updateDir); else if (OperatingSystem.IsWindows())
} {
else if (OperatingSystem.IsWindows()) ExtractZipFile(taskDialog, updateFile, _updateDir);
{ }
ExtractZipFile(taskDialog, updateFile, _updateDir); else
} {
else throw new NotSupportedException();
{ }
throw new NotSupportedException();
}
});
// Delete downloaded zip // Delete downloaded zip
File.Delete(updateFile); File.Delete(updateFile);
@ -613,36 +599,33 @@ namespace Ryujinx.Modules
if (!OperatingSystem.IsMacOS()) if (!OperatingSystem.IsMacOS())
{ {
// Replace old files // Replace old files
await Task.Run(() => double count = 0;
foreach (string file in allFiles)
{ {
double count = 0; count++;
foreach (string file in allFiles) try
{ {
count++; File.Move(file, file + ".ryuold");
try
{
File.Move(file, file + ".ryuold");
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.InvokeAsync(() =>
{
taskDialog.SetProgressBarState(GetPercentage(count, allFiles.Count), TaskDialogProgressState.Normal);
});
}
catch
{ {
Logger.Warning?.Print(LogClass.Application, LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.UpdaterRenameFailed, file)); taskDialog.SetProgressBarState(GetPercentage(count, allFiles.Count), TaskDialogProgressState.Normal);
} });
} }
catch
Dispatcher.UIThread.Post(() =>
{ {
taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterAddingFiles]; Logger.Warning?.Print(LogClass.Application, LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.UpdaterRenameFailed, file));
taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal); }
}); }
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); Directory.Delete(_updateDir, true);
} }
@ -658,12 +641,11 @@ namespace Ryujinx.Modules
{ {
if (showWarnings) if (showWarnings)
{ {
Dispatcher.UIThread.Post(async () => Dispatcher.UIThread.InvokeAsync(() =>
{ ContentDialogHelper.CreateWarningDialog(
await ContentDialogHelper.CreateWarningDialog(
LocaleManager.Instance[LocaleKeys.DialogUpdaterArchNotSupportedMessage], LocaleManager.Instance[LocaleKeys.DialogUpdaterArchNotSupportedMessage],
LocaleManager.Instance[LocaleKeys.DialogUpdaterArchNotSupportedSubMessage]); LocaleManager.Instance[LocaleKeys.DialogUpdaterArchNotSupportedSubMessage])
}); );
} }
return false; return false;
@ -673,12 +655,11 @@ namespace Ryujinx.Modules
{ {
if (showWarnings) if (showWarnings)
{ {
Dispatcher.UIThread.Post(async () => Dispatcher.UIThread.InvokeAsync(() =>
{ ContentDialogHelper.CreateWarningDialog(
await ContentDialogHelper.CreateWarningDialog(
LocaleManager.Instance[LocaleKeys.DialogUpdaterNoInternetMessage], LocaleManager.Instance[LocaleKeys.DialogUpdaterNoInternetMessage],
LocaleManager.Instance[LocaleKeys.DialogUpdaterNoInternetSubMessage]); LocaleManager.Instance[LocaleKeys.DialogUpdaterNoInternetSubMessage])
}); );
} }
return false; return false;
@ -688,12 +669,11 @@ namespace Ryujinx.Modules
{ {
if (showWarnings) if (showWarnings)
{ {
Dispatcher.UIThread.Post(async () => Dispatcher.UIThread.InvokeAsync(() =>
{ ContentDialogHelper.CreateWarningDialog(
await ContentDialogHelper.CreateWarningDialog(
LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildMessage], LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildMessage],
LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage]); LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage])
}); );
} }
return false; return false;
@ -705,21 +685,19 @@ namespace Ryujinx.Modules
{ {
if (ReleaseInformation.IsFlatHubBuild()) if (ReleaseInformation.IsFlatHubBuild())
{ {
Dispatcher.UIThread.Post(async () => Dispatcher.UIThread.InvokeAsync(() =>
{ ContentDialogHelper.CreateWarningDialog(
await ContentDialogHelper.CreateWarningDialog(
LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle], LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle],
LocaleManager.Instance[LocaleKeys.DialogUpdaterFlatpakNotSupportedMessage]); LocaleManager.Instance[LocaleKeys.DialogUpdaterFlatpakNotSupportedMessage])
}); );
} }
else else
{ {
Dispatcher.UIThread.Post(async () => Dispatcher.UIThread.InvokeAsync(() =>
{ ContentDialogHelper.CreateWarningDialog(
await ContentDialogHelper.CreateWarningDialog(
LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle], LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle],
LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage]); LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage])
}); );
} }
} }

View file

@ -106,7 +106,7 @@ namespace Ryujinx.Ava.UI.Applet
bool error = false; bool error = false;
string inputText = args.InitialText ?? ""; string inputText = args.InitialText ?? "";
Dispatcher.UIThread.Post(async () => Dispatcher.UIThread.InvokeAsync(async () =>
{ {
try try
{ {
@ -149,7 +149,7 @@ namespace Ryujinx.Ava.UI.Applet
bool showDetails = false; bool showDetails = false;
Dispatcher.UIThread.Post(async () => Dispatcher.UIThread.InvokeAsync(async () =>
{ {
try try
{ {

View file

@ -6,9 +6,11 @@
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="{locale:Locale ErrorWindowTitle}" Title="{locale:Locale ErrorWindowTitle}"
xmlns:views="using:Ryujinx.Ava.UI.Applet"
Width="450" Width="450"
Height="340" Height="340"
CanResize="False" CanResize="False"
x:DataType="views:ErrorAppletWindow"
SizeToContent="Height" SizeToContent="Height"
mc:Ignorable="d" mc:Ignorable="d"
Focusable="True"> Focusable="True">
@ -38,7 +40,7 @@
Grid.Column="1" Grid.Column="1"
Margin="10" Margin="10"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Text="{ReflectionBinding Message}" Text="{Binding Message}"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<StackPanel <StackPanel
Name="ButtonStack" Name="ButtonStack"
@ -49,4 +51,4 @@
Orientation="Horizontal" Orientation="Horizontal"
Spacing="10" /> Spacing="10" />
</Grid> </Grid>
</Window> </Window>

View file

@ -4,7 +4,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="using:Ryujinx.Ava.UI.Controls"
Width="400" Width="400"
x:DataType="views:SwkbdAppletDialog"
mc:Ignorable="d" mc:Ignorable="d"
Focusable="True"> Focusable="True">
<Grid <Grid
@ -34,13 +36,13 @@
Grid.Row="1" Grid.Row="1"
Grid.Column="1" Grid.Column="1"
Margin="5" Margin="5"
Text="{ReflectionBinding MainText}" Text="{Binding MainText}"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock <TextBlock
Grid.Row="2" Grid.Row="2"
Grid.Column="1" Grid.Column="1"
Margin="5" Margin="5"
Text="{ReflectionBinding SecondaryText}" Text="{Binding SecondaryText}"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBox <TextBox
Name="Input" Name="Input"
@ -50,7 +52,7 @@
VerticalAlignment="Center" VerticalAlignment="Center"
Focusable="True" Focusable="True"
KeyUp="Message_KeyUp" KeyUp="Message_KeyUp"
Text="{ReflectionBinding Message}" Text="{Binding Message}"
TextInput="Message_TextInput" TextInput="Message_TextInput"
TextWrapping="Wrap" TextWrapping="Wrap"
UseFloatingWatermark="True" /> UseFloatingWatermark="True" />

View file

@ -136,10 +136,10 @@ namespace Ryujinx.Ava.UI.Controls
string localeText; string localeText;
switch (mode) switch (mode)
{ {
case KeyboardMode.NumbersOnly: case KeyboardMode.Numeric:
localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeNumbersOnly); localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeNumeric);
validationInfoText = string.IsNullOrEmpty(validationInfoText) ? localeText : string.Join("\n", validationInfoText, localeText); validationInfoText = string.IsNullOrEmpty(validationInfoText) ? localeText : string.Join("\n", validationInfoText, localeText);
_checkInput = text => text.All(char.IsDigit); _checkInput = text => text.All(NumericCharacterValidation.IsNumeric);
break; break;
case KeyboardMode.Alphabet: case KeyboardMode.Alphabet:
localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeAlphabet); localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeAlphabet);

View file

@ -54,7 +54,7 @@ namespace Ryujinx.Ava.UI.Controls
{ {
if (sender is MenuItem { DataContext: MainWindowViewModel viewModel }) 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; 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) public void OpenBcatSaveDirectory_Click(object sender, RoutedEventArgs args)
{ {
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; 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) private static void OpenSaveDirectory(MainWindowViewModel viewModel, SaveDataType saveDataType, UserId userId)
@ -158,11 +158,12 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null) if (viewModel?.SelectedApplication != null)
{ {
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DialogWarning], UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.TitleName), LocaleManager.Instance[LocaleKeys.DialogWarning],
LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.TitleName),
LocaleManager.Instance[LocaleKeys.InputDialogNo], LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); LocaleManager.Instance[LocaleKeys.InputDialogNo],
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
if (result == UserResult.Yes) if (result == UserResult.Yes)
{ {
@ -205,11 +206,12 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null) if (viewModel?.SelectedApplication != null)
{ {
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DialogWarning], UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.TitleName), LocaleManager.Instance[LocaleKeys.DialogWarning],
LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.TitleName),
LocaleManager.Instance[LocaleKeys.InputDialogNo], LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); LocaleManager.Instance[LocaleKeys.InputDialogNo],
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
if (result == UserResult.Yes) 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; var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (viewModel?.SelectedApplication != null) if (viewModel?.SelectedApplication != null)
{ {
viewModel.LoadApplication(viewModel.SelectedApplication.Path); await viewModel.LoadApplication(viewModel.SelectedApplication.Path);
} }
} }
} }

View file

@ -46,7 +46,7 @@
<Setter Property="CornerRadius" Value="4" /> <Setter Property="CornerRadius" Value="4" />
</Style> </Style>
<Style Selector="ListBoxItem:selected /template/ Rectangle#SelectionIndicator"> <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> </Style>
</ListBox.Styles> </ListBox.Styles>
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
@ -56,10 +56,10 @@
Margin="10" Margin="10"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Classes.huge="{ReflectionBinding $parent[UserControl].DataContext.IsGridHuge}" Classes.huge="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridHuge}"
Classes.large="{ReflectionBinding $parent[UserControl].DataContext.IsGridLarge}" Classes.large="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridLarge}"
Classes.normal="{ReflectionBinding $parent[UserControl].DataContext.IsGridMedium}" Classes.normal="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridMedium}"
Classes.small="{ReflectionBinding $parent[UserControl].DataContext.IsGridSmall}" Classes.small="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridSmall}"
ClipToBounds="True" ClipToBounds="True"
CornerRadius="4"> CornerRadius="4">
<Grid> <Grid>
@ -78,7 +78,7 @@
Margin="0,10,0,0" Margin="0,10,0,0"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
IsVisible="{ReflectionBinding $parent[UserControl].DataContext.ShowNames}"> IsVisible="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).ShowNames}">
<TextBlock <TextBlock
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
@ -101,4 +101,4 @@
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
</Grid> </Grid>
</UserControl> </UserControl>

View file

@ -42,7 +42,7 @@
</ListBox.ItemsPanel> </ListBox.ItemsPanel>
<ListBox.Styles> <ListBox.Styles>
<Style Selector="ListBoxItem:selected /template/ Rectangle#SelectionIndicator"> <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> </Style>
</ListBox.Styles> </ListBox.Styles>
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
@ -67,10 +67,10 @@
Grid.RowSpan="3" Grid.RowSpan="3"
Grid.Column="0" Grid.Column="0"
Margin="0" Margin="0"
Classes.huge="{ReflectionBinding $parent[UserControl].DataContext.IsGridHuge}" Classes.huge="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridHuge}"
Classes.large="{ReflectionBinding $parent[UserControl].DataContext.IsGridLarge}" Classes.large="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridLarge}"
Classes.normal="{ReflectionBinding $parent[UserControl].DataContext.IsGridMedium}" Classes.normal="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridMedium}"
Classes.small="{ReflectionBinding $parent[UserControl].DataContext.IsGridSmall}" Classes.small="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridSmall}"
Source="{Binding Icon, Converter={StaticResource ByteImage}}" /> Source="{Binding Icon, Converter={StaticResource ByteImage}}" />
<Border <Border
Grid.Column="2" Grid.Column="2"
@ -157,4 +157,4 @@
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
</Grid> </Grid>
</UserControl> </UserControl>

View file

@ -327,7 +327,7 @@ namespace Ryujinx.Ava.UI.ViewModels
string imageUrl = _amiiboList.Find(amiibo => amiibo.Equals(selected)).Image; string imageUrl = _amiiboList.Find(amiibo => amiibo.Equals(selected)).Image;
string usageString = ""; StringBuilder usageStringBuilder = new();
for (int i = 0; i < _amiiboList.Count; i++) for (int i = 0; i < _amiiboList.Count; i++)
{ {
@ -341,20 +341,19 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
foreach (AmiiboApiUsage usageItem in item.AmiiboUsage) foreach (AmiiboApiUsage usageItem in item.AmiiboUsage)
{ {
usageString += Environment.NewLine + usageStringBuilder.Append($"{Environment.NewLine}- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}");
$"- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}";
writable = usageItem.Write; 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}";
} }
} }

View file

@ -212,9 +212,9 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
Patterns = new[] { "*.nsp" }, Patterns = new[] { "*.nsp" },
AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nsp" }, AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nsp" },
MimeTypes = new[] { "application/x-nx-nsp" } MimeTypes = new[] { "application/x-nx-nsp" },
} },
} },
}); });
foreach (var file in result) foreach (var file in result)

View file

@ -26,6 +26,7 @@ using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.Ui; using Ryujinx.HLE.Ui;
using Ryujinx.Input.HLE;
using Ryujinx.Modules; using Ryujinx.Modules;
using Ryujinx.Ui.App.Common; using Ryujinx.Ui.App.Common;
using Ryujinx.Ui.Common; using Ryujinx.Ui.Common;
@ -39,7 +40,6 @@ using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Image = SixLabors.ImageSharp.Image; using Image = SixLabors.ImageSharp.Image;
using InputManager = Ryujinx.Input.HLE.InputManager;
using Key = Ryujinx.Input.Key; using Key = Ryujinx.Input.Key;
using MissingKeyException = LibHac.Common.Keys.MissingKeyException; using MissingKeyException = LibHac.Common.Keys.MissingKeyException;
using ShaderCacheLoadingState = Ryujinx.Graphics.Gpu.Shader.ShaderCacheState; using ShaderCacheLoadingState = Ryujinx.Graphics.Gpu.Shader.ShaderCacheState;
@ -1068,9 +1068,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
Logger.Error?.Print(LogClass.Application, ex.ToString()); Logger.Error?.Print(LogClass.Application, ex.ToString());
static async void Action() => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys); await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys);
Dispatcher.UIThread.Post(Action);
} }
} }
catch (Exception ex) catch (Exception ex)
@ -1163,16 +1161,13 @@ namespace Ryujinx.Ava.UI.ViewModels
AppHost?.DisposeContext(); AppHost?.DisposeContext();
} }
private void HandleRelaunch() private async Task HandleRelaunch()
{ {
if (UserChannelPersistence.PreviousIndex != -1 && UserChannelPersistence.ShouldRestart) if (UserChannelPersistence.PreviousIndex != -1 && UserChannelPersistence.ShouldRestart)
{ {
UserChannelPersistence.ShouldRestart = false; UserChannelPersistence.ShouldRestart = false;
Dispatcher.UIThread.Post(() => await LoadApplication(_currentEmulatedGamePath);
{
LoadApplication(_currentEmulatedGamePath);
});
} }
else else
{ {
@ -1191,7 +1186,7 @@ namespace Ryujinx.Ava.UI.ViewModels
Application.Current.Styles.TryGetResource(args.VSyncEnabled Application.Current.Styles.TryGetResource(args.VSyncEnabled
? "VsyncEnabled" ? "VsyncEnabled"
: "VsyncDisabled", : "VsyncDisabled",
Avalonia.Application.Current.ActualThemeVariant, Application.Current.ActualThemeVariant,
out object color); out object color);
if (color is not null) if (color is not null)
@ -1283,7 +1278,12 @@ namespace Ryujinx.Ava.UI.ViewModels
Glyph = Glyph.Grid; 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 var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{ {
@ -1294,21 +1294,21 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
Patterns = new[] { "*.xci", "*.zip" }, Patterns = new[] { "*.xci", "*.zip" },
AppleUniformTypeIdentifiers = new[] { "com.ryujinx.xci", "public.zip-archive" }, 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") new("XCI")
{ {
Patterns = new[] { "*.xci" }, Patterns = new[] { "*.xci" },
AppleUniformTypeIdentifiers = new[] { "com.ryujinx.xci" }, AppleUniformTypeIdentifiers = new[] { "com.ryujinx.xci" },
MimeTypes = new[] { "application/x-nx-xci" } MimeTypes = new[] { "application/x-nx-xci" },
}, },
new("ZIP") new("ZIP")
{ {
Patterns = new[] { "*.zip" }, Patterns = new[] { "*.zip" },
AppleUniformTypeIdentifiers = new[] { "public.zip-archive" }, AppleUniformTypeIdentifiers = new[] { "public.zip-archive" },
MimeTypes = new[] { "application/zip" } MimeTypes = new[] { "application/zip" },
}, },
} },
}); });
if (result.Count > 0) 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 var result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{ {
AllowMultiple = false AllowMultiple = false,
}); });
if (result.Count > 0) 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) 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); await NavigationDialogHost.Show(AccountManager, ContentManager, VirtualFileSystem, LibHacHorizonManager.RyujinxClient);
} }
@ -1387,7 +1387,7 @@ namespace Ryujinx.Ava.UI.ViewModels
AppHost.Device.System.SimulateWakeUpMessage(); AppHost.Device.System.SimulateWakeUpMessage();
} }
public async void OpenFile() public async Task OpenFile()
{ {
var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{ {
@ -1404,7 +1404,7 @@ namespace Ryujinx.Ava.UI.ViewModels
"com.ryujinx.xci", "com.ryujinx.xci",
"com.ryujinx.nca", "com.ryujinx.nca",
"com.ryujinx.nro", "com.ryujinx.nro",
"com.ryujinx.nso" "com.ryujinx.nso",
}, },
MimeTypes = new[] MimeTypes = new[]
{ {
@ -1412,63 +1412,63 @@ namespace Ryujinx.Ava.UI.ViewModels
"application/x-nx-xci", "application/x-nx-xci",
"application/x-nx-nca", "application/x-nx-nca",
"application/x-nx-nro", "application/x-nx-nro",
"application/x-nx-nso" "application/x-nx-nso",
} },
}, },
new("NSP") new("NSP")
{ {
Patterns = new[] { "*.nsp" }, Patterns = new[] { "*.nsp" },
AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nsp" }, AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nsp" },
MimeTypes = new[] { "application/x-nx-nsp" } MimeTypes = new[] { "application/x-nx-nsp" },
}, },
new("XCI") new("XCI")
{ {
Patterns = new[] { "*.xci" }, Patterns = new[] { "*.xci" },
AppleUniformTypeIdentifiers = new[] { "com.ryujinx.xci" }, AppleUniformTypeIdentifiers = new[] { "com.ryujinx.xci" },
MimeTypes = new[] { "application/x-nx-xci" } MimeTypes = new[] { "application/x-nx-xci" },
}, },
new("NCA") new("NCA")
{ {
Patterns = new[] { "*.nca" }, Patterns = new[] { "*.nca" },
AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nca" }, AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nca" },
MimeTypes = new[] { "application/x-nx-nca" } MimeTypes = new[] { "application/x-nx-nca" },
}, },
new("NRO") new("NRO")
{ {
Patterns = new[] { "*.nro" }, Patterns = new[] { "*.nro" },
AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nro" }, AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nro" },
MimeTypes = new[] { "application/x-nx-nro" } MimeTypes = new[] { "application/x-nx-nro" },
}, },
new("NSO") new("NSO")
{ {
Patterns = new[] { "*.nso" }, Patterns = new[] { "*.nso" },
AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nso" }, AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nso" },
MimeTypes = new[] { "application/x-nx-nso" } MimeTypes = new[] { "application/x-nx-nso" },
}, },
} },
}); });
if (result.Count > 0) 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 var result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{ {
Title = LocaleManager.Instance[LocaleKeys.OpenFolderDialogTitle], Title = LocaleManager.Instance[LocaleKeys.OpenFolderDialogTitle],
AllowMultiple = false AllowMultiple = false,
}); });
if (result.Count > 0) 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) if (AppHost != null)
{ {
@ -1505,35 +1505,30 @@ namespace Ryujinx.Ava.UI.ViewModels
this, this,
TopLevel); TopLevel);
async void Action() if (!await AppHost.LoadGuestApplication())
{ {
if (!await AppHost.LoadGuestApplication()) AppHost.DisposeContext();
{ AppHost = null;
AppHost.DisposeContext();
AppHost = null;
return; 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();
} }
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) public void SwitchToRenderer(bool startFullscreen)
@ -1596,7 +1591,7 @@ namespace Ryujinx.Ava.UI.ViewModels
IsGameRunning = false; IsGameRunning = false;
Dispatcher.UIThread.InvokeAsync(() => Dispatcher.UIThread.InvokeAsync(async () =>
{ {
ShowMenuAndStatusBar = true; ShowMenuAndStatusBar = true;
ShowContent = true; ShowContent = true;
@ -1609,7 +1604,7 @@ namespace Ryujinx.Ava.UI.ViewModels
AppHost = null; AppHost = null;
HandleRelaunch(); await HandleRelaunch();
}); });
RendererHostControl.WindowCreated -= RendererHost_Created; RendererHostControl.WindowCreated -= RendererHost_Created;

View file

@ -10,6 +10,7 @@ using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Multiplayer;
using Ryujinx.Common.GraphicsDriver; using Ryujinx.Common.GraphicsDriver;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Vulkan; using Ryujinx.Graphics.Vulkan;
@ -54,6 +55,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public event Action CloseWindow; public event Action CloseWindow;
public event Action SaveSettingsEvent; public event Action SaveSettingsEvent;
private int _networkInterfaceIndex; private int _networkInterfaceIndex;
private int _multiplayerModeIndex;
public int ResolutionScale public int ResolutionScale
{ {
@ -76,14 +78,13 @@ namespace Ryujinx.Ava.UI.ViewModels
if (_graphicsBackendMultithreadingIndex != (int)ConfigurationState.Instance.Graphics.BackendThreading.Value) if (_graphicsBackendMultithreadingIndex != (int)ConfigurationState.Instance.Graphics.BackendThreading.Value)
{ {
Dispatcher.UIThread.Post(async () => Dispatcher.UIThread.InvokeAsync(() =>
{ ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningMessage],
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningMessage],
"", "",
"", "",
LocaleManager.Instance[LocaleKeys.InputDialogOk], LocaleManager.Instance[LocaleKeys.InputDialogOk],
LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningTitle]); LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningTitle])
}); );
} }
OnPropertyChanged(); OnPropertyChanged();
@ -147,6 +148,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool EnableTextureRecompression { get; set; } public bool EnableTextureRecompression { get; set; }
public bool EnableMacroHLE { get; set; } public bool EnableMacroHLE { get; set; }
public bool EnableColorSpacePassthrough { get; set; } public bool EnableColorSpacePassthrough { get; set; }
public bool ColorSpacePassthroughAvailable => IsMacOS;
public bool EnableFileLog { get; set; } public bool EnableFileLog { get; set; }
public bool EnableStub { get; set; } public bool EnableStub { get; set; }
public bool EnableInfo { get; set; } public bool EnableInfo { get; set; }
@ -256,6 +258,11 @@ namespace Ryujinx.Ava.UI.ViewModels
get => new(_networkInterfaces.Keys); get => new(_networkInterfaces.Keys);
} }
public AvaloniaList<string> MultiplayerModes
{
get => new(Enum.GetNames<MultiplayerMode>());
}
public KeyboardHotkeys KeyboardHotkeys public KeyboardHotkeys KeyboardHotkeys
{ {
get => _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() public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this()
{ {
_virtualFileSystem = virtualFileSystem; _virtualFileSystem = virtualFileSystem;
@ -484,6 +501,8 @@ namespace Ryujinx.Ava.UI.ViewModels
EnableFsAccessLog = config.Logger.EnableFsAccessLog; EnableFsAccessLog = config.Logger.EnableFsAccessLog;
FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode; FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode;
OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value; OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value;
MultiplayerModeIndex = (int)config.Multiplayer.Mode.Value;
} }
public void SaveSettings() public void SaveSettings()
@ -586,6 +605,7 @@ namespace Ryujinx.Ava.UI.ViewModels
config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel; config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel;
config.Multiplayer.LanInterfaceId.Value = _networkInterfaces[NetworkInterfaceList[NetworkInterfaceIndex]]; config.Multiplayer.LanInterfaceId.Value = _networkInterfaces[NetworkInterfaceList[NetworkInterfaceIndex]];
config.Multiplayer.Mode.Value = (MultiplayerMode)MultiplayerModeIndex;
config.ToFileFormat().SaveConfig(Program.ConfigurationPath); config.ToFileFormat().SaveConfig(Program.ConfigurationPath);

View file

@ -22,6 +22,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Path = System.IO.Path; using Path = System.IO.Path;
using SpanHelpers = LibHac.Common.SpanHelpers; using SpanHelpers = LibHac.Common.SpanHelpers;
@ -184,18 +185,12 @@ namespace Ryujinx.Ava.UI.ViewModels
} }
else else
{ {
Dispatcher.UIThread.Post(async () => Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]));
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]);
});
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Dispatcher.UIThread.Post(async () => Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadNcaErrorMessage, ex.Message, path)));
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadNcaErrorMessage, ex.Message, path));
});
} }
} }
} }
@ -207,7 +202,7 @@ namespace Ryujinx.Ava.UI.ViewModels
SortUpdates(); SortUpdates();
} }
public async void Add() public async Task Add()
{ {
var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{ {
@ -218,9 +213,9 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
Patterns = new[] { "*.nsp" }, Patterns = new[] { "*.nsp" },
AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nsp" }, AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nsp" },
MimeTypes = new[] { "application/x-nx-nsp" } MimeTypes = new[] { "application/x-nx-nsp" },
} },
} },
}); });
foreach (var file in result) foreach (var file in result)

View file

@ -465,6 +465,7 @@
Maximum="1" Maximum="1"
TickFrequency="0.01" TickFrequency="0.01"
IsSnapToTickEnabled="True" IsSnapToTickEnabled="True"
SmallChange="0.01"
Minimum="0" Minimum="0"
Value="{ReflectionBinding Configuration.DeadzoneLeft, Mode=TwoWay}" /> Value="{ReflectionBinding Configuration.DeadzoneLeft, Mode=TwoWay}" />
<TextBlock <TextBlock
@ -484,6 +485,7 @@
Maximum="2" Maximum="2"
TickFrequency="0.01" TickFrequency="0.01"
IsSnapToTickEnabled="True" IsSnapToTickEnabled="True"
SmallChange="0.01"
Minimum="0" Minimum="0"
Value="{ReflectionBinding Configuration.RangeLeft, Mode=TwoWay}" /> Value="{ReflectionBinding Configuration.RangeLeft, Mode=TwoWay}" />
<TextBlock <TextBlock
@ -607,6 +609,7 @@
Maximum="1" Maximum="1"
TickFrequency="0.01" TickFrequency="0.01"
IsSnapToTickEnabled="True" IsSnapToTickEnabled="True"
SmallChange="0.01"
Minimum="0" Minimum="0"
Value="{ReflectionBinding Configuration.TriggerThreshold, Mode=TwoWay}" /> Value="{ReflectionBinding Configuration.TriggerThreshold, Mode=TwoWay}" />
<TextBlock <TextBlock
@ -1085,6 +1088,7 @@
Maximum="1" Maximum="1"
TickFrequency="0.01" TickFrequency="0.01"
IsSnapToTickEnabled="True" IsSnapToTickEnabled="True"
SmallChange="0.01"
Padding="0" Padding="0"
VerticalAlignment="Center" VerticalAlignment="Center"
Minimum="0" Minimum="0"
@ -1106,6 +1110,7 @@
Maximum="2" Maximum="2"
TickFrequency="0.01" TickFrequency="0.01"
IsSnapToTickEnabled="True" IsSnapToTickEnabled="True"
SmallChange="0.01"
Minimum="0" Minimum="0"
Value="{ReflectionBinding Configuration.RangeRight, Mode=TwoWay}" /> Value="{ReflectionBinding Configuration.RangeRight, Mode=TwoWay}" />
<TextBlock <TextBlock

View file

@ -29,6 +29,7 @@
MaxWidth="150" MaxWidth="150"
TickFrequency="0.01" TickFrequency="0.01"
IsSnapToTickEnabled="True" IsSnapToTickEnabled="True"
SmallChange="0.01"
Maximum="100" Maximum="100"
Minimum="0" Minimum="0"
Value="{Binding Sensitivity, Mode=TwoWay}" /> Value="{Binding Sensitivity, Mode=TwoWay}" />
@ -50,6 +51,7 @@
MaxWidth="150" MaxWidth="150"
TickFrequency="0.01" TickFrequency="0.01"
IsSnapToTickEnabled="True" IsSnapToTickEnabled="True"
SmallChange="0.01"
Maximum="100" Maximum="100"
Minimum="0" Minimum="0"
Value="{Binding GyroDeadzone, Mode=TwoWay}" /> Value="{Binding GyroDeadzone, Mode=TwoWay}" />

View file

@ -26,6 +26,7 @@
Width="200" Width="200"
TickFrequency="0.01" TickFrequency="0.01"
IsSnapToTickEnabled="True" IsSnapToTickEnabled="True"
SmallChange="0.01"
Maximum="10" Maximum="10"
Minimum="0" Minimum="0"
Value="{Binding StrongRumble, Mode=TwoWay}" /> Value="{Binding StrongRumble, Mode=TwoWay}" />
@ -47,6 +48,7 @@
Maximum="10" Maximum="10"
TickFrequency="0.01" TickFrequency="0.01"
IsSnapToTickEnabled="True" IsSnapToTickEnabled="True"
SmallChange="0.01"
Minimum="0" Minimum="0"
Value="{Binding WeakRumble, Mode=TwoWay}" /> Value="{Binding WeakRumble, Mode=TwoWay}" />
<TextBlock <TextBlock

View file

@ -17,7 +17,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Views.Main namespace Ryujinx.Ava.UI.Views.Main
{ {
@ -107,20 +106,14 @@ namespace Ryujinx.Ava.UI.Views.Main
await Window.ViewModel.AppHost?.ShowExitPrompt(); 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) public async void OpenSettings(object sender, RoutedEventArgs e)
@ -132,13 +125,13 @@ namespace Ryujinx.Ava.UI.Views.Main
ViewModel.LoadConfigurableHotKeys(); 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); string contentPath = ViewModel.ContentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program);
if (!string.IsNullOrEmpty(contentPath)) 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()) if (FileAssociationHelper.Install())
{ {
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogInstallFileTypesSuccessMessage], await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogInstallFileTypesSuccessMessage], string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
} }
else else
{ {
@ -209,8 +201,7 @@ namespace Ryujinx.Ava.UI.Views.Main
{ {
if (FileAssociationHelper.Uninstall()) if (FileAssociationHelper.Uninstall())
{ {
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogUninstallFileTypesSuccessMessage], await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogUninstallFileTypesSuccessMessage], string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
} }
else else
{ {

View file

@ -6,6 +6,7 @@
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" 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" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.UI.Views.Main.MainStatusBarView" x:Class="Ryujinx.Ava.UI.Views.Main.MainStatusBarView"
x:DataType="viewModels:MainWindowViewModel"> x:DataType="viewModels:MainWindowViewModel">
@ -112,15 +113,52 @@
Background="Gray" Background="Gray"
BorderThickness="1" BorderThickness="1"
IsVisible="{Binding !ShowLoadProgress}" /> IsVisible="{Binding !ShowLoadProgress}" />
<TextBlock <SplitButton
Name="AspectRatioStatus" Name="AspectRatioStatus"
Margin="5,0,5,0" Padding="5,0,5,0"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Center" VerticalAlignment="Center"
Background="Transparent"
BorderThickness="0"
CornerRadius="0"
IsVisible="{Binding !ShowLoadProgress}" IsVisible="{Binding !ShowLoadProgress}"
PointerReleased="AspectRatioStatus_PointerReleased" Content="{Binding AspectRatioStatusText}"
Text="{Binding AspectRatioStatusText}" Click="AspectRatioStatus_OnClick"
TextAlignment="Left" /> 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 <Border
Width="2" Width="2"
Height="12" Height="12"

View file

@ -43,10 +43,9 @@ namespace Ryujinx.Ava.UI.Views.Main
ConfigurationState.Instance.System.EnableDockedMode.Value = !ConfigurationState.Instance.System.EnableDockedMode.Value; 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; 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; ConfigurationState.Instance.Graphics.AspectRatio.Value = (int)aspectRatio + 1 > Enum.GetNames(typeof(AspectRatio)).Length - 1 ? AspectRatio.Fixed4x3 : aspectRatio + 1;
} }

View file

@ -56,6 +56,7 @@
Margin="5,-10,5,0" Margin="5,-10,5,0"
VerticalAlignment="Center" VerticalAlignment="Center"
IsSnapToTickEnabled="True" IsSnapToTickEnabled="True"
SmallChange="1"
Maximum="4" Maximum="4"
Minimum="1" Minimum="1"
TickFrequency="1" TickFrequency="1"

View file

@ -77,6 +77,7 @@
<TextBlock Text="{locale:Locale SettingsEnableMacroHLE}" /> <TextBlock Text="{locale:Locale SettingsEnableMacroHLE}" />
</CheckBox> </CheckBox>
<CheckBox IsChecked="{Binding EnableColorSpacePassthrough}" <CheckBox IsChecked="{Binding EnableColorSpacePassthrough}"
IsVisible="{Binding ColorSpacePassthroughAvailable}"
ToolTip.Tip="{locale:Locale SettingsEnableColorSpacePassthroughTooltip}"> ToolTip.Tip="{locale:Locale SettingsEnableColorSpacePassthroughTooltip}">
<TextBlock Text="{locale:Locale SettingsEnableColorSpacePassthrough}" /> <TextBlock Text="{locale:Locale SettingsEnableColorSpacePassthrough}" />
</CheckBox> </CheckBox>

View file

@ -23,21 +23,34 @@
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Orientation="Vertical" Orientation="Vertical"
Spacing="10"> 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}" /> <TextBlock Classes="h1" Text="{locale:Locale SettingsTabNetworkConnection}" />
<CheckBox Margin="10,0,0,0" IsChecked="{Binding EnableInternetAccess}"> <CheckBox Margin="10,0,0,0" IsChecked="{Binding EnableInternetAccess}">
<TextBlock Text="{locale:Locale SettingsTabSystemEnableInternetAccess}" <TextBlock Text="{locale:Locale SettingsTabSystemEnableInternetAccess}"
ToolTip.Tip="{locale:Locale EnableInternetAccessTooltip}" /> ToolTip.Tip="{locale:Locale EnableInternetAccessTooltip}" />
</CheckBox> </CheckBox>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal"> <StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" <TextBlock VerticalAlignment="Center"
Text="{locale:Locale SettingsTabNetworkInterface}" Text="{locale:Locale SettingsTabNetworkInterface}"
ToolTip.Tip="{locale:Locale NetworkInterfaceTooltip}" ToolTip.Tip="{locale:Locale NetworkInterfaceTooltip}"
Width="200" /> Width="200" />
<ComboBox SelectedIndex="{Binding NetworkInterfaceIndex}" <ComboBox SelectedIndex="{Binding NetworkInterfaceIndex}"
ToolTip.Tip="{locale:Locale NetworkInterfaceTooltip}" ToolTip.Tip="{locale:Locale NetworkInterfaceTooltip}"
HorizontalContentAlignment="Left" HorizontalContentAlignment="Left"
ItemsSource="{Binding NetworkInterfaceList}" ItemsSource="{Binding NetworkInterfaceList}"
Width="250" /> Width="250" />
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</Border> </Border>

View file

@ -34,7 +34,7 @@ namespace Ryujinx.Ava.UI.Views.Settings
{ {
var result = await window.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions var result = await window.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{ {
AllowMultiple = false AllowMultiple = false,
}); });
if (result.Count > 0) if (result.Count > 0)
@ -75,9 +75,9 @@ namespace Ryujinx.Ava.UI.Views.Settings
{ {
Patterns = new[] { "*.xaml" }, Patterns = new[] { "*.xaml" },
AppleUniformTypeIdentifiers = new[] { "com.ryujinx.xaml" }, AppleUniformTypeIdentifiers = new[] { "com.ryujinx.xaml" },
MimeTypes = new[] { "application/xaml+xml" } MimeTypes = new[] { "application/xaml+xml" },
} },
} },
}); });
if (result.Count > 0) if (result.Count > 0)

View file

@ -75,9 +75,9 @@ namespace Ryujinx.Ava.UI.Views.User
{ {
Patterns = new[] { "*.jpg", "*.jpeg", "*.png", "*.bmp" }, Patterns = new[] { "*.jpg", "*.jpeg", "*.png", "*.bmp" },
AppleUniformTypeIdentifiers = new[] { "public.jpeg", "public.png", "com.microsoft.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) if (result.Count > 0)

View file

@ -11,6 +11,7 @@
Height="500" Height="500"
MinWidth="500" MinWidth="500"
MinHeight="500" MinHeight="500"
x:DataType="window:CheatWindow"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
mc:Ignorable="d" mc:Ignorable="d"
Focusable="True"> Focusable="True">
@ -40,7 +41,7 @@
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
LineHeight="18" LineHeight="18"
Text="{ReflectionBinding Heading}" Text="{Binding Heading}"
TextAlignment="Center" TextAlignment="Center"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock <TextBlock
@ -61,7 +62,7 @@
MinWidth="160" MinWidth="160"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{ReflectionBinding BuildId}" Text="{Binding BuildId}"
IsReadOnly="True" /> IsReadOnly="True" />
<Border <Border
Grid.Row="3" Grid.Row="3"
@ -77,7 +78,7 @@
MinHeight="300" MinHeight="300"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
ItemsSource="{ReflectionBinding LoadedCheats}"> ItemsSource="{Binding LoadedCheats}">
<TreeView.Styles> <TreeView.Styles>
<Styles> <Styles>
<Style Selector="TreeViewItem:empty /template/ ItemsPresenter"> <Style Selector="TreeViewItem:empty /template/ ItemsPresenter">
@ -120,18 +121,18 @@
Name="SaveButton" Name="SaveButton"
MinWidth="90" MinWidth="90"
Margin="5" Margin="5"
Command="{ReflectionBinding Save}" Command="{Binding Save}"
IsVisible="{ReflectionBinding !NoCheatsFound}"> IsVisible="{Binding !NoCheatsFound}">
<TextBlock Text="{locale:Locale SettingsButtonSave}" /> <TextBlock Text="{locale:Locale SettingsButtonSave}" />
</Button> </Button>
<Button <Button
Name="CancelButton" Name="CancelButton"
MinWidth="90" MinWidth="90"
Margin="5" Margin="5"
Command="{ReflectionBinding Close}"> Command="{Binding Close}">
<TextBlock Text="{locale:Locale InputDialogCancel}" /> <TextBlock Text="{locale:Locale InputDialogCancel}" />
</Button> </Button>
</DockPanel> </DockPanel>
</DockPanel> </DockPanel>
</Grid> </Grid>
</window:StyleableWindow> </window:StyleableWindow>

View file

@ -17,7 +17,7 @@ namespace Ryujinx.Ava.UI.Windows
private readonly string _enabledCheatsPath; private readonly string _enabledCheatsPath;
public bool NoCheatsFound { get; } public bool NoCheatsFound { get; }
private AvaloniaList<CheatsList> LoadedCheats { get; } public AvaloniaList<CheatsList> LoadedCheats { get; }
public string Heading { get; } public string Heading { get; }
public string BuildId { get; } public string BuildId { get; }

View file

@ -39,14 +39,14 @@
Name="EnableAllButton" Name="EnableAllButton"
MinWidth="90" MinWidth="90"
Margin="5" Margin="5"
Command="{ReflectionBinding EnableAll}"> Command="{Binding EnableAll}">
<TextBlock Text="{locale:Locale DlcManagerEnableAllButton}" /> <TextBlock Text="{locale:Locale DlcManagerEnableAllButton}" />
</Button> </Button>
<Button <Button
Name="DisableAllButton" Name="DisableAllButton"
MinWidth="90" MinWidth="90"
Margin="5" Margin="5"
Command="{ReflectionBinding DisableAll}"> Command="{Binding DisableAll}">
<TextBlock Text="{locale:Locale DlcManagerDisableAllButton}" /> <TextBlock Text="{locale:Locale DlcManagerDisableAllButton}" />
</Button> </Button>
</StackPanel> </StackPanel>
@ -157,14 +157,14 @@
Name="AddButton" Name="AddButton"
MinWidth="90" MinWidth="90"
Margin="5" Margin="5"
Command="{ReflectionBinding Add}"> Command="{Binding Add}">
<TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" /> <TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" />
</Button> </Button>
<Button <Button
Name="RemoveAllButton" Name="RemoveAllButton"
MinWidth="90" MinWidth="90"
Margin="5" Margin="5"
Command="{ReflectionBinding RemoveAll}"> Command="{Binding RemoveAll}">
<TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" /> <TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" />
</Button> </Button>
</StackPanel> </StackPanel>
@ -189,4 +189,4 @@
</StackPanel> </StackPanel>
</Panel> </Panel>
</Grid> </Grid>
</UserControl> </UserControl>

View file

@ -15,6 +15,7 @@ using Ryujinx.Graphics.Gpu;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.Input.HLE;
using Ryujinx.Input.SDL2; using Ryujinx.Input.SDL2;
using Ryujinx.Modules; using Ryujinx.Modules;
using Ryujinx.Ui.App.Common; using Ryujinx.Ui.App.Common;
@ -24,8 +25,8 @@ using Ryujinx.Ui.Common.Helper;
using System; using System;
using System.IO; using System.IO;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using InputManager = Ryujinx.Input.HLE.InputManager;
namespace Ryujinx.Ava.UI.Windows namespace Ryujinx.Ava.UI.Windows
{ {
@ -79,35 +80,11 @@ namespace Ryujinx.Ava.UI.Windows
if (Program.PreviewerDetached) if (Program.PreviewerDetached)
{ {
Initialize();
InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL2GamepadDriver()); 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.GetObservable(IsActiveProperty).Subscribe(IsActiveChanged);
this.ScalingChanged += OnScalingChanged; this.ScalingChanged += OnScalingChanged;
} }
ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated;
ApplicationLibrary.ApplicationAdded += ApplicationLibrary_ApplicationAdded;
} }
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
@ -122,36 +99,17 @@ namespace Ryujinx.Ava.UI.Windows
ViewModel.IsActive = obj; ViewModel.IsActive = obj;
} }
public void LoadGameList()
{
if (_isLoading)
{
return;
}
_isLoading = true;
LoadApplications();
_isLoading = false;
}
private void OnScalingChanged(object sender, EventArgs e) private void OnScalingChanged(object sender, EventArgs e)
{ {
Program.DesktopScaleFactor = this.RenderScaling; Program.DesktopScaleFactor = this.RenderScaling;
} }
public void AddApplication(ApplicationData applicationData)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
ViewModel.Applications.Add(applicationData);
});
}
private void ApplicationLibrary_ApplicationAdded(object sender, ApplicationAddedEventArgs e) 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) 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; string path = new FileInfo(args.Application.Path).FullName;
ViewModel.LoadApplication(path); ViewModel.LoadApplication(path).Wait();
} }
args.Handled = true; args.Handled = true;
@ -202,13 +160,10 @@ namespace Ryujinx.Ava.UI.Windows
ViewModel.ShowContent = true; ViewModel.ShowContent = true;
ViewModel.IsLoadingIndeterminate = false; 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) public void ShowLoading(bool startFullscreen = false)
@ -217,13 +172,10 @@ namespace Ryujinx.Ava.UI.Windows
ViewModel.ShowLoadProgress = true; ViewModel.ShowLoadProgress = true;
ViewModel.IsLoadingIndeterminate = 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() private void Initialize()
@ -251,11 +203,11 @@ namespace Ryujinx.Ava.UI.Windows
VirtualFileSystem.ReloadKeySet(); VirtualFileSystem.ReloadKeySet();
ApplicationHelper.Initialize(VirtualFileSystem, AccountManager, LibHacHorizonManager.RyujinxClient, this); ApplicationHelper.Initialize(VirtualFileSystem, AccountManager, LibHacHorizonManager.RyujinxClient);
} }
[SupportedOSPlatform("linux")] [SupportedOSPlatform("linux")]
private static async void ShowVmMaxMapCountWarning() private static async Task ShowVmMaxMapCountWarning()
{ {
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LinuxVmMaxMapCountWarningTextSecondary, LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LinuxVmMaxMapCountWarningTextSecondary,
LinuxHelper.VmMaxMapCount, LinuxHelper.RecommendedVmMaxMapCount); LinuxHelper.VmMaxMapCount, LinuxHelper.RecommendedVmMaxMapCount);
@ -267,7 +219,7 @@ namespace Ryujinx.Ava.UI.Windows
} }
[SupportedOSPlatform("linux")] [SupportedOSPlatform("linux")]
private static async void ShowVmMaxMapCountDialog() private static async Task ShowVmMaxMapCountDialog()
{ {
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LinuxVmMaxMapCountDialogTextPrimary, LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LinuxVmMaxMapCountDialogTextPrimary,
LinuxHelper.RecommendedVmMaxMapCount); LinuxHelper.RecommendedVmMaxMapCount);
@ -313,33 +265,46 @@ namespace Ryujinx.Ava.UI.Windows
private void CheckLaunchState() private void CheckLaunchState()
{ {
if (ShowKeyErrorOnLoad)
{
ShowKeyErrorOnLoad = false;
Dispatcher.UIThread.Post(async () => await
UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys));
}
if (OperatingSystem.IsLinux() && LinuxHelper.VmMaxMapCount < LinuxHelper.RecommendedVmMaxMapCount) 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})"); 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) if (LinuxHelper.PkExecPath is not null)
{ {
Dispatcher.UIThread.Post(ShowVmMaxMapCountDialog); Dispatcher.UIThread.Post(async () =>
{
if (OperatingSystem.IsLinux())
{
await ShowVmMaxMapCountDialog();
}
});
} }
else 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)) 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.WindowHeight = ConfigurationState.Instance.Ui.WindowStartup.WindowSizeHeight * Program.WindowScaleFactor;
ViewModel.WindowWidth = ConfigurationState.Instance.Ui.WindowStartup.WindowSizeWidth * 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)) if (CheckScreenBounds(savedPoint))
{ {
@ -415,6 +380,30 @@ namespace Ryujinx.Ava.UI.Windows
{ {
base.OnOpened(e); 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(); 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; StatusBarView.LoadProgressBar.IsVisible = true;
ViewModel.StatusBarProgressMaximum = 0; ViewModel.StatusBarProgressMaximum = 0;
ViewModel.StatusBarProgressValue = 0; ViewModel.StatusBarProgressValue = 0;
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarGamesLoaded, 0, 0); LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarGamesLoaded, 0, 0);
});
ReloadGameList(); ReloadGameList();
} }
@ -559,9 +545,17 @@ namespace Ryujinx.Ava.UI.Windows
_isLoading = true; _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();
} }
} }
} }

View file

@ -3,7 +3,6 @@ using Avalonia.Controls.Primitives;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Platform; using Avalonia.Platform;
using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Configuration;
using System;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
@ -25,11 +24,6 @@ namespace Ryujinx.Ava.UI.Windows
IconImage = new Bitmap(stream); IconImage = new Bitmap(stream);
} }
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{ {
base.OnApplyTemplate(e); base.OnApplyTemplate(e);

View file

@ -192,7 +192,7 @@ namespace Ryujinx.Common.Collections
{ {
if (start.CompareTo(overlap.End) < 0) if (start.CompareTo(overlap.End) < 0)
{ {
if (overlaps.Length >= overlapCount) if (overlaps.Length <= overlapCount)
{ {
Array.Resize(ref overlaps, overlapCount + ArrayGrowthSize); Array.Resize(ref overlaps, overlapCount + ArrayGrowthSize);
} }

View file

@ -0,0 +1,7 @@
namespace Ryujinx.Common.Configuration.Multiplayer
{
public enum MultiplayerMode
{
Disabled,
}
}

View file

@ -756,6 +756,18 @@ namespace Ryujinx.Common.Memory
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length); 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 public struct Array127<T> : IArray<T> where T : unmanaged
{ {
T _e0; T _e0;
@ -791,5 +803,34 @@ namespace Ryujinx.Common.Memory
[Pure] [Pure]
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length); 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 #pragma warning restore CS0169, IDE0051

View file

@ -5,7 +5,7 @@ namespace Ryujinx.Common
{ {
public class ReactiveObject<T> public class ReactiveObject<T>
{ {
private readonly ReaderWriterLock _readerWriterLock = new(); private readonly ReaderWriterLockSlim _readerWriterLock = new();
private bool _isInitialized; private bool _isInitialized;
private T _value; private T _value;
@ -15,15 +15,15 @@ namespace Ryujinx.Common
{ {
get get
{ {
_readerWriterLock.AcquireReaderLock(Timeout.Infinite); _readerWriterLock.EnterReadLock();
T value = _value; T value = _value;
_readerWriterLock.ReleaseReaderLock(); _readerWriterLock.ExitReadLock();
return value; return value;
} }
set set
{ {
_readerWriterLock.AcquireWriterLock(Timeout.Infinite); _readerWriterLock.EnterWriteLock();
T oldValue = _value; T oldValue = _value;
@ -32,7 +32,7 @@ namespace Ryujinx.Common
_isInitialized = true; _isInitialized = true;
_value = value; _value = value;
_readerWriterLock.ReleaseWriterLock(); _readerWriterLock.ExitWriteLock();
if (!oldIsInitialized || oldValue == null || !oldValue.Equals(_value)) if (!oldIsInitialized || oldValue == null || !oldValue.Equals(_value))
{ {

View file

@ -1,4 +1,6 @@
using System.Net.NetworkInformation; using System.Buffers.Binary;
using System.Net;
using System.Net.NetworkInformation;
namespace Ryujinx.Common.Utilities namespace Ryujinx.Common.Utilities
{ {
@ -62,5 +64,15 @@ namespace Ryujinx.Common.Utilities
return (targetProperties, targetAddressInfo); 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));
}
} }
} }

View file

@ -1,9 +1,11 @@
using Ryujinx.Cpu.AppleHv.Arm; using Ryujinx.Cpu.AppleHv.Arm;
using Ryujinx.Memory; using Ryujinx.Memory;
using System; using System;
using System.Runtime.Versioning;
namespace Ryujinx.Cpu.AppleHv namespace Ryujinx.Cpu.AppleHv
{ {
[SupportedOSPlatform("macos")]
class HvAddressSpace : IDisposable class HvAddressSpace : IDisposable
{ {
private const ulong KernelRegionBase = unchecked((ulong)-(1L << 39)); private const ulong KernelRegionBase = unchecked((ulong)-(1L << 39));

View file

@ -2,10 +2,12 @@ using Ryujinx.Cpu.AppleHv.Arm;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading; using System.Threading;
namespace Ryujinx.Cpu.AppleHv namespace Ryujinx.Cpu.AppleHv
{ {
[SupportedOSPlatform("macos")]
class HvAddressSpaceRange : IDisposable class HvAddressSpaceRange : IDisposable
{ {
private const ulong AllocationGranule = 1UL << 14; private const ulong AllocationGranule = 1UL << 14;

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace Ryujinx.Cpu.AppleHv namespace Ryujinx.Cpu.AppleHv
{ {
@ -12,10 +13,18 @@ namespace Ryujinx.Cpu.AppleHv
#pragma warning restore CS0649 #pragma warning restore CS0649
} }
enum HvExitReason : uint
{
Canceled,
Exception,
VTimerActivated,
Unknown,
}
struct HvVcpuExit struct HvVcpuExit
{ {
#pragma warning disable CS0649 // Field is never assigned to #pragma warning disable CS0649 // Field is never assigned to
public uint Reason; public HvExitReason Reason;
public HvVcpuExitException Exception; public HvVcpuExitException Exception;
#pragma warning restore CS0649 #pragma warning restore CS0649
} }
@ -255,6 +264,7 @@ namespace Ryujinx.Cpu.AppleHv
} }
} }
[SupportedOSPlatform("macos")]
static partial class HvApi static partial class HvApi
{ {
public const string LibraryName = "/System/Library/Frameworks/Hypervisor.framework/Hypervisor"; public const string LibraryName = "/System/Library/Frameworks/Hypervisor.framework/Hypervisor";

View file

@ -1,7 +1,9 @@
using ARMeilleure.Memory; using ARMeilleure.Memory;
using System.Runtime.Versioning;
namespace Ryujinx.Cpu.AppleHv namespace Ryujinx.Cpu.AppleHv
{ {
[SupportedOSPlatform("macos")]
class HvCpuContext : ICpuContext class HvCpuContext : ICpuContext
{ {
private readonly ITickSource _tickSource; private readonly ITickSource _tickSource;

View file

@ -1,7 +1,9 @@
using ARMeilleure.Memory; using ARMeilleure.Memory;
using System.Runtime.Versioning;
namespace Ryujinx.Cpu.AppleHv namespace Ryujinx.Cpu.AppleHv
{ {
[SupportedOSPlatform("macos")]
public class HvEngine : ICpuEngine public class HvEngine : ICpuEngine
{ {
private readonly ITickSource _tickSource; private readonly ITickSource _tickSource;

View file

@ -2,9 +2,12 @@ using ARMeilleure.State;
using Ryujinx.Cpu.AppleHv.Arm; using Ryujinx.Cpu.AppleHv.Arm;
using Ryujinx.Memory.Tracking; using Ryujinx.Memory.Tracking;
using System; using System;
using System.Runtime.Versioning;
using System.Threading;
namespace Ryujinx.Cpu.AppleHv namespace Ryujinx.Cpu.AppleHv
{ {
[SupportedOSPlatform("macos")]
class HvExecutionContext : IExecutionContext class HvExecutionContext : IExecutionContext
{ {
/// <inheritdoc/> /// <inheritdoc/>
@ -67,6 +70,8 @@ namespace Ryujinx.Cpu.AppleHv
private readonly ExceptionCallbacks _exceptionCallbacks; private readonly ExceptionCallbacks _exceptionCallbacks;
private int _interruptRequested;
public HvExecutionContext(ICounter counter, ExceptionCallbacks exceptionCallbacks) public HvExecutionContext(ICounter counter, ExceptionCallbacks exceptionCallbacks)
{ {
_counter = counter; _counter = counter;
@ -111,7 +116,15 @@ namespace Ryujinx.Cpu.AppleHv
/// <inheritdoc/> /// <inheritdoc/>
public void RequestInterrupt() 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/> /// <inheritdoc/>
@ -131,9 +144,9 @@ namespace Ryujinx.Cpu.AppleHv
{ {
HvApi.hv_vcpu_run(vcpu.Handle).ThrowOnError(); 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; uint hvEsr = (uint)vcpu.ExitInfo->Exception.Syndrome;
ExceptionClass hvEc = (ExceptionClass)(hvEsr >> 26); ExceptionClass hvEc = (ExceptionClass)(hvEsr >> 26);
@ -146,14 +159,22 @@ namespace Ryujinx.Cpu.AppleHv
address = SynchronousException(memoryManager, ref vcpu); address = SynchronousException(memoryManager, ref vcpu);
HvApi.hv_vcpu_set_reg(vcpu.Handle, HvReg.PC, address).ThrowOnError(); 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); ReturnToPool(vcpu);
InterruptHandler(); InterruptHandler();
vcpu = RentFromPool(memoryManager.AddressSpace, vcpu); 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 else
{ {

View file

@ -46,14 +46,5 @@ namespace Ryujinx.Cpu.AppleHv
{ {
_v[index] = value; _v[index] = value;
} }
public void RequestInterrupt()
{
}
public bool GetAndClearInterruptRequested()
{
return false;
}
} }
} }

View file

@ -2,10 +2,11 @@ using ARMeilleure.State;
using Ryujinx.Memory; using Ryujinx.Memory;
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Runtime.Versioning;
namespace Ryujinx.Cpu.AppleHv namespace Ryujinx.Cpu.AppleHv
{ {
[SupportedOSPlatform("macos")]
class HvExecutionContextVcpu : IHvExecutionContext class HvExecutionContextVcpu : IHvExecutionContext
{ {
private static readonly MemoryBlock _setSimdFpRegFuncMem; private static readonly MemoryBlock _setSimdFpRegFuncMem;
@ -135,7 +136,6 @@ namespace Ryujinx.Cpu.AppleHv
} }
private readonly ulong _vcpu; private readonly ulong _vcpu;
private int _interruptRequested;
public HvExecutionContextVcpu(ulong vcpu) public HvExecutionContextVcpu(ulong vcpu)
{ {
@ -181,16 +181,8 @@ namespace Ryujinx.Cpu.AppleHv
public void RequestInterrupt() public void RequestInterrupt()
{ {
if (Interlocked.Exchange(ref _interruptRequested, 1) == 0) ulong vcpu = _vcpu;
{ HvApi.hv_vcpus_exit(ref vcpu, 1);
ulong vcpu = _vcpu;
HvApi.hv_vcpus_exit(ref vcpu, 1);
}
}
public bool GetAndClearInterruptRequested()
{
return Interlocked.Exchange(ref _interruptRequested, 0) != 0;
} }
} }
} }

View file

@ -1,8 +1,10 @@
using Ryujinx.Memory; using Ryujinx.Memory;
using System; using System;
using System.Runtime.Versioning;
namespace Ryujinx.Cpu.AppleHv namespace Ryujinx.Cpu.AppleHv
{ {
[SupportedOSPlatform("macos")]
readonly struct HvMemoryBlockAllocation : IDisposable readonly struct HvMemoryBlockAllocation : IDisposable
{ {
private readonly HvMemoryBlockAllocator _owner; private readonly HvMemoryBlockAllocator _owner;

View file

@ -1,7 +1,9 @@
using Ryujinx.Memory; using Ryujinx.Memory;
using System.Runtime.Versioning;
namespace Ryujinx.Cpu.AppleHv namespace Ryujinx.Cpu.AppleHv
{ {
[SupportedOSPlatform("macos")]
class HvMemoryBlockAllocator : PrivateMemoryAllocatorImpl<HvMemoryBlockAllocator.Block> class HvMemoryBlockAllocator : PrivateMemoryAllocatorImpl<HvMemoryBlockAllocator.Block>
{ {
public class Block : PrivateMemoryAllocator.Block public class Block : PrivateMemoryAllocator.Block

View file

@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading; using System.Threading;
namespace Ryujinx.Cpu.AppleHv namespace Ryujinx.Cpu.AppleHv
@ -14,6 +15,7 @@ namespace Ryujinx.Cpu.AppleHv
/// <summary> /// <summary>
/// Represents a CPU memory manager which maps guest virtual memory directly onto the Hypervisor page table. /// Represents a CPU memory manager which maps guest virtual memory directly onto the Hypervisor page table.
/// </summary> /// </summary>
[SupportedOSPlatform("macos")]
public class HvMemoryManager : MemoryManagerBase, IMemoryManager, IVirtualMemoryManagerTracked, IWritableBlock public class HvMemoryManager : MemoryManagerBase, IMemoryManager, IVirtualMemoryManagerTracked, IWritableBlock
{ {
public const int PageBits = 12; public const int PageBits = 12;

View file

@ -1,7 +1,15 @@
using System.Diagnostics;
using System.Runtime.Versioning;
namespace Ryujinx.Cpu.AppleHv namespace Ryujinx.Cpu.AppleHv
{ {
[SupportedOSPlatform("macos")]
unsafe class HvVcpu unsafe class HvVcpu
{ {
private const ulong InterruptIntervalNs = 16 * 1000000; // 16 ms
private static ulong _interruptTimeDeltaTicks = 0;
public readonly ulong Handle; public readonly ulong Handle;
public readonly HvVcpuExit* ExitInfo; public readonly HvVcpuExit* ExitInfo;
public readonly IHvExecutionContext ShadowContext; public readonly IHvExecutionContext ShadowContext;
@ -21,5 +29,28 @@ namespace Ryujinx.Cpu.AppleHv
NativeContext = nativeContext; NativeContext = nativeContext;
IsEphemeral = isEphemeral; 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();
}
} }
} }

View file

@ -1,8 +1,10 @@
using System; using System;
using System.Runtime.Versioning;
using System.Threading; using System.Threading;
namespace Ryujinx.Cpu.AppleHv namespace Ryujinx.Cpu.AppleHv
{ {
[SupportedOSPlatform("macos")]
class HvVcpuPool class HvVcpuPool
{ {
// Since there's a limit on the number of VCPUs we can create, // 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); HvVcpu vcpu = new(vcpuHandle, exitInfo, shadowContext, nativeContext, isEphemeral);
vcpu.EnableAndUpdateVTimer();
return vcpu; return vcpu;
} }

View file

@ -1,8 +1,10 @@
using Ryujinx.Memory; using Ryujinx.Memory;
using System; using System;
using System.Runtime.Versioning;
namespace Ryujinx.Cpu.AppleHv namespace Ryujinx.Cpu.AppleHv
{ {
[SupportedOSPlatform("macos")]
static class HvVm static class HvVm
{ {
// This alignment allows us to use larger blocks on the page table. // This alignment allows us to use larger blocks on the page table.

View file

@ -2,7 +2,7 @@ using ARMeilleure.State;
namespace Ryujinx.Cpu.AppleHv namespace Ryujinx.Cpu.AppleHv
{ {
public interface IHvExecutionContext interface IHvExecutionContext
{ {
ulong Pc { get; set; } ulong Pc { get; set; }
ulong ElrEl1 { get; set; } ulong ElrEl1 { get; set; }
@ -39,8 +39,5 @@ namespace Ryujinx.Cpu.AppleHv
SetV(i, context.GetV(i)); SetV(i, context.GetV(i));
} }
} }
void RequestInterrupt();
bool GetAndClearInterruptRequested();
} }
} }

View file

@ -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);
}
}

View file

@ -39,6 +39,7 @@ namespace Ryujinx.Graphics.GAL
public readonly bool SupportsShaderBarrierDivergence; public readonly bool SupportsShaderBarrierDivergence;
public readonly bool SupportsShaderFloat64; public readonly bool SupportsShaderFloat64;
public readonly bool SupportsTextureShadowLod; public readonly bool SupportsTextureShadowLod;
public readonly bool SupportsVertexStoreAndAtomics;
public readonly bool SupportsViewportIndexVertexTessellation; public readonly bool SupportsViewportIndexVertexTessellation;
public readonly bool SupportsViewportMask; public readonly bool SupportsViewportMask;
public readonly bool SupportsViewportSwizzle; public readonly bool SupportsViewportSwizzle;
@ -53,7 +54,9 @@ namespace Ryujinx.Graphics.GAL
public readonly int MaximumComputeSharedMemorySize; public readonly int MaximumComputeSharedMemorySize;
public readonly float MaximumSupportedAnisotropy; public readonly float MaximumSupportedAnisotropy;
public readonly int ShaderSubgroupSize;
public readonly int StorageBufferOffsetAlignment; public readonly int StorageBufferOffsetAlignment;
public readonly int TextureBufferOffsetAlignment;
public readonly int GatherBiasPrecision; public readonly int GatherBiasPrecision;
@ -91,6 +94,7 @@ namespace Ryujinx.Graphics.GAL
bool supportsShaderBarrierDivergence, bool supportsShaderBarrierDivergence,
bool supportsShaderFloat64, bool supportsShaderFloat64,
bool supportsTextureShadowLod, bool supportsTextureShadowLod,
bool supportsVertexStoreAndAtomics,
bool supportsViewportIndexVertexTessellation, bool supportsViewportIndexVertexTessellation,
bool supportsViewportMask, bool supportsViewportMask,
bool supportsViewportSwizzle, bool supportsViewportSwizzle,
@ -103,7 +107,9 @@ namespace Ryujinx.Graphics.GAL
uint maximumImagesPerStage, uint maximumImagesPerStage,
int maximumComputeSharedMemorySize, int maximumComputeSharedMemorySize,
float maximumSupportedAnisotropy, float maximumSupportedAnisotropy,
int shaderSubgroupSize,
int storageBufferOffsetAlignment, int storageBufferOffsetAlignment,
int textureBufferOffsetAlignment,
int gatherBiasPrecision) int gatherBiasPrecision)
{ {
Api = api; Api = api;
@ -139,6 +145,7 @@ namespace Ryujinx.Graphics.GAL
SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence; SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence;
SupportsShaderFloat64 = supportsShaderFloat64; SupportsShaderFloat64 = supportsShaderFloat64;
SupportsTextureShadowLod = supportsTextureShadowLod; SupportsTextureShadowLod = supportsTextureShadowLod;
SupportsVertexStoreAndAtomics = supportsVertexStoreAndAtomics;
SupportsViewportIndexVertexTessellation = supportsViewportIndexVertexTessellation; SupportsViewportIndexVertexTessellation = supportsViewportIndexVertexTessellation;
SupportsViewportMask = supportsViewportMask; SupportsViewportMask = supportsViewportMask;
SupportsViewportSwizzle = supportsViewportSwizzle; SupportsViewportSwizzle = supportsViewportSwizzle;
@ -151,7 +158,9 @@ namespace Ryujinx.Graphics.GAL
MaximumImagesPerStage = maximumImagesPerStage; MaximumImagesPerStage = maximumImagesPerStage;
MaximumComputeSharedMemorySize = maximumComputeSharedMemorySize; MaximumComputeSharedMemorySize = maximumComputeSharedMemorySize;
MaximumSupportedAnisotropy = maximumSupportedAnisotropy; MaximumSupportedAnisotropy = maximumSupportedAnisotropy;
ShaderSubgroupSize = shaderSubgroupSize;
StorageBufferOffsetAlignment = storageBufferOffsetAlignment; StorageBufferOffsetAlignment = storageBufferOffsetAlignment;
TextureBufferOffsetAlignment = textureBufferOffsetAlignment;
GatherBiasPrecision = gatherBiasPrecision; GatherBiasPrecision = gatherBiasPrecision;
} }
} }

View file

@ -335,6 +335,45 @@ namespace Ryujinx.Graphics.GAL
return 1; 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> /// <summary>
/// Checks if the texture format is valid to use as image format. /// Checks if the texture format is valid to use as image format.
/// </summary> /// </summary>

View file

@ -15,14 +15,6 @@ namespace Ryujinx.Graphics.GAL
BufferImage, BufferImage,
} }
public enum ResourceAccess : byte
{
None = 0,
Read = 1,
Write = 2,
ReadWrite = Read | Write,
}
[Flags] [Flags]
public enum ResourceStages : byte public enum ResourceStages : byte
{ {
@ -81,19 +73,17 @@ namespace Ryujinx.Graphics.GAL
public int Binding { get; } public int Binding { get; }
public ResourceType Type { get; } public ResourceType Type { get; }
public ResourceStages Stages { 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; Binding = binding;
Type = type; Type = type;
Stages = stages; Stages = stages;
Access = access;
} }
public override int GetHashCode() public override int GetHashCode()
{ {
return HashCode.Combine(Binding, Type, Stages, Access); return HashCode.Combine(Binding, Type, Stages);
} }
public override bool Equals(object obj) public override bool Equals(object obj)
@ -103,7 +93,7 @@ namespace Ryujinx.Graphics.GAL
public bool Equals(ResourceUsage other) 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) public static bool operator ==(ResourceUsage left, ResourceUsage right)

View file

@ -32,6 +32,11 @@ namespace Ryujinx.Graphics.Gpu.Engine
/// </summary> /// </summary>
public ref TState State => ref _state.State; public ref TState State => ref _state.State;
/// <summary>
/// Current shadow state.
/// </summary>
public ref TState ShadowState => ref _shadowState.State;
/// <summary> /// <summary>
/// Creates a new instance of the device state, with shadow state. /// Creates a new instance of the device state, with shadow state.
/// </summary> /// </summary>

View file

@ -13,7 +13,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.GPFifo
/// <summary> /// <summary>
/// Represents a GPU General Purpose FIFO command processor. /// Represents a GPU General Purpose FIFO command processor.
/// </summary> /// </summary>
class GPFifoProcessor class GPFifoProcessor : IDisposable
{ {
private const int MacrosCount = 0x80; private const int MacrosCount = 0x80;
private const int MacroIndexMask = MacrosCount - 1; private const int MacroIndexMask = MacrosCount - 1;
@ -327,5 +327,19 @@ namespace Ryujinx.Graphics.Gpu.Engine.GPFifo
{ {
_3dClass.PerformDeferredDraws(); _3dClass.PerformDeferredDraws();
} }
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_3dClass.Dispose();
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
} }
} }

View file

@ -1,7 +1,10 @@
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.Graphics.Device; using Ryujinx.Graphics.Device;
using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Engine.GPFifo; using Ryujinx.Graphics.Gpu.Engine.GPFifo;
using Ryujinx.Graphics.Gpu.Engine.Threed;
using Ryujinx.Graphics.Gpu.Engine.Types;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -15,9 +18,18 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
private const int ColorLayerCountOffset = 0x818; private const int ColorLayerCountOffset = 0x818;
private const int ColorStructSize = 0x40; private const int ColorStructSize = 0x40;
private const int ZetaLayerCountOffset = 0x1230; private const int ZetaLayerCountOffset = 0x1230;
private const int UniformBufferBindVertexOffset = 0x2410;
private const int FirstVertexOffset = 0x1434;
private const int IndirectIndexedDataEntrySize = 0x14; 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 GPFifoProcessor _processor;
private readonly MacroHLEFunctionName _functionName; private readonly MacroHLEFunctionName _functionName;
@ -49,6 +61,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
{ {
switch (_functionName) switch (_functionName)
{ {
case MacroHLEFunctionName.BindShaderProgram:
BindShaderProgram(state, arg0);
break;
case MacroHLEFunctionName.ClearColor: case MacroHLEFunctionName.ClearColor:
ClearColor(state, arg0); ClearColor(state, arg0);
break; break;
@ -58,6 +73,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
case MacroHLEFunctionName.DrawArraysInstanced: case MacroHLEFunctionName.DrawArraysInstanced:
DrawArraysInstanced(state, arg0); DrawArraysInstanced(state, arg0);
break; break;
case MacroHLEFunctionName.DrawElements:
DrawElements(state, arg0);
break;
case MacroHLEFunctionName.DrawElementsInstanced: case MacroHLEFunctionName.DrawElementsInstanced:
DrawElementsInstanced(state, arg0); DrawElementsInstanced(state, arg0);
break; break;
@ -67,6 +85,21 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
case MacroHLEFunctionName.MultiDrawElementsIndirectCount: case MacroHLEFunctionName.MultiDrawElementsIndirectCount:
MultiDrawElementsIndirectCount(state, arg0); MultiDrawElementsIndirectCount(state, arg0);
break; 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: default:
throw new NotImplementedException(_functionName.ToString()); throw new NotImplementedException(_functionName.ToString());
} }
@ -75,6 +108,149 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
Fifo.Clear(); 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> /// <summary>
/// Clears one bound color target. /// Clears one bound color target.
/// </summary> /// </summary>
@ -129,6 +305,36 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
indexed: false); 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> /// <summary>
/// Performs a indexed draw. /// Performs a indexed draw.
/// </summary> /// </summary>

View file

@ -6,11 +6,19 @@
enum MacroHLEFunctionName enum MacroHLEFunctionName
{ {
None, None,
BindShaderProgram,
ClearColor, ClearColor,
ClearDepthStencil, ClearDepthStencil,
DrawArraysInstanced, DrawArraysInstanced,
DrawElements,
DrawElementsInstanced, DrawElementsInstanced,
DrawElementsIndirect, DrawElementsIndirect,
MultiDrawElementsIndirectCount, MultiDrawElementsIndirectCount,
UpdateBlendState,
UpdateColorMasks,
UpdateUniformBufferState,
UpdateUniformBufferStateCbu,
UpdateUniformBufferStateCbuV2
} }
} }

View file

@ -46,12 +46,19 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
private static readonly TableEntry[] _table = new TableEntry[] 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.ClearColor, new Hash128(0xA9FB28D1DC43645A, 0xB177E5D2EAE67FB0), 0x28),
new(MacroHLEFunctionName.ClearDepthStencil, new Hash128(0x1B96CB77D4879F4F, 0x8557032FE0C965FB), 0x24), new(MacroHLEFunctionName.ClearDepthStencil, new Hash128(0x1B96CB77D4879F4F, 0x8557032FE0C965FB), 0x24),
new(MacroHLEFunctionName.DrawArraysInstanced, new Hash128(0x197FB416269DBC26, 0x34288C01DDA82202), 0x48), 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.DrawElementsInstanced, new Hash128(0x1A501FD3D54EC8E0, 0x6CF570CF79DA74D6), 0x5c),
new(MacroHLEFunctionName.DrawElementsIndirect, new Hash128(0x86A3E8E903AF8F45, 0xD35BBA07C23860A4), 0x7c), new(MacroHLEFunctionName.DrawElementsIndirect, new Hash128(0x86A3E8E903AF8F45, 0xD35BBA07C23860A4), 0x7c),
new(MacroHLEFunctionName.MultiDrawElementsIndirectCount, new Hash128(0x890AF57ED3FB1C37, 0x35D0C95C61F5386F), 0x19C), 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> /// <summary>
@ -62,18 +69,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
/// <returns>True if the host supports the HLE macro, false otherwise</returns> /// <returns>True if the host supports the HLE macro, false otherwise</returns>
private static bool IsMacroHLESupported(Capabilities caps, MacroHLEFunctionName name) private static bool IsMacroHLESupported(Capabilities caps, MacroHLEFunctionName name)
{ {
if (name == MacroHLEFunctionName.ClearColor || if (name == MacroHLEFunctionName.MultiDrawElementsIndirectCount)
name == MacroHLEFunctionName.ClearDepthStencil ||
name == MacroHLEFunctionName.DrawArraysInstanced ||
name == MacroHLEFunctionName.DrawElementsInstanced ||
name == MacroHLEFunctionName.DrawElementsIndirect)
{
return true;
}
else if (name == MacroHLEFunctionName.MultiDrawElementsIndirectCount)
{ {
return caps.SupportsIndirectParameters; return caps.SupportsIndirectParameters;
} }
else if (name != MacroHLEFunctionName.None)
{
return true;
}
return false; return false;
} }

View file

@ -10,4 +10,22 @@ namespace Ryujinx.Graphics.Gpu.Engine
MethodPassthrough = 2, MethodPassthrough = 2,
MethodReplay = 3, 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;
}
}
} }

View file

@ -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)));
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

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