Compare commits
191 Commits
Author | SHA1 | Date | |
---|---|---|---|
e9edf0ab7f | |||
6e40b64554 | |||
1a676ee913 | |||
a23d8cb92f | |||
ab12fbe963 | |||
d0cc13ce0b | |||
65c035cdf8 | |||
56c5dbe557 | |||
5976a5161b | |||
3d4dea624d | |||
89a274c6a6 | |||
c6f8bfed90 | |||
9b94662b4b | |||
216026c096 | |||
7070cf6ae5 | |||
9839cd56fb | |||
99f46e22e2 | |||
22fb8c9d4f | |||
2f93ae9a19 | |||
3224ddeeb4 | |||
446f2854a5 | |||
8884d1fd73 | |||
268c9aecf8 | |||
e916662b0f | |||
2ddd3dd4a7 | |||
a8f7ababb5 | |||
22e3ff06b5 | |||
9480e5c5ce | |||
0652813b0f | |||
e7f2342eba | |||
543d75a587 | |||
338ff79e1e | |||
80201466ae | |||
7a971edb57 | |||
c1b0ab805a | |||
3e6e0e4afa | |||
808803d97a | |||
ead9a25141 | |||
3e0d67533f | |||
0b55914864 | |||
451a28afb8 | |||
12b235700c | |||
3be616207d | |||
791bf22109 | |||
66b1d59c66 | |||
c8bb05633e | |||
fb1171a21e | |||
22c0aa9c90 | |||
6d28b64312 | |||
05c041feeb | |||
8c2da1aa04 | |||
5def0429f8 | |||
8e74fa3456 | |||
6208c3e6f0 | |||
7124d679fd | |||
b323a01738 | |||
f6d24449b6 | |||
72bdc24db8 | |||
43514771bf | |||
dbfe859ed7 | |||
c94a73ec60 | |||
20a280525f | |||
75a4ea3370 | |||
d26ef2eec3 | |||
a0552fd78b | |||
bb8c5ebae1 | |||
50bdda5baa | |||
18df25f66f | |||
e19e7622a3 | |||
26026d1357 | |||
24068b023c | |||
1217a8e69b | |||
732db7581f | |||
fdd3263e31 | |||
ce607db944 | |||
6b4ee82e5d | |||
e2a655f1a4 | |||
d9a18919b0 | |||
8354434a37 | |||
5a900f38c5 | |||
a3a63d4394 | |||
3924bd1a43 | |||
50458b2472 | |||
dda0f26067 | |||
4e1a60328e | |||
2505a1abcd | |||
bc4d99a078 | |||
ec6cb0abb4 | |||
53b5985da6 | |||
87f238be60 | |||
4d0dbbfae2 | |||
08384ee5a8 | |||
d4d0a48bfe | |||
57d8afd0c9 | |||
c43fb92bbf | |||
167f50bbcd | |||
ba91f5d401 | |||
79f6c18a9b | |||
4f63782bac | |||
6f5fcb7970 | |||
6ef8946169 | |||
42340fc743 | |||
103e7cb021 | |||
31ed061bea | |||
4218311e6a | |||
e37735ed26 | |||
74a18b7c18 | |||
74fe814329 | |||
d1a093e5ca | |||
dfb14a5607 | |||
904a5ffcb4 | |||
946633276b | |||
baf94e0e3e | |||
cf6201a4a6 | |||
18909195d1 | |||
f06d22d6f0 | |||
84d6e8d121 | |||
95c4912d58 | |||
356a75af0b | |||
4ae9921063 | |||
6a8ac389e5 | |||
8dd1eb333c | |||
7dc3a62c14 | |||
e59dba42ef | |||
bd6937ae5c | |||
b82e789d4f | |||
4a6724622e | |||
0c73eba3db | |||
a082e14ede | |||
d29da11d5f | |||
ea07328aea | |||
a0b3d82ee0 | |||
609de33b0b | |||
dfc0819e72 | |||
d4803356bb | |||
459efd0db7 | |||
8bb7a3fc97 | |||
628d092fc6 | |||
6c90d50c8e | |||
d56bab1e24 | |||
a37e2d6e44 | |||
25123232bd | |||
8927e0669f | |||
bbed3b9926 | |||
24c8b0edc0 | |||
e5066449a5 | |||
d704bcd93b | |||
c94f0fbb83 | |||
d1b30fbe08 | |||
4505a7f162 | |||
ccbbaddbcb | |||
8bf102d2cd | |||
2adf031830 | |||
bb4a28b525 | |||
a8fbcdae9f | |||
4e81ab4229 | |||
4117c13377 | |||
20a392ad55 | |||
70fcba39de | |||
7795b662a9 | |||
30bdc4544e | |||
f6475cca17 | |||
0335c52254 | |||
b8d992e5a7 | |||
a620cbcc90 | |||
cea204d48e | |||
35fb409e85 | |||
d7ec4308b4 | |||
fbdd390f90 | |||
f33fea3287 | |||
5d3eea40be | |||
cd37c75b82 | |||
43705c2320 | |||
371e6fa24c | |||
1d9b63cc6a | |||
795539bc82 | |||
dd2e851e95 | |||
2ca70eb9a0 | |||
6575952432 | |||
9a28ba72b1 | |||
34a9922b57 | |||
4df22eb867 | |||
f241f88558 | |||
90455a05e6 | |||
edc76883db | |||
427b7d06b5 | |||
331c07807f | |||
a772b073ec | |||
870d9599cc | |||
2dbbc9bc05 | |||
72634c80f4 |
@ -17,8 +17,8 @@ tab_width = 4
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
# JSON files
|
||||
[*.json]
|
||||
# Markdown, JSON, YAML, props and csproj files
|
||||
[*.{md,json,yml,props,csproj}]
|
||||
|
||||
# Indentation and spacing
|
||||
indent_size = 2
|
||||
@ -259,12 +259,12 @@ dotnet_diagnostic.CA1861.severity = none
|
||||
# Disable "Prefer using 'string.Equals(string, StringComparison)' to perform a case-insensitive comparison, but keep in mind that this might cause subtle changes in behavior, so make sure to conduct thorough testing after applying the suggestion, or if culturally sensitive comparison is not required, consider using 'StringComparison.OrdinalIgnoreCase'"
|
||||
dotnet_diagnostic.CA1862.severity = none
|
||||
|
||||
[src/Ryujinx.HLE/HOS/Services/**.cs]
|
||||
# Disable "mark members as static" rule for services
|
||||
[src/Ryujinx/UI/ViewModels/**.cs]
|
||||
# Disable "mark members as static" rule for ViewModels
|
||||
dotnet_diagnostic.CA1822.severity = none
|
||||
|
||||
[src/Ryujinx.Ava/UI/ViewModels/**.cs]
|
||||
# Disable "mark members as static" rule for ViewModels
|
||||
[src/Ryujinx.HLE/HOS/Services/**.cs]
|
||||
# Disable "mark members as static" rule for services
|
||||
dotnet_diagnostic.CA1822.severity = none
|
||||
|
||||
[src/Ryujinx.Tests/Cpu/*.cs]
|
||||
|
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@ -7,7 +7,7 @@ updates:
|
||||
labels:
|
||||
- "infra"
|
||||
reviewers:
|
||||
- marysaka
|
||||
- TSRBerry
|
||||
commit-message:
|
||||
prefix: "ci"
|
||||
|
||||
@ -19,7 +19,7 @@ updates:
|
||||
labels:
|
||||
- "infra"
|
||||
reviewers:
|
||||
- marysaka
|
||||
- TSRBerry
|
||||
commit-message:
|
||||
prefix: nuget
|
||||
groups:
|
||||
|
2
.github/labeler.yml
vendored
2
.github/labeler.yml
vendored
@ -20,7 +20,7 @@ gpu:
|
||||
|
||||
gui:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['src/Ryujinx/**', 'src/Ryujinx.Ui.Common/**', 'src/Ryujinx.Ui.LocaleGenerator/**', 'src/Ryujinx.Ava/**']
|
||||
- any-glob-to-any-file: ['src/Ryujinx/**', 'src/Ryujinx.UI.Common/**', 'src/Ryujinx.UI.LocaleGenerator/**', 'src/Ryujinx.Gtk3/**']
|
||||
|
||||
horizon:
|
||||
- changed-files:
|
||||
|
7
.github/reviewers.yml
vendored
7
.github/reviewers.yml
vendored
@ -1,31 +1,24 @@
|
||||
audio:
|
||||
- marysaka
|
||||
|
||||
cpu:
|
||||
- gdkchan
|
||||
- riperiperi
|
||||
- marysaka
|
||||
- LDj3SNuD
|
||||
|
||||
gpu:
|
||||
- gdkchan
|
||||
- riperiperi
|
||||
- marysaka
|
||||
|
||||
gui:
|
||||
- Ack77
|
||||
- emmauss
|
||||
- TSRBerry
|
||||
- marysaka
|
||||
|
||||
horizon:
|
||||
- gdkchan
|
||||
- Ack77
|
||||
- marysaka
|
||||
- TSRBerry
|
||||
|
||||
infra:
|
||||
- marysaka
|
||||
- TSRBerry
|
||||
|
||||
default:
|
||||
|
87
.github/workflows/build.yml
vendored
87
.github/workflows/build.yml
vendored
@ -10,28 +10,17 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: ${{ matrix.OS_NAME }} (${{ matrix.configuration }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: ${{ matrix.platform.name }} (${{ matrix.configuration }})
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
configuration: [Debug, Release]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
OS_NAME: Linux x64
|
||||
DOTNET_RUNTIME_IDENTIFIER: linux-x64
|
||||
RELEASE_ZIP_OS_NAME: linux_x64
|
||||
|
||||
- os: macOS-latest
|
||||
OS_NAME: macOS x64
|
||||
DOTNET_RUNTIME_IDENTIFIER: osx-x64
|
||||
RELEASE_ZIP_OS_NAME: osx_x64
|
||||
|
||||
- os: windows-latest
|
||||
OS_NAME: Windows x64
|
||||
DOTNET_RUNTIME_IDENTIFIER: win-x64
|
||||
RELEASE_ZIP_OS_NAME: win_x64
|
||||
platform:
|
||||
- { name: win-x64, os: windows-latest, zip_os_name: win_x64 }
|
||||
- { name: linux-x64, os: ubuntu-latest, zip_os_name: linux_x64 }
|
||||
- { name: linux-arm64, os: ubuntu-latest, zip_os_name: linux_arm64 }
|
||||
- { name: osx-x64, os: macos-13, zip_os_name: osx_x64 }
|
||||
|
||||
fail-fast: false
|
||||
steps:
|
||||
@ -40,7 +29,7 @@ jobs:
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
|
||||
- name: Overwrite csc problem matcher
|
||||
run: echo "::add-matcher::.github/csc.json"
|
||||
|
||||
@ -49,6 +38,16 @@ jobs:
|
||||
run: echo "result=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Change config filename
|
||||
run: sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
shell: bash
|
||||
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
|
||||
|
||||
- name: Change config filename for macOS
|
||||
run: sed -r -i '' 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
shell: bash
|
||||
if: github.event_name == 'pull_request' && matrix.platform.os == 'macos-13'
|
||||
|
||||
- name: Build
|
||||
run: dotnet build -c "${{ matrix.configuration }}" -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER
|
||||
|
||||
@ -58,46 +57,47 @@ jobs:
|
||||
commands: dotnet test --no-build -c "${{ matrix.configuration }}"
|
||||
timeout-minutes: 10
|
||||
retry-codes: 139
|
||||
if: matrix.platform.name != 'linux-arm64'
|
||||
|
||||
- name: Publish Ryujinx
|
||||
run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.DOTNET_RUNTIME_IDENTIFIER }}" -o ./publish -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx --self-contained true
|
||||
if: github.event_name == 'pull_request' && matrix.os != 'macOS-latest'
|
||||
run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx --self-contained true
|
||||
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
|
||||
|
||||
- name: Publish Ryujinx.Headless.SDL2
|
||||
run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.DOTNET_RUNTIME_IDENTIFIER }}" -o ./publish_sdl2_headless -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Headless.SDL2 --self-contained true
|
||||
if: github.event_name == 'pull_request' && matrix.os != 'macOS-latest'
|
||||
run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish_sdl2_headless -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Headless.SDL2 --self-contained true
|
||||
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
|
||||
|
||||
- name: Publish Ryujinx.Ava
|
||||
run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.DOTNET_RUNTIME_IDENTIFIER }}" -o ./publish_ava -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Ava --self-contained true
|
||||
if: github.event_name == 'pull_request' && matrix.os != 'macOS-latest'
|
||||
- name: Publish Ryujinx.Gtk3
|
||||
run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish_gtk -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Gtk3 --self-contained true
|
||||
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
|
||||
|
||||
- name: Set executable bit
|
||||
run: |
|
||||
chmod +x ./publish/Ryujinx ./publish/Ryujinx.sh
|
||||
chmod +x ./publish_sdl2_headless/Ryujinx.Headless.SDL2 ./publish_sdl2_headless/Ryujinx.sh
|
||||
chmod +x ./publish_ava/Ryujinx.Ava ./publish_ava/Ryujinx.sh
|
||||
if: github.event_name == 'pull_request' && matrix.os == 'ubuntu-latest'
|
||||
chmod +x ./publish_gtk/Ryujinx.Gtk3 ./publish_gtk/Ryujinx.sh
|
||||
if: github.event_name == 'pull_request' && matrix.platform.os == 'ubuntu-latest'
|
||||
|
||||
- name: Upload Ryujinx artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.RELEASE_ZIP_OS_NAME }}
|
||||
name: ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}
|
||||
path: publish
|
||||
if: github.event_name == 'pull_request' && matrix.os != 'macOS-latest'
|
||||
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
|
||||
|
||||
- name: Upload Ryujinx.Headless.SDL2 artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sdl2-ryujinx-headless-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.RELEASE_ZIP_OS_NAME }}
|
||||
name: sdl2-ryujinx-headless-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}
|
||||
path: publish_sdl2_headless
|
||||
if: github.event_name == 'pull_request' && matrix.os != 'macOS-latest'
|
||||
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
|
||||
|
||||
- name: Upload Ryujinx.Ava artifact
|
||||
- name: Upload Ryujinx.Gtk3 artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ava-ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.RELEASE_ZIP_OS_NAME }}
|
||||
path: publish_ava
|
||||
if: github.event_name == 'pull_request' && matrix.os != 'macOS-latest'
|
||||
name: gtk-ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}
|
||||
path: publish_gtk
|
||||
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
|
||||
|
||||
build_macos:
|
||||
name: macOS Universal (${{ matrix.configuration }})
|
||||
@ -135,19 +135,24 @@ jobs:
|
||||
id: git_short_hash
|
||||
run: echo "result=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Publish macOS Ryujinx.Ava
|
||||
- name: Change config filename
|
||||
run: sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
shell: bash
|
||||
if: github.event_name == 'pull_request'
|
||||
|
||||
- name: Publish macOS Ryujinx
|
||||
run: |
|
||||
./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"
|
||||
./distribution/macos/create_macos_build_ava.sh . publish_tmp publish ./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 artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ava-ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-macos_universal
|
||||
path: "publish_ava/*.tar.gz"
|
||||
name: ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-macos_universal
|
||||
path: "publish/*.tar.gz"
|
||||
if: github.event_name == 'pull_request'
|
||||
|
||||
- name: Upload Ryujinx.Headless.SDL2 artifact
|
||||
|
2
.github/workflows/checks.yml
vendored
2
.github/workflows/checks.yml
vendored
@ -8,7 +8,7 @@ on:
|
||||
- '!.github/**'
|
||||
- '!*.yml'
|
||||
- '!*.config'
|
||||
- '!README.md'
|
||||
- '!*.md'
|
||||
- '.github/workflows/*.yml'
|
||||
|
||||
permissions:
|
||||
|
72
.github/workflows/flatpak.yml
vendored
72
.github/workflows/flatpak.yml
vendored
@ -51,38 +51,76 @@ jobs:
|
||||
- name: Restore Nuget packages
|
||||
# With .NET 8.0.100, Microsoft.NET.ILLink.Tasks isn't restored by default and only seems to appears when publishing.
|
||||
# So we just publish to grab the dependencies
|
||||
run: dotnet publish -c Release -r linux-x64 Ryujinx/${{ env.RYUJINX_PROJECT_FILE }} --self-contained
|
||||
run: |
|
||||
dotnet publish -c Release -r linux-x64 Ryujinx/${{ env.RYUJINX_PROJECT_FILE }} --self-contained
|
||||
dotnet publish -c Release -r linux-arm64 Ryujinx/${{ env.RYUJINX_PROJECT_FILE }} --self-contained
|
||||
|
||||
- name: Generate nuget_sources.json
|
||||
shell: python
|
||||
run: |
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import base64
|
||||
import binascii
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
|
||||
sources = []
|
||||
|
||||
for path in Path(os.environ['NUGET_PACKAGES']).glob('**/*.nupkg.sha512'):
|
||||
name = path.parent.parent.name
|
||||
version = path.parent.name
|
||||
filename = '{}.{}.nupkg'.format(name, version)
|
||||
url = 'https://api.nuget.org/v3-flatcontainer/{}/{}/{}'.format(name, version, filename)
|
||||
|
||||
with path.open() as fp:
|
||||
sha512 = binascii.hexlify(base64.b64decode(fp.read())).decode('ascii')
|
||||
def create_source_from_external(name, version):
|
||||
full_dir_path = Path(os.environ["NUGET_PACKAGES"]).joinpath(name).joinpath(version)
|
||||
os.makedirs(full_dir_path, exist_ok=True)
|
||||
|
||||
sources.append({
|
||||
'type': 'file',
|
||||
'url': url,
|
||||
'sha512': sha512,
|
||||
'dest': os.environ['NUGET_SOURCES_DESTDIR'],
|
||||
'dest-filename': filename,
|
||||
})
|
||||
filename = "{}.{}.nupkg".format(name, version)
|
||||
url = "https://api.nuget.org/v3-flatcontainer/{}/{}/{}".format(
|
||||
name, version, filename
|
||||
)
|
||||
|
||||
with open('flathub/nuget_sources.json', 'w') as fp:
|
||||
json.dump(sources, fp, indent=4)
|
||||
print(f"Processing {url}...")
|
||||
response = urllib.request.urlopen(url)
|
||||
sha512 = hashlib.sha512(response.read()).hexdigest()
|
||||
|
||||
return {
|
||||
"type": "file",
|
||||
"url": url,
|
||||
"sha512": sha512,
|
||||
"dest": os.environ["NUGET_SOURCES_DESTDIR"],
|
||||
"dest-filename": filename,
|
||||
}
|
||||
|
||||
|
||||
has_added_x64_apphost = False
|
||||
|
||||
for path in Path(os.environ["NUGET_PACKAGES"]).glob("**/*.nupkg.sha512"):
|
||||
name = path.parent.parent.name
|
||||
version = path.parent.name
|
||||
filename = "{}.{}.nupkg".format(name, version)
|
||||
url = "https://api.nuget.org/v3-flatcontainer/{}/{}/{}".format(
|
||||
name, version, filename
|
||||
)
|
||||
|
||||
with path.open() as fp:
|
||||
sha512 = binascii.hexlify(base64.b64decode(fp.read())).decode("ascii")
|
||||
|
||||
sources.append(
|
||||
{
|
||||
"type": "file",
|
||||
"url": url,
|
||||
"sha512": sha512,
|
||||
"dest": os.environ["NUGET_SOURCES_DESTDIR"],
|
||||
"dest-filename": filename,
|
||||
}
|
||||
)
|
||||
|
||||
# .NET will not add current installed application host to the list, force inject it here.
|
||||
if not has_added_x64_apphost and name.startswith('microsoft.netcore.app.host'):
|
||||
sources.append(create_source_from_external("microsoft.netcore.app.host.linux-x64", version))
|
||||
has_added_x64_apphost = True
|
||||
|
||||
with open("flathub/nuget_sources.json", "w") as fp:
|
||||
json.dump(sources, fp, indent=4)
|
||||
|
||||
- name: Update flatpak metadata
|
||||
id: metadata
|
||||
|
41
.github/workflows/mako.yml
vendored
Normal file
41
.github/workflows/mako.yml
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
name: Mako
|
||||
on:
|
||||
discussion:
|
||||
types: [created, edited, answered, unanswered, category_changed]
|
||||
discussion_comment:
|
||||
types: [created, edited]
|
||||
gollum:
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
issues:
|
||||
types: [opened, edited, reopened, pinned, milestoned, demilestoned, assigned, unassigned, labeled, unlabeled]
|
||||
pull_request_target:
|
||||
types: [opened, edited, reopened, synchronize, ready_for_review, assigned, unassigned]
|
||||
|
||||
jobs:
|
||||
tasks:
|
||||
name: Run Ryujinx tasks
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
discussions: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
if: github.event_name == 'pull_request_target'
|
||||
with:
|
||||
# Ensure we pin the source origin as pull_request_target run under forks.
|
||||
fetch-depth: 0
|
||||
repository: Ryujinx/Ryujinx
|
||||
ref: master
|
||||
|
||||
- name: Run Mako command
|
||||
uses: Ryujinx/Ryujinx-Mako@master
|
||||
with:
|
||||
command: exec-ryujinx-tasks
|
||||
args: --event-name "${{ github.event_name }}" --event-path "${{ github.event_path }}" -w "${{ github.workspace }}" "${{ github.repository }}" "${{ github.run_id }}"
|
||||
app_id: ${{ secrets.MAKO_APP_ID }}
|
||||
private_key: ${{ secrets.MAKO_PRIVATE_KEY }}
|
||||
installation_id: ${{ secrets.MAKO_INSTALLATION_ID }}
|
10
.github/workflows/nightly_pr_comment.yml
vendored
10
.github/workflows/nightly_pr_comment.yml
vendored
@ -39,24 +39,24 @@ jobs:
|
||||
return core.error(`No artifacts found`);
|
||||
}
|
||||
let body = `Download the artifacts for this pull request:\n`;
|
||||
let hidden_avalonia_artifacts = `\n\n <details><summary>Experimental GUI (Avalonia)</summary>\n`;
|
||||
let hidden_gtk_artifacts = `\n\n <details><summary>Old GUI (GTK3)</summary>\n`;
|
||||
let hidden_headless_artifacts = `\n\n <details><summary>GUI-less (SDL2)</summary>\n`;
|
||||
let hidden_debug_artifacts = `\n\n <details><summary>Only for Developers</summary>\n`;
|
||||
for (const art of artifacts) {
|
||||
if(art.name.includes('Debug')) {
|
||||
hidden_debug_artifacts += `\n* [${art.name}](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
|
||||
} else if(art.name.includes('ava-ryujinx')) {
|
||||
hidden_avalonia_artifacts += `\n* [${art.name}](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
|
||||
} else if(art.name.includes('gtk-ryujinx')) {
|
||||
hidden_gtk_artifacts += `\n* [${art.name}](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
|
||||
} else if(art.name.includes('sdl2-ryujinx-headless')) {
|
||||
hidden_headless_artifacts += `\n* [${art.name}](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
|
||||
} else {
|
||||
body += `\n* [${art.name}](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
|
||||
}
|
||||
}
|
||||
hidden_avalonia_artifacts += `\n</details>`;
|
||||
hidden_gtk_artifacts += `\n</details>`;
|
||||
hidden_headless_artifacts += `\n</details>`;
|
||||
hidden_debug_artifacts += `\n</details>`;
|
||||
body += hidden_avalonia_artifacts;
|
||||
body += hidden_gtk_artifacts;
|
||||
body += hidden_headless_artifacts;
|
||||
body += hidden_debug_artifacts;
|
||||
|
||||
|
19
.github/workflows/pr_triage.yml
vendored
19
.github/workflows/pr_triage.yml
vendored
@ -21,27 +21,8 @@ jobs:
|
||||
repository: Ryujinx/Ryujinx
|
||||
ref: master
|
||||
|
||||
- name: Checkout Ryujinx-Mako
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: Ryujinx/Ryujinx-Mako
|
||||
ref: master
|
||||
path: '.ryujinx-mako'
|
||||
|
||||
- name: Setup Ryujinx-Mako
|
||||
uses: ./.ryujinx-mako/.github/actions/setup-mako
|
||||
|
||||
- name: Update labels based on changes
|
||||
uses: actions/labeler@v5
|
||||
with:
|
||||
sync-labels: true
|
||||
dot: true
|
||||
|
||||
- name: Assign reviewers
|
||||
run: |
|
||||
poetry -n -C .ryujinx-mako run ryujinx-mako update-reviewers ${{ github.repository }} ${{ github.event.pull_request.number }} .github/reviewers.yml
|
||||
shell: bash
|
||||
env:
|
||||
MAKO_APP_ID: ${{ secrets.MAKO_APP_ID }}
|
||||
MAKO_PRIVATE_KEY: ${{ secrets.MAKO_PRIVATE_KEY }}
|
||||
MAKO_INSTALLATION_ID: ${{ secrets.MAKO_INSTALLATION_ID }}
|
||||
|
76
.github/workflows/release.yml
vendored
76
.github/workflows/release.yml
vendored
@ -10,7 +10,7 @@ on:
|
||||
- '*.yml'
|
||||
- '*.json'
|
||||
- '*.config'
|
||||
- 'README.md'
|
||||
- '*.md'
|
||||
|
||||
concurrency: release
|
||||
|
||||
@ -44,30 +44,34 @@ jobs:
|
||||
sha: context.sha
|
||||
})
|
||||
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
name: ${{ 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)."
|
||||
omitBodyDuringUpdate: true
|
||||
owner: ${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}
|
||||
repo: ${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
release:
|
||||
name: Release ${{ matrix.OS_NAME }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: Release for ${{ matrix.platform.name }}
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
timeout-minutes: ${{ fromJSON(vars.JOB_TIMEOUT) }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest ]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
OS_NAME: Linux x64
|
||||
DOTNET_RUNTIME_IDENTIFIER: linux-x64
|
||||
RELEASE_ZIP_OS_NAME: linux_x64
|
||||
|
||||
- os: windows-latest
|
||||
OS_NAME: Windows x64
|
||||
DOTNET_RUNTIME_IDENTIFIER: win-x64
|
||||
RELEASE_ZIP_OS_NAME: win_x64
|
||||
platform:
|
||||
- { name: win-x64, os: windows-latest, zip_os_name: win_x64 }
|
||||
- { name: linux-x64, os: ubuntu-latest, zip_os_name: linux_x64 }
|
||||
- { name: linux-arm64, os: ubuntu-latest, zip_os_name: linux_arm64 }
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
|
||||
- name: Overwrite csc problem matcher
|
||||
run: echo "::add-matcher::.github/csc.json"
|
||||
|
||||
@ -85,6 +89,7 @@ jobs:
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/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
|
||||
sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
shell: bash
|
||||
|
||||
- name: Create output dir
|
||||
@ -92,42 +97,36 @@ jobs:
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
dotnet publish -c Release -r "${{ matrix.DOTNET_RUNTIME_IDENTIFIER }}" -o ./publish_gtk/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx --self-contained true
|
||||
dotnet publish -c Release -r "${{ matrix.DOTNET_RUNTIME_IDENTIFIER }}" -o ./publish_sdl2_headless/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx.Headless.SDL2 --self-contained true
|
||||
dotnet publish -c Release -r "${{ matrix.DOTNET_RUNTIME_IDENTIFIER }}" -o ./publish_ava/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx.Ava --self-contained true
|
||||
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_ava/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx --self-contained true
|
||||
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_sdl2_headless/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx.Headless.SDL2 --self-contained true
|
||||
|
||||
- name: Packing Windows builds
|
||||
if: matrix.os == 'windows-latest'
|
||||
if: matrix.platform.os == 'windows-latest'
|
||||
run: |
|
||||
pushd publish_gtk
|
||||
7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-win_x64.zip publish
|
||||
pushd publish_ava
|
||||
cp publish/Ryujinx.exe publish/Ryujinx.Ava.exe
|
||||
7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip publish
|
||||
7z a ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip publish
|
||||
popd
|
||||
|
||||
pushd publish_sdl2_headless
|
||||
7z a ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-win_x64.zip publish
|
||||
popd
|
||||
|
||||
pushd publish_ava
|
||||
7z a ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-win_x64.zip publish
|
||||
7z a ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip publish
|
||||
popd
|
||||
shell: bash
|
||||
|
||||
- name: Packing Linux builds
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
if: matrix.platform.os == 'ubuntu-latest'
|
||||
run: |
|
||||
pushd publish_gtk
|
||||
chmod +x publish/Ryujinx.sh publish/Ryujinx
|
||||
tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar.gz publish
|
||||
pushd publish_ava
|
||||
cp publish/Ryujinx publish/Ryujinx.Ava
|
||||
chmod +x publish/Ryujinx.sh publish/Ryujinx.Ava publish/Ryujinx
|
||||
tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz publish
|
||||
tar -czvf ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz publish
|
||||
popd
|
||||
|
||||
pushd publish_sdl2_headless
|
||||
chmod +x publish/Ryujinx.sh publish/Ryujinx.Headless.SDL2
|
||||
tar -czvf ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-linux_x64.tar.gz publish
|
||||
popd
|
||||
|
||||
pushd publish_ava
|
||||
chmod +x publish/Ryujinx.sh publish/Ryujinx.Ava
|
||||
tar -czvf ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar.gz publish
|
||||
tar -czvf ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz publish
|
||||
popd
|
||||
shell: bash
|
||||
|
||||
@ -186,12 +185,13 @@ jobs:
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/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
|
||||
sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
shell: bash
|
||||
|
||||
- name: Publish macOS Ryujinx.Ava
|
||||
- name: Publish macOS Ryujinx
|
||||
run: |
|
||||
./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
|
||||
|
@ -3,52 +3,50 @@
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Avalonia" Version="11.0.5" />
|
||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.0.5" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.0.5" />
|
||||
<PackageVersion Include="Avalonia.Diagnostics" Version="11.0.5" />
|
||||
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.0.5" />
|
||||
<PackageVersion Include="Avalonia.Svg" Version="11.0.0.3" />
|
||||
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0.3" />
|
||||
<PackageVersion Include="Avalonia" Version="11.0.10" />
|
||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.0.10" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.0.10" />
|
||||
<PackageVersion Include="Avalonia.Diagnostics" Version="11.0.10" />
|
||||
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.0.10" />
|
||||
<PackageVersion Include="Avalonia.Svg" Version="11.0.0.16" />
|
||||
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0.16" />
|
||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageVersion Include="Concentus" Version="1.1.7" />
|
||||
<PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
<PackageVersion Include="DynamicData" Version="7.14.2" />
|
||||
<PackageVersion Include="FluentAvaloniaUI" Version="2.0.4" />
|
||||
<PackageVersion Include="DynamicData" Version="8.4.1" />
|
||||
<PackageVersion Include="FluentAvaloniaUI" Version="2.0.5" />
|
||||
<PackageVersion Include="GtkSharp.Dependencies" Version="1.1.1" />
|
||||
<PackageVersion Include="GtkSharp.Dependencies.osx" Version="0.0.5" />
|
||||
<PackageVersion Include="jp2masa.Avalonia.Flexbox" Version="0.3.0-beta.4" />
|
||||
<PackageVersion Include="LibHac" Version="0.19.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
|
||||
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.2.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
|
||||
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.5.1" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
|
||||
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
|
||||
<PackageVersion Include="NetCoreServer" Version="7.0.0" />
|
||||
<PackageVersion Include="NetCoreServer" Version="8.0.7" />
|
||||
<PackageVersion Include="NUnit" Version="3.13.3" />
|
||||
<PackageVersion Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageVersion Include="OpenTK.Core" Version="4.8.1" />
|
||||
<PackageVersion Include="OpenTK.Graphics" Version="4.8.1" />
|
||||
<PackageVersion Include="OpenTK.Audio.OpenAL" Version="4.8.1" />
|
||||
<PackageVersion Include="OpenTK.Windowing.GraphicsLibraryFramework" Version="4.8.1" />
|
||||
<PackageVersion Include="OpenTK.Core" Version="4.8.2" />
|
||||
<PackageVersion Include="OpenTK.Graphics" Version="4.8.2" />
|
||||
<PackageVersion Include="OpenTK.Audio.OpenAL" Version="4.8.2" />
|
||||
<PackageVersion Include="OpenTK.Windowing.GraphicsLibraryFramework" Version="4.8.2" />
|
||||
<PackageVersion Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" />
|
||||
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies" Version="5.0.1-build13" />
|
||||
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies" Version="5.0.3-build14" />
|
||||
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
|
||||
<PackageVersion Include="Ryujinx.GtkSharp" Version="3.24.24.59-ryujinx" />
|
||||
<PackageVersion Include="Ryujinx.SDL2-CS" Version="2.28.1-build28" />
|
||||
<PackageVersion Include="Ryujinx.SDL2-CS" Version="2.30.0-build32" />
|
||||
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
|
||||
<PackageVersion Include="shaderc.net" Version="0.1.0" />
|
||||
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan" Version="2.16.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.16.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.16.0" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="1.0.4" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta11" />
|
||||
<PackageVersion Include="SPB" Version="0.0.4-build28" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="8.0.0" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="2.1.8" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp.Drawing" Version="1.0.0" />
|
||||
<PackageVersion Include="SPB" Version="0.0.4-build32" />
|
||||
<PackageVersion Include="System.IO.Hashing" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Management" Version="8.0.0" />
|
||||
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
93
README.md
93
README.md
@ -1,21 +1,21 @@
|
||||
|
||||
<h1 align="center">
|
||||
<br>
|
||||
<a href="https://ryujinx.org/"><img src="https://i.imgur.com/WcCj6Rt.png" alt="Ryujinx" width="150"></a>
|
||||
<a href="https://ryujinx.org/"><img src="https://raw.githubusercontent.com/Ryujinx/Ryujinx/master/distribution/misc/Logo.svg" alt="Ryujinx" width="150"></a>
|
||||
<br>
|
||||
<b>Ryujinx</b>
|
||||
<br>
|
||||
<sub><sup><b>(REE-YOU-JINX)</b></sup></sub>
|
||||
<br>
|
||||
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
Ryujinx is an open-source Nintendo Switch emulator, created by gdkchan, written in C#.
|
||||
This emulator aims at providing excellent accuracy and performance, a user-friendly interface and consistent builds.
|
||||
It was written from scratch and development on the project began in September 2017. Ryujinx is available on Github under the <a href="https://github.com/Ryujinx/Ryujinx/blob/master/LICENSE.txt" target="_blank">MIT license</a>. <br />
|
||||
|
||||
Ryujinx is an open-source Nintendo Switch emulator, created by gdkchan, written in C#.
|
||||
This emulator aims at providing excellent accuracy and performance, a user-friendly interface and consistent builds.
|
||||
It was written from scratch and development on the project began in September 2017.
|
||||
Ryujinx is available on Github under the <a href="https://github.com/Ryujinx/Ryujinx/blob/master/LICENSE.txt" target="_blank">MIT license</a>.
|
||||
<br />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Ryujinx/Ryujinx/actions/workflows/release.yml">
|
||||
<img src="https://github.com/Ryujinx/Ryujinx/actions/workflows/release.yml/badge.svg"
|
||||
@ -34,87 +34,111 @@
|
||||
<img src="https://raw.githubusercontent.com/Ryujinx/Ryujinx-Website/master/public/assets/images/shell.png">
|
||||
</p>
|
||||
|
||||
<h5 align="center">
|
||||
|
||||
</h5>
|
||||
|
||||
## Compatibility
|
||||
|
||||
As of April 2023, Ryujinx has been tested on approximately 4,050 titles; over 4,000 boot past menus and into gameplay, with roughly 3,400 of those being considered playable.
|
||||
You can check out the compatibility list [here](https://github.com/Ryujinx/Ryujinx-Games-List/issues). Anyone is free to submit a new game test or update an existing game test entry; simply follow the new issue template and testing guidelines, or post as a reply to the applicable game issue. Use the search function to see if a game has been tested already!
|
||||
As of October 2023, Ryujinx has been tested on approximately 4,200 titles;
|
||||
over 4,150 boot past menus and into gameplay, with roughly 3,500 of those being considered playable.
|
||||
|
||||
You can check out the compatibility list [here](https://github.com/Ryujinx/Ryujinx-Games-List/issues).
|
||||
|
||||
Anyone is free to submit a new game test or update an existing game test entry;
|
||||
simply follow the new issue template and testing guidelines, or post as a reply to the applicable game issue.
|
||||
Use the search function to see if a game has been tested already!
|
||||
|
||||
## Usage
|
||||
|
||||
To run this emulator, your PC must be equipped with at least 8GiB of RAM; failing to meet this requirement may result in a poor gameplay experience or unexpected crashes.
|
||||
To run this emulator, your PC must be equipped with at least 8GiB of RAM;
|
||||
failing to meet this requirement may result in a poor gameplay experience or unexpected crashes.
|
||||
|
||||
See our [Setup & Configuration Guide](https://github.com/Ryujinx/Ryujinx/wiki/Ryujinx-Setup-&-Configuration-Guide) on how to set up the emulator.
|
||||
|
||||
For our Local Wireless and LAN builds, see our [Multiplayer: Local Play/Local Wireless Guide
|
||||
For our Local Wireless (LDN) builds, see our [Multiplayer: Local Play/Local Wireless Guide
|
||||
](https://github.com/Ryujinx/Ryujinx/wiki/Multiplayer-(LDN-Local-Wireless)-Guide).
|
||||
|
||||
Avalonia UI comes with translations for various languages. See [Crowdin](https://crwd.in/ryujinx) for more information.
|
||||
|
||||
## Latest build
|
||||
|
||||
These builds are compiled automatically for each commit on the master branch. While we strive to ensure optimal stability and performance prior to pushing an update, our automated builds **may be unstable or completely broken.**
|
||||
These builds are compiled automatically for each commit on the master branch.
|
||||
While we strive to ensure optimal stability and performance prior to pushing an update, our automated builds **may be unstable or completely broken**.
|
||||
|
||||
If you want to see details on updates to the emulator, you can visit our [Changelog](https://github.com/Ryujinx/Ryujinx/wiki/Changelog).
|
||||
|
||||
The latest automatic build for Windows, macOS, and Linux can be found on the [Official Website](https://ryujinx.org/download).
|
||||
|
||||
## Documentation
|
||||
|
||||
If you are planning to contribute or just want to learn more about this project please read through our [documentation](docs/README.md).
|
||||
|
||||
## Building
|
||||
|
||||
If you wish to build the emulator yourself, follow these steps:
|
||||
|
||||
### Step 1
|
||||
Install the X64 version of [.NET 8.0 (or higher) SDK](https://dotnet.microsoft.com/download/dotnet/8.0).
|
||||
|
||||
Install the [.NET 8.0 (or higher) SDK](https://dotnet.microsoft.com/download/dotnet/8.0).
|
||||
Make sure your SDK version is higher or equal to the required version specified in [global.json](global.json).
|
||||
|
||||
### Step 2
|
||||
|
||||
Either use `git clone https://github.com/Ryujinx/Ryujinx` on the command line to clone the repository or use Code --> Download zip button to get the files.
|
||||
|
||||
### Step 3
|
||||
|
||||
To build Ryujinx, open a command prompt inside the project directory. You can quickly access it on Windows by holding shift in File Explorer, then right clicking and selecting `Open command window here`. Then type the following command:
|
||||
`dotnet build -c Release -o build`
|
||||
To build Ryujinx, open a command prompt inside the project directory.
|
||||
You can quickly access it on Windows by holding shift in File Explorer, then right clicking and selecting `Open command window here`.
|
||||
Then type the following command: `dotnet build -c Release -o build`
|
||||
the built files will be found in the newly created build directory.
|
||||
|
||||
Ryujinx system files are stored in the `Ryujinx` folder. This folder is located in the user folder, which can be accessed by clicking `Open Ryujinx Folder` under the File menu in the GUI.
|
||||
|
||||
Ryujinx system files are stored in the `Ryujinx` folder.
|
||||
This folder is located in the user folder, which can be accessed by clicking `Open Ryujinx Folder` under the File menu in the GUI.
|
||||
|
||||
## Features
|
||||
|
||||
- **Audio**
|
||||
- **Audio**
|
||||
|
||||
Audio output is entirely supported, audio input (microphone) isn't supported. We use C# wrappers for [OpenAL](https://openal-soft.org/), and [SDL2](https://www.libsdl.org/) & [libsoundio](http://libsound.io/) as fallbacks.
|
||||
Audio output is entirely supported, audio input (microphone) isn't supported.
|
||||
We use C# wrappers for [OpenAL](https://openal-soft.org/), and [SDL2](https://www.libsdl.org/) & [libsoundio](http://libsound.io/) as fallbacks.
|
||||
|
||||
- **CPU**
|
||||
|
||||
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, including partial 32-bit support. It translates the ARM code to a custom IR, performs a few optimizations, and turns that into x86 code.
|
||||
There are three memory manager options available depending on the user's preference, leveraging both software-based (slower) and host-mapped modes (much faster). The fastest option (host, unchecked) is set by default.
|
||||
Ryujinx also features an optional Profiled Persistent Translation Cache, which essentially caches translated functions so that they do not need to be translated every time the game loads. The net result is a significant reduction in load times (the amount of time between launching a game and arriving at the title screen) for nearly every game. NOTE: this feature is enabled by default in the Options menu > System tab. You must launch the game at least twice to the title screen or beyond before performance improvements are unlocked on the third launch! These improvements are permanent and do not require any extra launches going forward.
|
||||
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, including partial 32-bit support.
|
||||
It translates the ARM code to a custom IR, performs a few optimizations, and turns that into x86 code.
|
||||
There are three memory manager options available depending on the user's preference, leveraging both software-based (slower) and host-mapped modes (much faster).
|
||||
The fastest option (host, unchecked) is set by default.
|
||||
Ryujinx also features an optional Profiled Persistent Translation Cache, which essentially caches translated functions so that they do not need to be translated every time the game loads.
|
||||
The net result is a significant reduction in load times (the amount of time between launching a game and arriving at the title screen) for nearly every game.
|
||||
NOTE: This feature is enabled by default in the Options menu > System tab.
|
||||
You must launch the game at least twice to the title screen or beyond before performance improvements are unlocked on the third launch!
|
||||
These improvements are permanent and do not require any extra launches going forward.
|
||||
|
||||
- **GPU**
|
||||
|
||||
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. There are currently six graphics enhancements available to the end user in Ryujinx: Disk Shader Caching, Resolution Scaling, Anti-Aliasing, Scaling Filters (including FSR), Anisotropic Filtering and Aspect Ratio Adjustment. These enhancements can be adjusted or toggled as desired in the GUI.
|
||||
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.
|
||||
There are currently six graphics enhancements available to the end user in Ryujinx: Disk Shader Caching, Resolution Scaling, Anti-Aliasing, Scaling Filters (including FSR), Anisotropic Filtering and Aspect Ratio Adjustment.
|
||||
These enhancements can be adjusted or toggled as desired in the GUI.
|
||||
|
||||
- **Input**
|
||||
|
||||
We currently have support for keyboard, mouse, touch input, JoyCon input support, and nearly all controllers. Motion controls are natively supported in most cases; for dual-JoyCon motion support, DS4Windows or BetterJoy are currently required.
|
||||
In all scenarios, you can set up everything inside the input configuration menu.
|
||||
We currently have support for keyboard, mouse, touch input, JoyCon input support, and nearly all controllers.
|
||||
Motion controls are natively supported in most cases; for dual-JoyCon motion support, DS4Windows or BetterJoy are currently required.
|
||||
In all scenarios, you can set up everything inside the input configuration menu.
|
||||
|
||||
- **DLC & Modifications**
|
||||
|
||||
Ryujinx is able to manage add-on content/downloadable content through the GUI. Mods (romfs, exefs, and runtime mods such as cheats) are also supported; the GUI contains a shortcut to open the respective mods folder for a particular game.
|
||||
Ryujinx is able to manage add-on content/downloadable content through the GUI.
|
||||
Mods (romfs, exefs, and runtime mods such as cheats) are also supported;
|
||||
the GUI contains a shortcut to open the respective mods folder for a particular game.
|
||||
|
||||
- **Configuration**
|
||||
|
||||
The emulator has settings for enabling or disabling some logging, remapping controllers, and more. You can configure all of them through the graphical interface or manually through the config file, `Config.json`, found in the user folder which can be accessed by clicking `Open Ryujinx Folder` under the File menu in the GUI.
|
||||
|
||||
The emulator has settings for enabling or disabling some logging, remapping controllers, and more.
|
||||
You can configure all of them through the graphical interface or manually through the config file, `Config.json`, found in the user folder which can be accessed by clicking `Open Ryujinx Folder` under the File menu in the GUI.
|
||||
|
||||
## Contact
|
||||
|
||||
If you have contributions, suggestions, need emulator support or just want to get in touch with the team, join our [Discord server](https://discord.com/invite/Ryujinx). You may also review our [FAQ](https://github.com/Ryujinx/Ryujinx/wiki/Frequently-Asked-Questions).
|
||||
If you have contributions, suggestions, need emulator support or just want to get in touch with the team, join our [Discord server](https://discord.com/invite/Ryujinx).
|
||||
You may also review our [FAQ](https://github.com/Ryujinx/Ryujinx/wiki/Frequently-Asked-Questions).
|
||||
|
||||
## Donations
|
||||
|
||||
@ -134,9 +158,10 @@ All funds received through Patreon are considered a donation to support the proj
|
||||
|
||||
## License
|
||||
|
||||
This software is licensed under the terms of the <a href="https://github.com/Ryujinx/Ryujinx/blob/master/LICENSE.txt" target="_blank">MIT license.</a></i><br />
|
||||
This software is licensed under the terms of the [MIT license](LICENSE.txt).
|
||||
This project makes use of code authored by the libvpx project, licensed under BSD and the ffmpeg project, licensed under LGPLv3.
|
||||
See [LICENSE.txt](LICENSE.txt) and [THIRDPARTY.md](distribution/legal/THIRDPARTY.md) for more details.
|
||||
|
||||
## Credits
|
||||
|
||||
- [LibHac](https://github.com/Thealexbarney/LibHac) is used for our file-system.
|
||||
|
@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.1.32228.430
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx", "src\Ryujinx\Ryujinx.csproj", "{074045D4-3ED2-4711-9169-E385F2BFB5A0}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Gtk3", "src\Ryujinx.Gtk3\Ryujinx.Gtk3.csproj", "{074045D4-3ED2-4711-9169-E385F2BFB5A0}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Tests", "src\Ryujinx.Tests\Ryujinx.Tests.csproj", "{EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}"
|
||||
EndProject
|
||||
@ -69,9 +69,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Headless.SDL2", "sr
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Nvdec.FFmpeg", "src\Ryujinx.Graphics.Nvdec.FFmpeg\Ryujinx.Graphics.Nvdec.FFmpeg.csproj", "{BEE1C184-C9A4-410B-8DFC-FB74D5C93AEB}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Ava", "src\Ryujinx.Ava\Ryujinx.Ava.csproj", "{7C1B2721-13DA-4B62-B046-C626605ECCE6}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx", "src\Ryujinx\Ryujinx.csproj", "{7C1B2721-13DA-4B62-B046-C626605ECCE6}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Ui.Common", "src\Ryujinx.Ui.Common\Ryujinx.Ui.Common.csproj", "{BA161CA0-CD65-4E6E-B644-51C8D1E542DC}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.UI.Common", "src\Ryujinx.UI.Common\Ryujinx.UI.Common.csproj", "{BA161CA0-CD65-4E6E-B644-51C8D1E542DC}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Horizon.Generators", "src\Ryujinx.Horizon.Generators\Ryujinx.Horizon.Generators.csproj", "{6AE2A5E8-4C5A-48B9-997B-E1455C0355C6}"
|
||||
EndProject
|
||||
@ -79,7 +79,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Vulkan", "
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spv.Generator", "src\Spv.Generator\Spv.Generator.csproj", "{2BCB3D7A-38C0-4FE7-8FDA-374C6AD56D0E}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Ui.LocaleGenerator", "src\Ryujinx.Ui.LocaleGenerator\Ryujinx.Ui.LocaleGenerator.csproj", "{77D01AD9-2C98-478E-AE1D-8F7100738FB4}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.UI.LocaleGenerator", "src\Ryujinx.UI.LocaleGenerator\Ryujinx.UI.LocaleGenerator.csproj", "{77D01AD9-2C98-478E-AE1D-8F7100738FB4}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Horizon.Common", "src\Ryujinx.Horizon.Common\Ryujinx.Horizon.Common.csproj", "{77F96ECE-4952-42DB-A528-DED25572A573}"
|
||||
EndProject
|
||||
|
@ -4,6 +4,8 @@
|
||||
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForOtherTypes/@EntryValue">UseExplicitType</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForSimpleTypes/@EntryValue">UseExplicitType</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypesAndNamespaces/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="I" Suffix="" Style="AaBb" /></Policy></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=a0b4bc4d_002Dd13b_002D4a37_002Db37e_002Dc9c6864e4302/@EntryIndexedValue"><Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="I" Suffix="" Style="AaBb" /></Policy></Policy></s:String>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=ASET/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Astc/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Luma/@EntryIndexedValue">True</s:Boolean>
|
||||
|
@ -4,7 +4,7 @@ Name=Ryujinx
|
||||
Type=Application
|
||||
Icon=Ryujinx
|
||||
Exec=Ryujinx.sh %f
|
||||
Comment=Plays Nintendo Switch applications
|
||||
Comment=A Nintendo Switch Emulator
|
||||
GenericName=Nintendo Switch Emulator
|
||||
Terminal=false
|
||||
Categories=Game;Emulator;
|
||||
|
15
distribution/linux/Ryujinx.sh
Normal file → Executable file
15
distribution/linux/Ryujinx.sh
Normal file → Executable file
@ -1,20 +1,23 @@
|
||||
#!/bin/sh
|
||||
|
||||
SCRIPT_DIR=$(dirname "$(realpath "$0")")
|
||||
RYUJINX_BIN="Ryujinx"
|
||||
|
||||
if [ -f "$SCRIPT_DIR/Ryujinx.Ava" ]; then
|
||||
RYUJINX_BIN="Ryujinx.Ava"
|
||||
fi
|
||||
|
||||
if [ -f "$SCRIPT_DIR/Ryujinx.Headless.SDL2" ]; then
|
||||
RYUJINX_BIN="Ryujinx.Headless.SDL2"
|
||||
fi
|
||||
|
||||
if [ -f "$SCRIPT_DIR/Ryujinx" ]; then
|
||||
RYUJINX_BIN="Ryujinx"
|
||||
fi
|
||||
|
||||
if [ -z "$RYUJINX_BIN" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
COMMAND="env DOTNET_EnableAlternateStackCheck=1"
|
||||
|
||||
if command -v gamemoderun > /dev/null 2>&1; then
|
||||
COMMAND="$COMMAND gamemoderun"
|
||||
fi
|
||||
|
||||
$COMMAND "$SCRIPT_DIR/$RYUJINX_BIN" "$@"
|
||||
exec $COMMAND "$SCRIPT_DIR/$RYUJINX_BIN" "$@"
|
||||
|
@ -14,8 +14,8 @@ mkdir "$APP_BUNDLE_DIRECTORY/Contents/Frameworks"
|
||||
mkdir "$APP_BUNDLE_DIRECTORY/Contents/MacOS"
|
||||
mkdir "$APP_BUNDLE_DIRECTORY/Contents/Resources"
|
||||
|
||||
# Copy executables first
|
||||
cp "$PUBLISH_DIRECTORY/Ryujinx.Ava" "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx"
|
||||
# Copy executable and nsure executable can be executed
|
||||
cp "$PUBLISH_DIRECTORY/Ryujinx" "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx"
|
||||
chmod u+x "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx"
|
||||
|
||||
# Then all libraries
|
||||
|
@ -22,9 +22,9 @@ EXTRA_ARGS=$8
|
||||
|
||||
if [ "$VERSION" == "1.1.0" ];
|
||||
then
|
||||
RELEASE_TAR_FILE_NAME=test-ava-ryujinx-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.app.tar
|
||||
RELEASE_TAR_FILE_NAME=ryujinx-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.app.tar
|
||||
else
|
||||
RELEASE_TAR_FILE_NAME=test-ava-ryujinx-$VERSION-macos_universal.app.tar
|
||||
RELEASE_TAR_FILE_NAME=ryujinx-$VERSION-macos_universal.app.tar
|
||||
fi
|
||||
|
||||
ARM64_APP_BUNDLE="$TEMP_DIRECTORY/output_arm64/Ryujinx.app"
|
||||
@ -38,9 +38,9 @@ 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.Ava
|
||||
dotnet publish -c "$CONFIGURATION" -r osx-arm64 -o "$TEMP_DIRECTORY/publish_arm64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx.Ava
|
||||
dotnet publish -c "$CONFIGURATION" -r osx-x64 -o "$TEMP_DIRECTORY/publish_x64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx.Ava
|
||||
dotnet build -c "$CONFIGURATION" src/Ryujinx
|
||||
dotnet publish -c "$CONFIGURATION" -r osx-arm64 -o "$TEMP_DIRECTORY/publish_arm64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx
|
||||
dotnet publish -c "$CONFIGURATION" -r osx-x64 -o "$TEMP_DIRECTORY/publish_x64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx
|
||||
|
||||
# Get rid of the support library for ARMeilleure for x64 (that's only for arm64)
|
||||
rm -rf "$TEMP_DIRECTORY/publish_x64/libarmeilleure-jitsupport.dylib"
|
||||
@ -108,6 +108,13 @@ tar --exclude "Ryujinx.app/Contents/MacOS/Ryujinx" -cvf "$RELEASE_TAR_FILE_NAME"
|
||||
python3 "$BASE_DIR/distribution/misc/add_tar_exec.py" "$RELEASE_TAR_FILE_NAME" "Ryujinx.app/Contents/MacOS/Ryujinx" "Ryujinx.app/Contents/MacOS/Ryujinx"
|
||||
gzip -9 < "$RELEASE_TAR_FILE_NAME" > "$RELEASE_TAR_FILE_NAME.gz"
|
||||
rm "$RELEASE_TAR_FILE_NAME"
|
||||
|
||||
# Create legacy update package for Avalonia to not left behind old testers.
|
||||
if [ "$VERSION" != "1.1.0" ];
|
||||
then
|
||||
cp $RELEASE_TAR_FILE_NAME.gz test-ava-ryujinx-$VERSION-macos_universal.app.tar.gz
|
||||
fi
|
||||
|
||||
popd
|
||||
|
||||
echo "Done"
|
8
distribution/macos/shortcut-launch-script.sh
Normal file
8
distribution/macos/shortcut-launch-script.sh
Normal file
@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
launch_arch="$(uname -m)"
|
||||
if [ "$(sysctl -in sysctl.proc_translated)" = "1" ]
|
||||
then
|
||||
launch_arch="arm64"
|
||||
fi
|
||||
|
||||
arch -$launch_arch {0} {1}
|
@ -33,8 +33,3 @@ 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
|
||||
|
@ -9,7 +9,7 @@ namespace ARMeilleure.Common
|
||||
/// Represents a table of guest address to a value.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntry">Type of the value</typeparam>
|
||||
unsafe class AddressTable<TEntry> : IDisposable where TEntry : unmanaged
|
||||
public unsafe class AddressTable<TEntry> : IDisposable where TEntry : unmanaged
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a level in an <see cref="AddressTable{TEntry}"/>.
|
||||
|
@ -517,7 +517,10 @@ namespace ARMeilleure.Decoders
|
||||
SetA64("0x00111100>>>xxx100111xxxxxxxxxx", InstName.Sqrshrn_V, InstEmit.Sqrshrn_V, OpCodeSimdShImm.Create);
|
||||
SetA64("0111111100>>>xxx100011xxxxxxxxxx", InstName.Sqrshrun_S, InstEmit.Sqrshrun_S, OpCodeSimdShImm.Create);
|
||||
SetA64("0x10111100>>>xxx100011xxxxxxxxxx", InstName.Sqrshrun_V, InstEmit.Sqrshrun_V, OpCodeSimdShImm.Create);
|
||||
SetA64("010111110>>>>xxx011101xxxxxxxxxx", InstName.Sqshl_Si, InstEmit.Sqshl_Si, OpCodeSimdShImm.Create);
|
||||
SetA64("0>001110<<1xxxxx010011xxxxxxxxxx", InstName.Sqshl_V, InstEmit.Sqshl_V, OpCodeSimdReg.Create);
|
||||
SetA64("0000111100>>>xxx011101xxxxxxxxxx", InstName.Sqshl_Vi, InstEmit.Sqshl_Vi, OpCodeSimdShImm.Create);
|
||||
SetA64("010011110>>>>xxx011101xxxxxxxxxx", InstName.Sqshl_Vi, InstEmit.Sqshl_Vi, OpCodeSimdShImm.Create);
|
||||
SetA64("0101111100>>>xxx100101xxxxxxxxxx", InstName.Sqshrn_S, InstEmit.Sqshrn_S, OpCodeSimdShImm.Create);
|
||||
SetA64("0x00111100>>>xxx100101xxxxxxxxxx", InstName.Sqshrn_V, InstEmit.Sqshrn_V, OpCodeSimdShImm.Create);
|
||||
SetA64("0111111100>>>xxx100001xxxxxxxxxx", InstName.Sqshrun_S, InstEmit.Sqshrun_S, OpCodeSimdShImm.Create);
|
||||
@ -872,6 +875,7 @@ namespace ARMeilleure.Decoders
|
||||
SetVfp("<<<<11100x10xxxxxxxx101xx1x0xxxx", InstName.Vnmul, InstEmit32.Vnmul_S, OpCode32SimdRegS.Create, OpCode32SimdRegS.CreateT32);
|
||||
SetVfp("111111101x1110xxxxxx101x01x0xxxx", InstName.Vrint, InstEmit32.Vrint_RM, OpCode32SimdS.Create, OpCode32SimdS.CreateT32);
|
||||
SetVfp("<<<<11101x110110xxxx101x11x0xxxx", InstName.Vrint, InstEmit32.Vrint_Z, OpCode32SimdS.Create, OpCode32SimdS.CreateT32);
|
||||
SetVfp("<<<<11101x110110xxxx101x01x0xxxx", InstName.Vrintr, InstEmit32.Vrintr_S, OpCode32SimdS.Create, OpCode32SimdS.CreateT32);
|
||||
SetVfp("<<<<11101x110111xxxx101x01x0xxxx", InstName.Vrintx, InstEmit32.Vrintx_S, OpCode32SimdS.Create, OpCode32SimdS.CreateT32);
|
||||
SetVfp("<<<<11101x110001xxxx101x11x0xxxx", InstName.Vsqrt, InstEmit32.Vsqrt_S, OpCode32SimdS.Create, OpCode32SimdS.CreateT32);
|
||||
SetVfp("111111100xxxxxxxxxxx101xx0x0xxxx", InstName.Vsel, InstEmit32.Vsel, OpCode32SimdSel.Create, OpCode32SimdSel.CreateT32);
|
||||
@ -992,6 +996,7 @@ namespace ARMeilleure.Decoders
|
||||
SetAsimd("1111001x1x000xxxxxxx<<x10x01xxxx", InstName.Vorr, InstEmit32.Vorr_II, OpCode32SimdImm.Create, OpCode32SimdImm.CreateT32);
|
||||
SetAsimd("111100100x<<xxxxxxxx1011x0x1xxxx", InstName.Vpadd, InstEmit32.Vpadd_I, OpCode32SimdReg.Create, OpCode32SimdReg.CreateT32);
|
||||
SetAsimd("111100110x00xxxxxxxx1101x0x0xxxx", InstName.Vpadd, InstEmit32.Vpadd_V, OpCode32SimdReg.Create, OpCode32SimdReg.CreateT32);
|
||||
SetAsimd("111100111x11<<00xxxx0110xxx0xxxx", InstName.Vpadal, InstEmit32.Vpadal, OpCode32SimdCmpZ.Create, OpCode32SimdCmpZ.CreateT32);
|
||||
SetAsimd("111100111x11<<00xxxx0010xxx0xxxx", InstName.Vpaddl, InstEmit32.Vpaddl, OpCode32SimdCmpZ.Create, OpCode32SimdCmpZ.CreateT32);
|
||||
SetAsimd("1111001x0x<<xxxxxxxx1010x0x0xxxx", InstName.Vpmax, InstEmit32.Vpmax_I, OpCode32SimdReg.Create, OpCode32SimdReg.CreateT32);
|
||||
SetAsimd("111100110x00xxxxxxxx1111x0x0xxxx", InstName.Vpmax, InstEmit32.Vpmax_V, OpCode32SimdReg.Create, OpCode32SimdReg.CreateT32);
|
||||
|
@ -19,6 +19,12 @@ namespace ARMeilleure.Instructions
|
||||
Operand n = GetAluN(context);
|
||||
Operand m = GetAluM(context, setCarry: false);
|
||||
|
||||
if (op.Rn == RegisterAlias.Aarch32Pc && op is OpCodeT32AluImm12)
|
||||
{
|
||||
// For ADR, PC is always 4 bytes aligned, even in Thumb mode.
|
||||
n = context.BitwiseAnd(n, Const(~3u));
|
||||
}
|
||||
|
||||
Operand res = context.Add(n, m);
|
||||
|
||||
if (ShouldSetFlags(context))
|
||||
@ -467,6 +473,12 @@ namespace ARMeilleure.Instructions
|
||||
Operand n = GetAluN(context);
|
||||
Operand m = GetAluM(context, setCarry: false);
|
||||
|
||||
if (op.Rn == RegisterAlias.Aarch32Pc && op is OpCodeT32AluImm12)
|
||||
{
|
||||
// For ADR, PC is always 4 bytes aligned, even in Thumb mode.
|
||||
n = context.BitwiseAnd(n, Const(~3u));
|
||||
}
|
||||
|
||||
Operand res = context.Subtract(n, m);
|
||||
|
||||
if (ShouldSetFlags(context))
|
||||
|
@ -157,7 +157,7 @@ namespace ARMeilleure.Instructions
|
||||
|
||||
context.Copy(temp, value);
|
||||
|
||||
if (!context.Memory.Type.IsHostMapped())
|
||||
if (!context.Memory.Type.IsHostMappedOrTracked())
|
||||
{
|
||||
context.Branch(lblEnd);
|
||||
|
||||
@ -198,7 +198,7 @@ namespace ARMeilleure.Instructions
|
||||
|
||||
SetInt(context, rt, value);
|
||||
|
||||
if (!context.Memory.Type.IsHostMapped())
|
||||
if (!context.Memory.Type.IsHostMappedOrTracked())
|
||||
{
|
||||
context.Branch(lblEnd);
|
||||
|
||||
@ -265,7 +265,7 @@ namespace ARMeilleure.Instructions
|
||||
|
||||
context.Copy(GetVec(rt), value);
|
||||
|
||||
if (!context.Memory.Type.IsHostMapped())
|
||||
if (!context.Memory.Type.IsHostMappedOrTracked())
|
||||
{
|
||||
context.Branch(lblEnd);
|
||||
|
||||
@ -312,7 +312,7 @@ namespace ARMeilleure.Instructions
|
||||
break;
|
||||
}
|
||||
|
||||
if (!context.Memory.Type.IsHostMapped())
|
||||
if (!context.Memory.Type.IsHostMappedOrTracked())
|
||||
{
|
||||
context.Branch(lblEnd);
|
||||
|
||||
@ -385,7 +385,7 @@ namespace ARMeilleure.Instructions
|
||||
break;
|
||||
}
|
||||
|
||||
if (!context.Memory.Type.IsHostMapped())
|
||||
if (!context.Memory.Type.IsHostMappedOrTracked())
|
||||
{
|
||||
context.Branch(lblEnd);
|
||||
|
||||
@ -403,6 +403,27 @@ namespace ARMeilleure.Instructions
|
||||
{
|
||||
return EmitHostMappedPointer(context, address);
|
||||
}
|
||||
else if (context.Memory.Type.IsHostTracked())
|
||||
{
|
||||
if (address.Type == OperandType.I32)
|
||||
{
|
||||
address = context.ZeroExtend32(OperandType.I64, address);
|
||||
}
|
||||
|
||||
if (context.Memory.Type == MemoryManagerType.HostTracked)
|
||||
{
|
||||
Operand mask = Const(ulong.MaxValue >> (64 - context.Memory.AddressSpaceBits));
|
||||
address = context.BitwiseAnd(address, mask);
|
||||
}
|
||||
|
||||
Operand ptBase = !context.HasPtc
|
||||
? Const(context.Memory.PageTablePointer.ToInt64())
|
||||
: Const(context.Memory.PageTablePointer.ToInt64(), Ptc.PageTableSymbol);
|
||||
|
||||
Operand ptOffset = context.ShiftRightUI(address, Const(PageBits));
|
||||
|
||||
return context.Add(address, context.Load(OperandType.I64, context.Add(ptBase, context.ShiftLeft(ptOffset, Const(3)))));
|
||||
}
|
||||
|
||||
int ptLevelBits = context.Memory.AddressSpaceBits - PageBits;
|
||||
int ptLevelSize = 1 << ptLevelBits;
|
||||
|
@ -2426,7 +2426,11 @@ namespace ARMeilleure.Instructions
|
||||
}
|
||||
else if (Optimizations.FastFP && Optimizations.UseSse41 && sizeF == 0)
|
||||
{
|
||||
Operand res = EmitSse41Round32Exp8OpF(context, context.AddIntrinsic(Intrinsic.X86Rsqrtss, GetVec(op.Rn)), scalar: true);
|
||||
// RSQRTSS handles subnormals as zero, which differs from Arm, so we can't use it here.
|
||||
|
||||
Operand res = context.AddIntrinsic(Intrinsic.X86Sqrtss, GetVec(op.Rn));
|
||||
res = context.AddIntrinsic(Intrinsic.X86Rcpss, res);
|
||||
res = EmitSse41Round32Exp8OpF(context, res, scalar: true);
|
||||
|
||||
context.Copy(GetVec(op.Rd), context.VectorZeroUpper96(res));
|
||||
}
|
||||
@ -2451,7 +2455,11 @@ namespace ARMeilleure.Instructions
|
||||
}
|
||||
else if (Optimizations.FastFP && Optimizations.UseSse41 && sizeF == 0)
|
||||
{
|
||||
Operand res = EmitSse41Round32Exp8OpF(context, context.AddIntrinsic(Intrinsic.X86Rsqrtps, GetVec(op.Rn)), scalar: false);
|
||||
// RSQRTPS handles subnormals as zero, which differs from Arm, so we can't use it here.
|
||||
|
||||
Operand res = context.AddIntrinsic(Intrinsic.X86Sqrtps, GetVec(op.Rn));
|
||||
res = context.AddIntrinsic(Intrinsic.X86Rcpps, res);
|
||||
res = EmitSse41Round32Exp8OpF(context, res, scalar: false);
|
||||
|
||||
if (op.RegisterSize == RegisterSize.Simd64)
|
||||
{
|
||||
|
@ -1115,6 +1115,13 @@ namespace ARMeilleure.Instructions
|
||||
}
|
||||
}
|
||||
|
||||
public static void Vpadal(ArmEmitterContext context)
|
||||
{
|
||||
OpCode32Simd op = (OpCode32Simd)context.CurrOp;
|
||||
|
||||
EmitVectorPairwiseTernaryLongOpI32(context, (op1, op2, op3) => context.Add(context.Add(op1, op2), op3), op.Opc != 1);
|
||||
}
|
||||
|
||||
public static void Vpaddl(ArmEmitterContext context)
|
||||
{
|
||||
OpCode32Simd op = (OpCode32Simd)context.CurrOp;
|
||||
|
@ -578,6 +578,22 @@ namespace ARMeilleure.Instructions
|
||||
}
|
||||
}
|
||||
|
||||
// VRINTR (floating-point).
|
||||
public static void Vrintr_S(ArmEmitterContext context)
|
||||
{
|
||||
if (Optimizations.UseAdvSimd)
|
||||
{
|
||||
InstEmitSimdHelper32Arm64.EmitScalarUnaryOpF32(context, Intrinsic.Arm64FrintiS);
|
||||
}
|
||||
else
|
||||
{
|
||||
EmitScalarUnaryOpF32(context, (op1) =>
|
||||
{
|
||||
return EmitRoundByRMode(context, op1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// VRINTZ (floating-point).
|
||||
public static void Vrint_Z(ArmEmitterContext context)
|
||||
{
|
||||
|
@ -673,6 +673,35 @@ namespace ARMeilleure.Instructions
|
||||
context.Copy(GetVecA32(op.Qd), res);
|
||||
}
|
||||
|
||||
public static void EmitVectorPairwiseTernaryLongOpI32(ArmEmitterContext context, Func3I emit, bool signed)
|
||||
{
|
||||
OpCode32Simd op = (OpCode32Simd)context.CurrOp;
|
||||
|
||||
int elems = op.GetBytesCount() >> op.Size;
|
||||
int pairs = elems >> 1;
|
||||
|
||||
Operand res = GetVecA32(op.Qd);
|
||||
|
||||
for (int index = 0; index < pairs; index++)
|
||||
{
|
||||
int pairIndex = index * 2;
|
||||
Operand m1 = EmitVectorExtract32(context, op.Qm, op.Im + pairIndex, op.Size, signed);
|
||||
Operand m2 = EmitVectorExtract32(context, op.Qm, op.Im + pairIndex + 1, op.Size, signed);
|
||||
|
||||
if (op.Size == 2)
|
||||
{
|
||||
m1 = signed ? context.SignExtend32(OperandType.I64, m1) : context.ZeroExtend32(OperandType.I64, m1);
|
||||
m2 = signed ? context.SignExtend32(OperandType.I64, m2) : context.ZeroExtend32(OperandType.I64, m2);
|
||||
}
|
||||
|
||||
Operand d1 = EmitVectorExtract32(context, op.Qd, op.Id + index, op.Size + 1, signed);
|
||||
|
||||
res = EmitVectorInsert(context, res, emit(m1, m2, d1), op.Id + index, op.Size + 1);
|
||||
}
|
||||
|
||||
context.Copy(GetVecA32(op.Qd), res);
|
||||
}
|
||||
|
||||
// Narrow
|
||||
|
||||
public static void EmitVectorUnaryNarrowOp32(ArmEmitterContext context, Func1I emit, bool signed = false)
|
||||
|
@ -116,7 +116,7 @@ namespace ARMeilleure.Instructions
|
||||
}
|
||||
else if (shift >= eSize)
|
||||
{
|
||||
if ((op.RegisterSize == RegisterSize.Simd64))
|
||||
if (op.RegisterSize == RegisterSize.Simd64)
|
||||
{
|
||||
Operand res = context.VectorZeroUpper64(GetVec(op.Rd));
|
||||
|
||||
@ -359,6 +359,16 @@ namespace ARMeilleure.Instructions
|
||||
}
|
||||
}
|
||||
|
||||
public static void Sqshl_Si(ArmEmitterContext context)
|
||||
{
|
||||
EmitShlImmOp(context, signedDst: true, ShlRegFlags.Signed | ShlRegFlags.Scalar | ShlRegFlags.Saturating);
|
||||
}
|
||||
|
||||
public static void Sqshl_Vi(ArmEmitterContext context)
|
||||
{
|
||||
EmitShlImmOp(context, signedDst: true, ShlRegFlags.Signed | ShlRegFlags.Saturating);
|
||||
}
|
||||
|
||||
public static void Sqshrn_S(ArmEmitterContext context)
|
||||
{
|
||||
if (Optimizations.UseAdvSimd)
|
||||
@ -1593,6 +1603,99 @@ namespace ARMeilleure.Instructions
|
||||
Saturating = 1 << 3,
|
||||
}
|
||||
|
||||
private static void EmitShlImmOp(ArmEmitterContext context, bool signedDst, ShlRegFlags flags = ShlRegFlags.None)
|
||||
{
|
||||
bool scalar = flags.HasFlag(ShlRegFlags.Scalar);
|
||||
bool signed = flags.HasFlag(ShlRegFlags.Signed);
|
||||
bool saturating = flags.HasFlag(ShlRegFlags.Saturating);
|
||||
|
||||
OpCodeSimdShImm op = (OpCodeSimdShImm)context.CurrOp;
|
||||
|
||||
Operand res = context.VectorZero();
|
||||
|
||||
int elems = !scalar ? op.GetBytesCount() >> op.Size : 1;
|
||||
|
||||
for (int index = 0; index < elems; index++)
|
||||
{
|
||||
Operand ne = EmitVectorExtract(context, op.Rn, index, op.Size, signed);
|
||||
|
||||
Operand e = !saturating
|
||||
? EmitShlImm(context, ne, GetImmShl(op), op.Size)
|
||||
: EmitShlImmSatQ(context, ne, GetImmShl(op), op.Size, signed, signedDst);
|
||||
|
||||
res = EmitVectorInsert(context, res, e, index, op.Size);
|
||||
}
|
||||
|
||||
context.Copy(GetVec(op.Rd), res);
|
||||
}
|
||||
|
||||
private static Operand EmitShlImm(ArmEmitterContext context, Operand op, int shiftLsB, int size)
|
||||
{
|
||||
int eSize = 8 << size;
|
||||
|
||||
Debug.Assert(op.Type == OperandType.I64);
|
||||
Debug.Assert(eSize == 8 || eSize == 16 || eSize == 32 || eSize == 64);
|
||||
|
||||
Operand res = context.AllocateLocal(OperandType.I64);
|
||||
|
||||
if (shiftLsB >= eSize)
|
||||
{
|
||||
Operand shl = context.ShiftLeft(op, Const(shiftLsB));
|
||||
context.Copy(res, shl);
|
||||
}
|
||||
else
|
||||
{
|
||||
Operand zeroL = Const(0L);
|
||||
context.Copy(res, zeroL);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private static Operand EmitShlImmSatQ(ArmEmitterContext context, Operand op, int shiftLsB, int size, bool signedSrc, bool signedDst)
|
||||
{
|
||||
int eSize = 8 << size;
|
||||
|
||||
Debug.Assert(op.Type == OperandType.I64);
|
||||
Debug.Assert(eSize == 8 || eSize == 16 || eSize == 32 || eSize == 64);
|
||||
|
||||
Operand lblEnd = Label();
|
||||
|
||||
Operand res = context.Copy(context.AllocateLocal(OperandType.I64), op);
|
||||
|
||||
if (shiftLsB >= eSize)
|
||||
{
|
||||
context.Copy(res, signedSrc
|
||||
? EmitSignedSignSatQ(context, op, size)
|
||||
: EmitUnsignedSignSatQ(context, op, size));
|
||||
}
|
||||
else
|
||||
{
|
||||
Operand shl = context.ShiftLeft(op, Const(shiftLsB));
|
||||
if (eSize == 64)
|
||||
{
|
||||
Operand sarOrShr = signedSrc
|
||||
? context.ShiftRightSI(shl, Const(shiftLsB))
|
||||
: context.ShiftRightUI(shl, Const(shiftLsB));
|
||||
context.Copy(res, shl);
|
||||
context.BranchIf(lblEnd, sarOrShr, op, Comparison.Equal);
|
||||
context.Copy(res, signedSrc
|
||||
? EmitSignedSignSatQ(context, op, size)
|
||||
: EmitUnsignedSignSatQ(context, op, size));
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Copy(res, signedSrc
|
||||
? EmitSignedSrcSatQ(context, shl, size, signedDst)
|
||||
: EmitUnsignedSrcSatQ(context, shl, size, signedDst));
|
||||
}
|
||||
}
|
||||
|
||||
context.MarkLabel(lblEnd);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private static void EmitShlRegOp(ArmEmitterContext context, ShlRegFlags flags = ShlRegFlags.None)
|
||||
{
|
||||
bool scalar = flags.HasFlag(ShlRegFlags.Scalar);
|
||||
|
@ -384,7 +384,9 @@ namespace ARMeilleure.Instructions
|
||||
Sqrshrn_V,
|
||||
Sqrshrun_S,
|
||||
Sqrshrun_V,
|
||||
Sqshl_Si,
|
||||
Sqshl_V,
|
||||
Sqshl_Vi,
|
||||
Sqshrn_S,
|
||||
Sqshrn_V,
|
||||
Sqshrun_S,
|
||||
@ -635,6 +637,7 @@ namespace ARMeilleure.Instructions
|
||||
Vorn,
|
||||
Vorr,
|
||||
Vpadd,
|
||||
Vpadal,
|
||||
Vpaddl,
|
||||
Vpmax,
|
||||
Vpmin,
|
||||
@ -654,6 +657,7 @@ namespace ARMeilleure.Instructions
|
||||
Vrintm,
|
||||
Vrintn,
|
||||
Vrintp,
|
||||
Vrintr,
|
||||
Vrintx,
|
||||
Vrshr,
|
||||
Vrshrn,
|
||||
|
@ -91,54 +91,54 @@ namespace ARMeilleure.Instructions
|
||||
#region "Read"
|
||||
public static byte ReadByte(ulong address)
|
||||
{
|
||||
return GetMemoryManager().ReadTracked<byte>(address);
|
||||
return GetMemoryManager().ReadGuest<byte>(address);
|
||||
}
|
||||
|
||||
public static ushort ReadUInt16(ulong address)
|
||||
{
|
||||
return GetMemoryManager().ReadTracked<ushort>(address);
|
||||
return GetMemoryManager().ReadGuest<ushort>(address);
|
||||
}
|
||||
|
||||
public static uint ReadUInt32(ulong address)
|
||||
{
|
||||
return GetMemoryManager().ReadTracked<uint>(address);
|
||||
return GetMemoryManager().ReadGuest<uint>(address);
|
||||
}
|
||||
|
||||
public static ulong ReadUInt64(ulong address)
|
||||
{
|
||||
return GetMemoryManager().ReadTracked<ulong>(address);
|
||||
return GetMemoryManager().ReadGuest<ulong>(address);
|
||||
}
|
||||
|
||||
public static V128 ReadVector128(ulong address)
|
||||
{
|
||||
return GetMemoryManager().ReadTracked<V128>(address);
|
||||
return GetMemoryManager().ReadGuest<V128>(address);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region "Write"
|
||||
public static void WriteByte(ulong address, byte value)
|
||||
{
|
||||
GetMemoryManager().Write(address, value);
|
||||
GetMemoryManager().WriteGuest(address, value);
|
||||
}
|
||||
|
||||
public static void WriteUInt16(ulong address, ushort value)
|
||||
{
|
||||
GetMemoryManager().Write(address, value);
|
||||
GetMemoryManager().WriteGuest(address, value);
|
||||
}
|
||||
|
||||
public static void WriteUInt32(ulong address, uint value)
|
||||
{
|
||||
GetMemoryManager().Write(address, value);
|
||||
GetMemoryManager().WriteGuest(address, value);
|
||||
}
|
||||
|
||||
public static void WriteUInt64(ulong address, ulong value)
|
||||
{
|
||||
GetMemoryManager().Write(address, value);
|
||||
GetMemoryManager().WriteGuest(address, value);
|
||||
}
|
||||
|
||||
public static void WriteVector128(ulong address, V128 value)
|
||||
{
|
||||
GetMemoryManager().Write(address, value);
|
||||
GetMemoryManager().WriteGuest(address, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
@ -4,7 +4,5 @@ namespace ARMeilleure.Memory
|
||||
{
|
||||
IJitMemoryBlock Allocate(ulong size);
|
||||
IJitMemoryBlock Reserve(ulong size);
|
||||
|
||||
ulong GetPageSize();
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ namespace ARMeilleure.Memory
|
||||
|
||||
void Commit(ulong offset, ulong size);
|
||||
|
||||
void MapAsRw(ulong offset, ulong size);
|
||||
void MapAsRx(ulong offset, ulong size);
|
||||
void MapAsRwx(ulong offset, ulong size);
|
||||
}
|
||||
|
@ -28,6 +28,17 @@ namespace ARMeilleure.Memory
|
||||
/// <returns>The data</returns>
|
||||
T ReadTracked<T>(ulong va) where T : unmanaged;
|
||||
|
||||
/// <summary>
|
||||
/// Reads data from CPU mapped memory, from guest code. (with read tracking)
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the data being read</typeparam>
|
||||
/// <param name="va">Virtual address of the data in memory</param>
|
||||
/// <returns>The data</returns>
|
||||
T ReadGuest<T>(ulong va) where T : unmanaged
|
||||
{
|
||||
return ReadTracked<T>(va);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes data to CPU mapped memory.
|
||||
/// </summary>
|
||||
@ -36,6 +47,17 @@ namespace ARMeilleure.Memory
|
||||
/// <param name="value">Data to be written</param>
|
||||
void Write<T>(ulong va, T value) where T : unmanaged;
|
||||
|
||||
/// <summary>
|
||||
/// Writes data to CPU mapped memory, from guest code.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the data being written</typeparam>
|
||||
/// <param name="va">Virtual address to write the data into</param>
|
||||
/// <param name="value">Data to be written</param>
|
||||
void WriteGuest<T>(ulong va, T value) where T : unmanaged
|
||||
{
|
||||
Write(va, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a read-only span of data from CPU mapped memory.
|
||||
/// </summary>
|
||||
|
@ -29,13 +29,35 @@ namespace ARMeilleure.Memory
|
||||
/// Allows invalid access from JIT code to the rest of the program, but is faster.
|
||||
/// </summary>
|
||||
HostMappedUnsafe,
|
||||
|
||||
/// <summary>
|
||||
/// High level implementation using a software flat page table for address translation
|
||||
/// with no support for handling invalid or non-contiguous memory access.
|
||||
/// </summary>
|
||||
HostTracked,
|
||||
|
||||
/// <summary>
|
||||
/// High level implementation using a software flat page table for address translation
|
||||
/// without masking the address and no support for handling invalid or non-contiguous memory access.
|
||||
/// </summary>
|
||||
HostTrackedUnsafe,
|
||||
}
|
||||
|
||||
static class MemoryManagerTypeExtensions
|
||||
public static class MemoryManagerTypeExtensions
|
||||
{
|
||||
public static bool IsHostMapped(this MemoryManagerType type)
|
||||
{
|
||||
return type == MemoryManagerType.HostMapped || type == MemoryManagerType.HostMappedUnsafe;
|
||||
}
|
||||
|
||||
public static bool IsHostTracked(this MemoryManagerType type)
|
||||
{
|
||||
return type == MemoryManagerType.HostTracked || type == MemoryManagerType.HostTrackedUnsafe;
|
||||
}
|
||||
|
||||
public static bool IsHostMappedOrTracked(this MemoryManagerType type)
|
||||
{
|
||||
return type.IsHostMapped() || type.IsHostTracked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ using System;
|
||||
|
||||
namespace ARMeilleure.Memory
|
||||
{
|
||||
class ReservedRegion
|
||||
public class ReservedRegion
|
||||
{
|
||||
public const int DefaultGranularity = 65536; // Mapping granularity in Windows.
|
||||
|
||||
|
@ -5,7 +5,7 @@ using System.Runtime.Versioning;
|
||||
namespace ARMeilleure.Native
|
||||
{
|
||||
[SupportedOSPlatform("macos")]
|
||||
internal static partial class JitSupportDarwin
|
||||
static partial class JitSupportDarwin
|
||||
{
|
||||
[LibraryImport("libarmeilleure-jitsupport", EntryPoint = "armeilleure_jit_memcpy")]
|
||||
public static partial void Copy(IntPtr dst, IntPtr src, ulong n);
|
||||
|
@ -1,63 +1,14 @@
|
||||
using ARMeilleure.IntermediateRepresentation;
|
||||
using ARMeilleure.Memory;
|
||||
using ARMeilleure.Translation;
|
||||
using ARMeilleure.Translation.Cache;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using static ARMeilleure.IntermediateRepresentation.Operand.Factory;
|
||||
|
||||
namespace ARMeilleure.Signal
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
struct SignalHandlerRange
|
||||
public static class NativeSignalHandlerGenerator
|
||||
{
|
||||
public int IsActive;
|
||||
public nuint RangeAddress;
|
||||
public nuint RangeEndAddress;
|
||||
public IntPtr ActionPointer;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
struct SignalHandlerConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// The byte offset of the faulting address in the SigInfo or ExceptionRecord struct.
|
||||
/// </summary>
|
||||
public int StructAddressOffset;
|
||||
|
||||
/// <summary>
|
||||
/// The byte offset of the write flag in the SigInfo or ExceptionRecord struct.
|
||||
/// </summary>
|
||||
public int StructWriteOffset;
|
||||
|
||||
/// <summary>
|
||||
/// The sigaction handler that was registered before this one. (unix only)
|
||||
/// </summary>
|
||||
public nuint UnixOldSigaction;
|
||||
|
||||
/// <summary>
|
||||
/// The type of the previous sigaction. True for the 3 argument variant. (unix only)
|
||||
/// </summary>
|
||||
public int UnixOldSigaction3Arg;
|
||||
|
||||
public SignalHandlerRange Range0;
|
||||
public SignalHandlerRange Range1;
|
||||
public SignalHandlerRange Range2;
|
||||
public SignalHandlerRange Range3;
|
||||
public SignalHandlerRange Range4;
|
||||
public SignalHandlerRange Range5;
|
||||
public SignalHandlerRange Range6;
|
||||
public SignalHandlerRange Range7;
|
||||
}
|
||||
|
||||
public static class NativeSignalHandler
|
||||
{
|
||||
private delegate void UnixExceptionHandler(int sig, IntPtr info, IntPtr ucontext);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
private delegate int VectoredExceptionHandler(IntPtr exceptionInfo);
|
||||
|
||||
private const int MaxTrackedRanges = 8;
|
||||
public const int MaxTrackedRanges = 8;
|
||||
|
||||
private const int StructAddressOffset = 0;
|
||||
private const int StructWriteOffset = 4;
|
||||
@ -70,124 +21,7 @@ namespace ARMeilleure.Signal
|
||||
|
||||
private const uint EXCEPTION_ACCESS_VIOLATION = 0xc0000005;
|
||||
|
||||
private static ulong _pageSize;
|
||||
private static ulong _pageMask;
|
||||
|
||||
private static readonly IntPtr _handlerConfig;
|
||||
private static IntPtr _signalHandlerPtr;
|
||||
private static IntPtr _signalHandlerHandle;
|
||||
|
||||
private static readonly object _lock = new();
|
||||
private static bool _initialized;
|
||||
|
||||
static NativeSignalHandler()
|
||||
{
|
||||
_handlerConfig = Marshal.AllocHGlobal(Unsafe.SizeOf<SignalHandlerConfig>());
|
||||
ref SignalHandlerConfig config = ref GetConfigRef();
|
||||
|
||||
config = new SignalHandlerConfig();
|
||||
}
|
||||
|
||||
public static void Initialize(IJitMemoryAllocator allocator)
|
||||
{
|
||||
JitCache.Initialize(allocator);
|
||||
}
|
||||
|
||||
public static void InitializeSignalHandler(ulong pageSize, Func<IntPtr, IntPtr, IntPtr> customSignalHandlerFactory = null)
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_pageSize = pageSize;
|
||||
_pageMask = pageSize - 1;
|
||||
|
||||
ref SignalHandlerConfig config = ref GetConfigRef();
|
||||
|
||||
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||
{
|
||||
_signalHandlerPtr = Marshal.GetFunctionPointerForDelegate(GenerateUnixSignalHandler(_handlerConfig));
|
||||
|
||||
if (customSignalHandlerFactory != null)
|
||||
{
|
||||
_signalHandlerPtr = customSignalHandlerFactory(UnixSignalHandlerRegistration.GetSegfaultExceptionHandler().sa_handler, _signalHandlerPtr);
|
||||
}
|
||||
|
||||
var old = UnixSignalHandlerRegistration.RegisterExceptionHandler(_signalHandlerPtr);
|
||||
|
||||
config.UnixOldSigaction = (nuint)(ulong)old.sa_handler;
|
||||
config.UnixOldSigaction3Arg = old.sa_flags & 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
config.StructAddressOffset = 40; // ExceptionInformation1
|
||||
config.StructWriteOffset = 32; // ExceptionInformation0
|
||||
|
||||
_signalHandlerPtr = Marshal.GetFunctionPointerForDelegate(GenerateWindowsSignalHandler(_handlerConfig));
|
||||
|
||||
if (customSignalHandlerFactory != null)
|
||||
{
|
||||
_signalHandlerPtr = customSignalHandlerFactory(IntPtr.Zero, _signalHandlerPtr);
|
||||
}
|
||||
|
||||
_signalHandlerHandle = WindowsSignalHandlerRegistration.RegisterExceptionHandler(_signalHandlerPtr);
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe ref SignalHandlerConfig GetConfigRef()
|
||||
{
|
||||
return ref Unsafe.AsRef<SignalHandlerConfig>((void*)_handlerConfig);
|
||||
}
|
||||
|
||||
public static unsafe bool AddTrackedRegion(nuint address, nuint endAddress, IntPtr action)
|
||||
{
|
||||
var ranges = &((SignalHandlerConfig*)_handlerConfig)->Range0;
|
||||
|
||||
for (int i = 0; i < MaxTrackedRanges; i++)
|
||||
{
|
||||
if (ranges[i].IsActive == 0)
|
||||
{
|
||||
ranges[i].RangeAddress = address;
|
||||
ranges[i].RangeEndAddress = endAddress;
|
||||
ranges[i].ActionPointer = action;
|
||||
ranges[i].IsActive = 1;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static unsafe bool RemoveTrackedRegion(nuint address)
|
||||
{
|
||||
var ranges = &((SignalHandlerConfig*)_handlerConfig)->Range0;
|
||||
|
||||
for (int i = 0; i < MaxTrackedRanges; i++)
|
||||
{
|
||||
if (ranges[i].IsActive == 1 && ranges[i].RangeAddress == address)
|
||||
{
|
||||
ranges[i].IsActive = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Operand EmitGenericRegionCheck(EmitterContext context, IntPtr signalStructPtr, Operand faultAddress, Operand isWrite)
|
||||
private static Operand EmitGenericRegionCheck(EmitterContext context, IntPtr signalStructPtr, Operand faultAddress, Operand isWrite, int rangeStructSize)
|
||||
{
|
||||
Operand inRegionLocal = context.AllocateLocal(OperandType.I32);
|
||||
context.Copy(inRegionLocal, Const(0));
|
||||
@ -196,7 +30,7 @@ namespace ARMeilleure.Signal
|
||||
|
||||
for (int i = 0; i < MaxTrackedRanges; i++)
|
||||
{
|
||||
ulong rangeBaseOffset = (ulong)(RangeOffset + i * Unsafe.SizeOf<SignalHandlerRange>());
|
||||
ulong rangeBaseOffset = (ulong)(RangeOffset + i * rangeStructSize);
|
||||
|
||||
Operand nextLabel = Label();
|
||||
|
||||
@ -210,13 +44,12 @@ namespace ARMeilleure.Signal
|
||||
// Is the fault address within this tracked region?
|
||||
Operand inRange = context.BitwiseAnd(
|
||||
context.ICompare(faultAddress, rangeAddress, Comparison.GreaterOrEqualUI),
|
||||
context.ICompare(faultAddress, rangeEndAddress, Comparison.LessUI)
|
||||
);
|
||||
context.ICompare(faultAddress, rangeEndAddress, Comparison.LessUI));
|
||||
|
||||
// Only call tracking if in range.
|
||||
context.BranchIfFalse(nextLabel, inRange, BasicBlockFrequency.Cold);
|
||||
|
||||
Operand offset = context.BitwiseAnd(context.Subtract(faultAddress, rangeAddress), Const(~_pageMask));
|
||||
Operand offset = context.Subtract(faultAddress, rangeAddress);
|
||||
|
||||
// Call the tracking action, with the pointer's relative offset to the base address.
|
||||
Operand trackingActionPtr = context.Load(OperandType.I64, Const((ulong)signalStructPtr + rangeBaseOffset + 20));
|
||||
@ -227,8 +60,10 @@ namespace ARMeilleure.Signal
|
||||
|
||||
// Tracking action should be non-null to call it, otherwise assume false return.
|
||||
context.BranchIfFalse(skipActionLabel, trackingActionPtr);
|
||||
Operand result = context.Call(trackingActionPtr, OperandType.I32, offset, Const(_pageSize), isWrite);
|
||||
context.Copy(inRegionLocal, result);
|
||||
Operand result = context.Call(trackingActionPtr, OperandType.I64, offset, Const(1UL), isWrite);
|
||||
context.Copy(inRegionLocal, context.ICompareNotEqual(result, Const(0UL)));
|
||||
|
||||
GenerateFaultAddressPatchCode(context, faultAddress, result);
|
||||
|
||||
context.MarkLabel(skipActionLabel);
|
||||
|
||||
@ -269,8 +104,7 @@ namespace ARMeilleure.Signal
|
||||
Operand esr = context.Load(OperandType.I64, context.Add(ctxPtr, Const(EsrOffset)));
|
||||
return context.BitwiseAnd(esr, Const(0x40ul));
|
||||
}
|
||||
|
||||
if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
|
||||
else if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
|
||||
{
|
||||
const ulong ErrOffset = 4; // __es.__err
|
||||
Operand err = context.Load(OperandType.I64, context.Add(ctxPtr, Const(ErrOffset)));
|
||||
@ -310,8 +144,7 @@ namespace ARMeilleure.Signal
|
||||
Operand esr = context.Load(OperandType.I64, context.Add(auxPtr, Const(8ul)));
|
||||
return context.BitwiseAnd(esr, Const(0x40ul));
|
||||
}
|
||||
|
||||
if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
|
||||
else if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
|
||||
{
|
||||
const int ErrOffset = 192; // uc_mcontext.gregs[REG_ERR]
|
||||
Operand err = context.Load(OperandType.I64, context.Add(ucontextPtr, Const(ErrOffset)));
|
||||
@ -322,7 +155,7 @@ namespace ARMeilleure.Signal
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
|
||||
private static UnixExceptionHandler GenerateUnixSignalHandler(IntPtr signalStructPtr)
|
||||
public static byte[] GenerateUnixSignalHandler(IntPtr signalStructPtr, int rangeStructSize)
|
||||
{
|
||||
EmitterContext context = new();
|
||||
|
||||
@ -335,7 +168,7 @@ namespace ARMeilleure.Signal
|
||||
|
||||
Operand isWrite = context.ICompareNotEqual(writeFlag, Const(0L)); // Normalize to 0/1.
|
||||
|
||||
Operand isInRegion = EmitGenericRegionCheck(context, signalStructPtr, faultAddress, isWrite);
|
||||
Operand isInRegion = EmitGenericRegionCheck(context, signalStructPtr, faultAddress, isWrite, rangeStructSize);
|
||||
|
||||
Operand endLabel = Label();
|
||||
|
||||
@ -367,10 +200,10 @@ namespace ARMeilleure.Signal
|
||||
|
||||
OperandType[] argTypes = new OperandType[] { OperandType.I32, OperandType.I64, OperandType.I64 };
|
||||
|
||||
return Compiler.Compile(cfg, argTypes, OperandType.None, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<UnixExceptionHandler>();
|
||||
return Compiler.Compile(cfg, argTypes, OperandType.None, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Code;
|
||||
}
|
||||
|
||||
private static VectoredExceptionHandler GenerateWindowsSignalHandler(IntPtr signalStructPtr)
|
||||
public static byte[] GenerateWindowsSignalHandler(IntPtr signalStructPtr, int rangeStructSize)
|
||||
{
|
||||
EmitterContext context = new();
|
||||
|
||||
@ -399,7 +232,7 @@ namespace ARMeilleure.Signal
|
||||
|
||||
Operand isWrite = context.ICompareNotEqual(writeFlag, Const(0L)); // Normalize to 0/1.
|
||||
|
||||
Operand isInRegion = EmitGenericRegionCheck(context, signalStructPtr, faultAddress, isWrite);
|
||||
Operand isInRegion = EmitGenericRegionCheck(context, signalStructPtr, faultAddress, isWrite, rangeStructSize);
|
||||
|
||||
Operand endLabel = Label();
|
||||
|
||||
@ -421,7 +254,88 @@ namespace ARMeilleure.Signal
|
||||
|
||||
OperandType[] argTypes = new OperandType[] { OperandType.I64 };
|
||||
|
||||
return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<VectoredExceptionHandler>();
|
||||
return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Code;
|
||||
}
|
||||
|
||||
private static void GenerateFaultAddressPatchCode(EmitterContext context, Operand faultAddress, Operand newAddress)
|
||||
{
|
||||
if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
||||
{
|
||||
if (SupportsFaultAddressPatchingForHostOs())
|
||||
{
|
||||
Operand lblSkip = Label();
|
||||
|
||||
context.BranchIf(lblSkip, faultAddress, newAddress, Comparison.Equal);
|
||||
|
||||
Operand ucontextPtr = context.LoadArgument(OperandType.I64, 2);
|
||||
Operand pcCtxAddress = default;
|
||||
ulong baseRegsOffset = 0;
|
||||
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
pcCtxAddress = context.Add(ucontextPtr, Const(440UL));
|
||||
baseRegsOffset = 184UL;
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
|
||||
{
|
||||
ucontextPtr = context.Load(OperandType.I64, context.Add(ucontextPtr, Const(48UL)));
|
||||
|
||||
pcCtxAddress = context.Add(ucontextPtr, Const(272UL));
|
||||
baseRegsOffset = 16UL;
|
||||
}
|
||||
|
||||
Operand pc = context.Load(OperandType.I64, pcCtxAddress);
|
||||
|
||||
Operand reg = GetAddressRegisterFromArm64Instruction(context, pc);
|
||||
Operand reg64 = context.ZeroExtend32(OperandType.I64, reg);
|
||||
Operand regCtxAddress = context.Add(ucontextPtr, context.Add(context.ShiftLeft(reg64, Const(3)), Const(baseRegsOffset)));
|
||||
Operand regAddress = context.Load(OperandType.I64, regCtxAddress);
|
||||
|
||||
Operand addressDelta = context.Subtract(regAddress, faultAddress);
|
||||
|
||||
context.Store(regCtxAddress, context.Add(newAddress, addressDelta));
|
||||
|
||||
context.MarkLabel(lblSkip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Operand GetAddressRegisterFromArm64Instruction(EmitterContext context, Operand pc)
|
||||
{
|
||||
Operand inst = context.Load(OperandType.I32, pc);
|
||||
Operand reg = context.AllocateLocal(OperandType.I32);
|
||||
|
||||
Operand isSysInst = context.ICompareEqual(context.BitwiseAnd(inst, Const(0xFFF80000)), Const(0xD5080000));
|
||||
|
||||
Operand lblSys = Label();
|
||||
Operand lblEnd = Label();
|
||||
|
||||
context.BranchIfTrue(lblSys, isSysInst, BasicBlockFrequency.Cold);
|
||||
|
||||
context.Copy(reg, context.BitwiseAnd(context.ShiftRightUI(inst, Const(5)), Const(0x1F)));
|
||||
context.Branch(lblEnd);
|
||||
|
||||
context.MarkLabel(lblSys);
|
||||
context.Copy(reg, context.BitwiseAnd(inst, Const(0x1F)));
|
||||
|
||||
context.MarkLabel(lblEnd);
|
||||
|
||||
return reg;
|
||||
}
|
||||
|
||||
public static bool SupportsFaultAddressPatchingForHost()
|
||||
{
|
||||
return SupportsFaultAddressPatchingForHostArch() && SupportsFaultAddressPatchingForHostOs();
|
||||
}
|
||||
|
||||
private static bool SupportsFaultAddressPatchingForHostArch()
|
||||
{
|
||||
return RuntimeInformation.ProcessArchitecture == Architecture.Arm64;
|
||||
}
|
||||
|
||||
private static bool SupportsFaultAddressPatchingForHostOs()
|
||||
{
|
||||
return OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS();
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ using ARMeilleure.IntermediateRepresentation;
|
||||
using ARMeilleure.Translation;
|
||||
using Ryujinx.Common.Memory.PartialUnmaps;
|
||||
using System;
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using static ARMeilleure.IntermediateRepresentation.Operand.Factory;
|
||||
|
||||
namespace ARMeilleure.Signal
|
||||
@ -10,8 +10,28 @@ namespace ARMeilleure.Signal
|
||||
/// <summary>
|
||||
/// Methods to handle signals caused by partial unmaps. See the structs for C# implementations of the methods.
|
||||
/// </summary>
|
||||
internal static class WindowsPartialUnmapHandler
|
||||
internal static partial class WindowsPartialUnmapHandler
|
||||
{
|
||||
[LibraryImport("kernel32.dll", SetLastError = true, EntryPoint = "LoadLibraryA")]
|
||||
private static partial IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpFileName);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
private static partial IntPtr GetProcAddress(IntPtr hModule, [MarshalAs(UnmanagedType.LPStr)] string procName);
|
||||
|
||||
private static IntPtr _getCurrentThreadIdPtr;
|
||||
|
||||
public static IntPtr GetCurrentThreadIdFunc()
|
||||
{
|
||||
if (_getCurrentThreadIdPtr == IntPtr.Zero)
|
||||
{
|
||||
IntPtr handle = LoadLibrary("kernel32.dll");
|
||||
|
||||
_getCurrentThreadIdPtr = GetProcAddress(handle, "GetCurrentThreadId");
|
||||
}
|
||||
|
||||
return _getCurrentThreadIdPtr;
|
||||
}
|
||||
|
||||
public static Operand EmitRetryFromAccessViolation(EmitterContext context)
|
||||
{
|
||||
IntPtr partialRemapStatePtr = PartialUnmapState.GlobalState;
|
||||
@ -20,7 +40,7 @@ namespace ARMeilleure.Signal
|
||||
// Get the lock first.
|
||||
EmitNativeReaderLockAcquire(context, IntPtr.Add(partialRemapStatePtr, PartialUnmapState.PartialUnmapLockOffset));
|
||||
|
||||
IntPtr getCurrentThreadId = WindowsSignalHandlerRegistration.GetCurrentThreadIdFunc();
|
||||
IntPtr getCurrentThreadId = GetCurrentThreadIdFunc();
|
||||
Operand threadId = context.Call(Const((ulong)getCurrentThreadId), OperandType.I32);
|
||||
Operand threadIndex = EmitThreadLocalMapIntGetOrReserve(context, localCountsPtr, threadId, Const(0));
|
||||
|
||||
@ -137,17 +157,6 @@ namespace ARMeilleure.Signal
|
||||
return context.Add(structsPtr, context.SignExtend32(OperandType.I64, offset));
|
||||
}
|
||||
|
||||
#pragma warning disable IDE0051 // Remove unused private member
|
||||
private static void EmitThreadLocalMapIntRelease(EmitterContext context, IntPtr threadLocalMapPtr, Operand threadId, Operand index)
|
||||
{
|
||||
Operand offset = context.Multiply(index, Const(sizeof(int)));
|
||||
Operand idsPtr = Const((ulong)IntPtr.Add(threadLocalMapPtr, ThreadLocalMap<int>.ThreadIdsOffset));
|
||||
Operand idPtr = context.Add(idsPtr, context.SignExtend32(OperandType.I64, offset));
|
||||
|
||||
context.CompareAndSwap(idPtr, threadId, Const(0));
|
||||
}
|
||||
#pragma warning restore IDE0051
|
||||
|
||||
private static void EmitAtomicAddI32(EmitterContext context, Operand ptr, Operand additive)
|
||||
{
|
||||
Operand loop = Label();
|
||||
|
@ -1,44 +0,0 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace ARMeilleure.Signal
|
||||
{
|
||||
unsafe partial class WindowsSignalHandlerRegistration
|
||||
{
|
||||
[LibraryImport("kernel32.dll")]
|
||||
private static partial IntPtr AddVectoredExceptionHandler(uint first, IntPtr handler);
|
||||
|
||||
[LibraryImport("kernel32.dll")]
|
||||
private static partial ulong RemoveVectoredExceptionHandler(IntPtr handle);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true, EntryPoint = "LoadLibraryA")]
|
||||
private static partial IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpFileName);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
private static partial IntPtr GetProcAddress(IntPtr hModule, [MarshalAs(UnmanagedType.LPStr)] string procName);
|
||||
|
||||
private static IntPtr _getCurrentThreadIdPtr;
|
||||
|
||||
public static IntPtr RegisterExceptionHandler(IntPtr action)
|
||||
{
|
||||
return AddVectoredExceptionHandler(1, action);
|
||||
}
|
||||
|
||||
public static bool RemoveExceptionHandler(IntPtr handle)
|
||||
{
|
||||
return RemoveVectoredExceptionHandler(handle) != 0;
|
||||
}
|
||||
|
||||
public static IntPtr GetCurrentThreadIdFunc()
|
||||
{
|
||||
if (_getCurrentThreadIdPtr == IntPtr.Zero)
|
||||
{
|
||||
IntPtr handle = LoadLibrary("kernel32.dll");
|
||||
|
||||
_getCurrentThreadIdPtr = GetProcAddress(handle, "GetCurrentThreadId");
|
||||
}
|
||||
|
||||
return _getCurrentThreadIdPtr;
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ namespace ARMeilleure.Translation
|
||||
/// </summary>
|
||||
/// <typeparam name="TK">Key</typeparam>
|
||||
/// <typeparam name="TV">Value</typeparam>
|
||||
class IntervalTree<TK, TV> where TK : IComparable<TK>
|
||||
public class IntervalTree<TK, TV> where TK : IComparable<TK>
|
||||
{
|
||||
private const int ArrayGrowthSize = 32;
|
||||
|
||||
|
@ -29,7 +29,7 @@ namespace ARMeilleure.Translation.PTC
|
||||
private const string OuterHeaderMagicString = "PTCohd\0\0";
|
||||
private const string InnerHeaderMagicString = "PTCihd\0\0";
|
||||
|
||||
private const uint InternalVersion = 5518; //! To be incremented manually for each change to the ARMeilleure project.
|
||||
private const uint InternalVersion = 6634; //! To be incremented manually for each change to the ARMeilleure project.
|
||||
|
||||
private const string ActualDir = "0";
|
||||
private const string BackupDir = "1";
|
||||
@ -857,8 +857,14 @@ namespace ARMeilleure.Translation.PTC
|
||||
|
||||
Stopwatch sw = Stopwatch.StartNew();
|
||||
|
||||
threads.ForEach((thread) => thread.Start());
|
||||
threads.ForEach((thread) => thread.Join());
|
||||
foreach (var thread in threads)
|
||||
{
|
||||
thread.Start();
|
||||
}
|
||||
foreach (var thread in threads)
|
||||
{
|
||||
thread.Join();
|
||||
}
|
||||
|
||||
threads.Clear();
|
||||
|
||||
|
@ -57,9 +57,6 @@ namespace ARMeilleure.Translation
|
||||
private Thread[] _backgroundTranslationThreads;
|
||||
private volatile int _threadCount;
|
||||
|
||||
// FIXME: Remove this once the init logic of the emulator will be redone.
|
||||
public static readonly ManualResetEvent IsReadyForTranslation = new(false);
|
||||
|
||||
public Translator(IJitMemoryAllocator allocator, IMemoryManager memory, bool for64Bits)
|
||||
{
|
||||
_allocator = allocator;
|
||||
@ -76,14 +73,9 @@ namespace ARMeilleure.Translation
|
||||
CountTable = new EntryTable<uint>();
|
||||
Functions = new TranslatorCache<TranslatedFunction>();
|
||||
FunctionTable = new AddressTable<ulong>(for64Bits ? _levels64Bit : _levels32Bit);
|
||||
Stubs = new TranslatorStubs(this);
|
||||
Stubs = new TranslatorStubs(FunctionTable);
|
||||
|
||||
FunctionTable.Fill = (ulong)Stubs.SlowDispatchStub;
|
||||
|
||||
if (memory.Type.IsHostMapped())
|
||||
{
|
||||
NativeSignalHandler.InitializeSignalHandler(allocator.GetPageSize());
|
||||
}
|
||||
}
|
||||
|
||||
public IPtcLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled)
|
||||
@ -105,8 +97,6 @@ namespace ARMeilleure.Translation
|
||||
{
|
||||
if (Interlocked.Increment(ref _threadCount) == 1)
|
||||
{
|
||||
IsReadyForTranslation.WaitOne();
|
||||
|
||||
if (_ptc.State == PtcState.Enabled)
|
||||
{
|
||||
Debug.Assert(Functions.Count == 0);
|
||||
|
@ -1,3 +1,4 @@
|
||||
using ARMeilleure.Common;
|
||||
using ARMeilleure.Instructions;
|
||||
using ARMeilleure.IntermediateRepresentation;
|
||||
using ARMeilleure.State;
|
||||
@ -14,11 +15,11 @@ namespace ARMeilleure.Translation
|
||||
/// </summary>
|
||||
class TranslatorStubs : IDisposable
|
||||
{
|
||||
private static readonly Lazy<IntPtr> _slowDispatchStub = new(GenerateSlowDispatchStub, isThreadSafe: true);
|
||||
private readonly Lazy<IntPtr> _slowDispatchStub;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
private readonly Translator _translator;
|
||||
private readonly AddressTable<ulong> _functionTable;
|
||||
private readonly Lazy<IntPtr> _dispatchStub;
|
||||
private readonly Lazy<DispatcherFunction> _dispatchLoop;
|
||||
private readonly Lazy<WrapperFunction> _contextWrapper;
|
||||
@ -83,13 +84,14 @@ namespace ARMeilleure.Translation
|
||||
/// Initializes a new instance of the <see cref="TranslatorStubs"/> class with the specified
|
||||
/// <see cref="Translator"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="translator"><see cref="Translator"/> instance to use</param>
|
||||
/// <param name="functionTable">Function table used to store pointers to the functions that the guest code will call</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="translator"/> is null</exception>
|
||||
public TranslatorStubs(Translator translator)
|
||||
public TranslatorStubs(AddressTable<ulong> functionTable)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(translator);
|
||||
ArgumentNullException.ThrowIfNull(functionTable);
|
||||
|
||||
_translator = translator;
|
||||
_functionTable = functionTable;
|
||||
_slowDispatchStub = new(GenerateSlowDispatchStub, isThreadSafe: true);
|
||||
_dispatchStub = new(GenerateDispatchStub, isThreadSafe: true);
|
||||
_dispatchLoop = new(GenerateDispatchLoop, isThreadSafe: true);
|
||||
_contextWrapper = new(GenerateContextWrapper, isThreadSafe: true);
|
||||
@ -151,15 +153,15 @@ namespace ARMeilleure.Translation
|
||||
context.Add(nativeContext, Const((ulong)NativeContext.GetDispatchAddressOffset())));
|
||||
|
||||
// Check if guest address is within range of the AddressTable.
|
||||
Operand masked = context.BitwiseAnd(guestAddress, Const(~_translator.FunctionTable.Mask));
|
||||
Operand masked = context.BitwiseAnd(guestAddress, Const(~_functionTable.Mask));
|
||||
context.BranchIfTrue(lblFallback, masked);
|
||||
|
||||
Operand index = default;
|
||||
Operand page = Const((long)_translator.FunctionTable.Base);
|
||||
Operand page = Const((long)_functionTable.Base);
|
||||
|
||||
for (int i = 0; i < _translator.FunctionTable.Levels.Length; i++)
|
||||
for (int i = 0; i < _functionTable.Levels.Length; i++)
|
||||
{
|
||||
ref var level = ref _translator.FunctionTable.Levels[i];
|
||||
ref var level = ref _functionTable.Levels[i];
|
||||
|
||||
// level.Mask is not used directly because it is more often bigger than 32-bits, so it will not
|
||||
// be encoded as an immediate on x86's bitwise and operation.
|
||||
@ -167,7 +169,7 @@ namespace ARMeilleure.Translation
|
||||
|
||||
index = context.BitwiseAnd(context.ShiftRightUI(guestAddress, Const(level.Index)), mask);
|
||||
|
||||
if (i < _translator.FunctionTable.Levels.Length - 1)
|
||||
if (i < _functionTable.Levels.Length - 1)
|
||||
{
|
||||
page = context.Load(OperandType.I64, context.Add(page, context.ShiftLeft(index, Const(3))));
|
||||
context.BranchIfFalse(lblFallback, page);
|
||||
@ -196,7 +198,7 @@ namespace ARMeilleure.Translation
|
||||
/// Generates a <see cref="SlowDispatchStub"/>.
|
||||
/// </summary>
|
||||
/// <returns>Generated <see cref="SlowDispatchStub"/></returns>
|
||||
private static IntPtr GenerateSlowDispatchStub()
|
||||
private IntPtr GenerateSlowDispatchStub()
|
||||
{
|
||||
var context = new EmitterContext();
|
||||
|
||||
@ -205,8 +207,7 @@ namespace ARMeilleure.Translation
|
||||
Operand guestAddress = context.Load(OperandType.I64,
|
||||
context.Add(nativeContext, Const((ulong)NativeContext.GetDispatchAddressOffset())));
|
||||
|
||||
MethodInfo getFuncAddress = typeof(NativeInterface).GetMethod(nameof(NativeInterface.GetFunctionAddress));
|
||||
Operand hostAddress = context.Call(getFuncAddress, guestAddress);
|
||||
Operand hostAddress = context.Call(typeof(NativeInterface).GetMethod(nameof(NativeInterface.GetFunctionAddress)), guestAddress);
|
||||
context.Tailcall(hostAddress, nativeContext);
|
||||
|
||||
var cfg = context.GetControlFlowGraph();
|
||||
|
@ -20,6 +20,25 @@ namespace Ryujinx.Audio.Backends.OpenAL
|
||||
private bool _stillRunning;
|
||||
private readonly Thread _updaterThread;
|
||||
|
||||
private float _volume;
|
||||
|
||||
public float Volume
|
||||
{
|
||||
get
|
||||
{
|
||||
return _volume;
|
||||
}
|
||||
set
|
||||
{
|
||||
_volume = value;
|
||||
|
||||
foreach (OpenALHardwareDeviceSession session in _sessions.Keys)
|
||||
{
|
||||
session.UpdateMasterVolume(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public OpenALHardwareDeviceDriver()
|
||||
{
|
||||
_device = ALC.OpenDevice("");
|
||||
@ -34,6 +53,8 @@ namespace Ryujinx.Audio.Backends.OpenAL
|
||||
Name = "HardwareDeviceDriver.OpenAL",
|
||||
};
|
||||
|
||||
_volume = 1f;
|
||||
|
||||
_updaterThread.Start();
|
||||
}
|
||||
|
||||
@ -52,7 +73,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
|
||||
}
|
||||
}
|
||||
|
||||
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
|
||||
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
|
||||
{
|
||||
if (channelCount == 0)
|
||||
{
|
||||
@ -73,7 +94,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
|
||||
throw new ArgumentException($"{channelCount}");
|
||||
}
|
||||
|
||||
OpenALHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount, volume);
|
||||
OpenALHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount);
|
||||
|
||||
_sessions.TryAdd(session, 0);
|
||||
|
||||
|
@ -16,10 +16,11 @@ namespace Ryujinx.Audio.Backends.OpenAL
|
||||
private bool _isActive;
|
||||
private readonly Queue<OpenALAudioBuffer> _queuedBuffers;
|
||||
private ulong _playedSampleCount;
|
||||
private float _volume;
|
||||
|
||||
private readonly object _lock = new();
|
||||
|
||||
public OpenALHardwareDeviceSession(OpenALHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, float requestedVolume) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
|
||||
public OpenALHardwareDeviceSession(OpenALHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
|
||||
{
|
||||
_driver = driver;
|
||||
_queuedBuffers = new Queue<OpenALAudioBuffer>();
|
||||
@ -27,7 +28,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
|
||||
_targetFormat = GetALFormat();
|
||||
_isActive = false;
|
||||
_playedSampleCount = 0;
|
||||
SetVolume(requestedVolume);
|
||||
SetVolume(1f);
|
||||
}
|
||||
|
||||
private ALFormat GetALFormat()
|
||||
@ -85,17 +86,22 @@ namespace Ryujinx.Audio.Backends.OpenAL
|
||||
|
||||
public override void SetVolume(float volume)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
AL.Source(_sourceId, ALSourcef.Gain, volume);
|
||||
}
|
||||
_volume = volume;
|
||||
|
||||
UpdateMasterVolume(_driver.Volume);
|
||||
}
|
||||
|
||||
public override float GetVolume()
|
||||
{
|
||||
AL.GetSource(_sourceId, ALSourcef.Gain, out float volume);
|
||||
return _volume;
|
||||
}
|
||||
|
||||
return volume;
|
||||
public void UpdateMasterVolume(float newVolume)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
AL.Source(_sourceId, ALSourcef.Gain, newVolume * _volume);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Start()
|
||||
|
@ -20,6 +20,8 @@ namespace Ryujinx.Audio.Backends.SDL2
|
||||
|
||||
private readonly bool _supportSurroundConfiguration;
|
||||
|
||||
public float Volume { get; set; }
|
||||
|
||||
// TODO: Add this to SDL2-CS
|
||||
// NOTE: We use a DllImport here because of marshaling issue for spec.
|
||||
#pragma warning disable SYSLIB1054
|
||||
@ -48,6 +50,8 @@ namespace Ryujinx.Audio.Backends.SDL2
|
||||
{
|
||||
_supportSurroundConfiguration = spec.channels >= 6;
|
||||
}
|
||||
|
||||
Volume = 1f;
|
||||
}
|
||||
|
||||
public static bool IsSupported => IsSupportedInternal();
|
||||
@ -74,7 +78,7 @@ namespace Ryujinx.Audio.Backends.SDL2
|
||||
return _pauseEvent;
|
||||
}
|
||||
|
||||
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
|
||||
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
|
||||
{
|
||||
if (channelCount == 0)
|
||||
{
|
||||
@ -91,7 +95,7 @@ namespace Ryujinx.Audio.Backends.SDL2
|
||||
throw new NotImplementedException("Input direction is currently not implemented on SDL2 backend!");
|
||||
}
|
||||
|
||||
SDL2HardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount, volume);
|
||||
SDL2HardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount);
|
||||
|
||||
_sessions.TryAdd(session, 0);
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
using Ryujinx.Audio.Backends.Common;
|
||||
using Ryujinx.Audio.Common;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Memory;
|
||||
using Ryujinx.Memory;
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
@ -26,7 +28,7 @@ namespace Ryujinx.Audio.Backends.SDL2
|
||||
private float _volume;
|
||||
private readonly ushort _nativeSampleFormat;
|
||||
|
||||
public SDL2HardwareDeviceSession(SDL2HardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, float requestedVolume) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
|
||||
public SDL2HardwareDeviceSession(SDL2HardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
|
||||
{
|
||||
_driver = driver;
|
||||
_updateRequiredEvent = _driver.GetUpdateRequiredEvent();
|
||||
@ -37,7 +39,7 @@ namespace Ryujinx.Audio.Backends.SDL2
|
||||
_nativeSampleFormat = SDL2HardwareDeviceDriver.GetSDL2Format(RequestedSampleFormat);
|
||||
_sampleCount = uint.MaxValue;
|
||||
_started = false;
|
||||
_volume = requestedVolume;
|
||||
_volume = 1f;
|
||||
}
|
||||
|
||||
private void EnsureAudioStreamSetup(AudioBuffer buffer)
|
||||
@ -87,7 +89,9 @@ namespace Ryujinx.Audio.Backends.SDL2
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] samples = new byte[frameCount * _bytesPerFrame];
|
||||
using IMemoryOwner<byte> samplesOwner = ByteMemoryPool.Rent(frameCount * _bytesPerFrame);
|
||||
|
||||
Span<byte> samples = samplesOwner.Memory.Span;
|
||||
|
||||
_ringBuffer.Read(samples, 0, samples.Length);
|
||||
|
||||
@ -99,7 +103,7 @@ namespace Ryujinx.Audio.Backends.SDL2
|
||||
streamSpan.Clear();
|
||||
|
||||
// Apply volume to written data
|
||||
SDL_MixAudioFormat(stream, pStreamSrc, _nativeSampleFormat, (uint)samples.Length, (int)(_volume * SDL_MIX_MAXVOLUME));
|
||||
SDL_MixAudioFormat(stream, pStreamSrc, _nativeSampleFormat, (uint)samples.Length, (int)(_driver.Volume * _volume * SDL_MIX_MAXVOLUME));
|
||||
}
|
||||
|
||||
ulong sampleCount = GetSampleCount(samples.Length);
|
||||
|
@ -11,15 +11,15 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ContentWithTargetPath Include="Native\libsoundio\libs\libsoundio.dll" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'">
|
||||
<ContentWithTargetPath Include="Native\libsoundio\libs\libsoundio.dll" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'osx-x64'">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>libsoundio.dll</TargetPath>
|
||||
</ContentWithTargetPath>
|
||||
<ContentWithTargetPath Include="Native\libsoundio\libs\libsoundio.dylib" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'win-x64'">
|
||||
<ContentWithTargetPath Include="Native\libsoundio\libs\libsoundio.dylib" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'win-x64'">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>libsoundio.dylib</TargetPath>
|
||||
</ContentWithTargetPath>
|
||||
<ContentWithTargetPath Include="Native\libsoundio\libs\libsoundio.so" Condition="'$(RuntimeIdentifier)' != 'win-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'">
|
||||
<ContentWithTargetPath Include="Native\libsoundio\libs\libsoundio.so" Condition="'$(RuntimeIdentifier)' != 'win-x64' AND '$(RuntimeIdentifier)' != 'osx-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64'">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>libsoundio.so</TargetPath>
|
||||
</ContentWithTargetPath>
|
||||
|
@ -19,6 +19,25 @@ namespace Ryujinx.Audio.Backends.SoundIo
|
||||
private readonly ConcurrentDictionary<SoundIoHardwareDeviceSession, byte> _sessions;
|
||||
private int _disposeState;
|
||||
|
||||
private float _volume = 1f;
|
||||
|
||||
public float Volume
|
||||
{
|
||||
get
|
||||
{
|
||||
return _volume;
|
||||
}
|
||||
set
|
||||
{
|
||||
_volume = value;
|
||||
|
||||
foreach (SoundIoHardwareDeviceSession session in _sessions.Keys)
|
||||
{
|
||||
session.UpdateMasterVolume(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SoundIoHardwareDeviceDriver()
|
||||
{
|
||||
_audioContext = SoundIoContext.Create();
|
||||
@ -122,7 +141,7 @@ namespace Ryujinx.Audio.Backends.SoundIo
|
||||
return _pauseEvent;
|
||||
}
|
||||
|
||||
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
|
||||
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
|
||||
{
|
||||
if (channelCount == 0)
|
||||
{
|
||||
@ -134,14 +153,12 @@ namespace Ryujinx.Audio.Backends.SoundIo
|
||||
sampleRate = Constants.TargetSampleRate;
|
||||
}
|
||||
|
||||
volume = Math.Clamp(volume, 0, 1);
|
||||
|
||||
if (direction != Direction.Output)
|
||||
{
|
||||
throw new NotImplementedException("Input direction is currently not implemented on SoundIO backend!");
|
||||
}
|
||||
|
||||
SoundIoHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount, volume);
|
||||
SoundIoHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount);
|
||||
|
||||
_sessions.TryAdd(session, 0);
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
using Ryujinx.Audio.Backends.Common;
|
||||
using Ryujinx.Audio.Backends.SoundIo.Native;
|
||||
using Ryujinx.Audio.Common;
|
||||
using Ryujinx.Common.Memory;
|
||||
using Ryujinx.Memory;
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
@ -18,16 +20,18 @@ namespace Ryujinx.Audio.Backends.SoundIo
|
||||
private readonly DynamicRingBuffer _ringBuffer;
|
||||
private ulong _playedSampleCount;
|
||||
private readonly ManualResetEvent _updateRequiredEvent;
|
||||
private float _volume;
|
||||
private int _disposeState;
|
||||
|
||||
public SoundIoHardwareDeviceSession(SoundIoHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, float requestedVolume) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
|
||||
public SoundIoHardwareDeviceSession(SoundIoHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
|
||||
{
|
||||
_driver = driver;
|
||||
_updateRequiredEvent = _driver.GetUpdateRequiredEvent();
|
||||
_queuedBuffers = new ConcurrentQueue<SoundIoAudioBuffer>();
|
||||
_ringBuffer = new DynamicRingBuffer();
|
||||
_volume = 1f;
|
||||
|
||||
SetupOutputStream(requestedVolume);
|
||||
SetupOutputStream(driver.Volume);
|
||||
}
|
||||
|
||||
private void SetupOutputStream(float requestedVolume)
|
||||
@ -35,7 +39,7 @@ namespace Ryujinx.Audio.Backends.SoundIo
|
||||
_outputStream = _driver.OpenStream(RequestedSampleFormat, RequestedSampleRate, RequestedChannelCount);
|
||||
_outputStream.WriteCallback += Update;
|
||||
_outputStream.Volume = requestedVolume;
|
||||
// TODO: Setup other callbacks (errors, ect).
|
||||
// TODO: Setup other callbacks (errors, etc.)
|
||||
|
||||
_outputStream.Open();
|
||||
}
|
||||
@ -47,7 +51,7 @@ namespace Ryujinx.Audio.Backends.SoundIo
|
||||
|
||||
public override float GetVolume()
|
||||
{
|
||||
return _outputStream.Volume;
|
||||
return _volume;
|
||||
}
|
||||
|
||||
public override void PrepareToClose() { }
|
||||
@ -63,7 +67,14 @@ namespace Ryujinx.Audio.Backends.SoundIo
|
||||
|
||||
public override void SetVolume(float volume)
|
||||
{
|
||||
_outputStream.SetVolume(volume);
|
||||
_volume = volume;
|
||||
|
||||
_outputStream.SetVolume(_driver.Volume * volume);
|
||||
}
|
||||
|
||||
public void UpdateMasterVolume(float newVolume)
|
||||
{
|
||||
_outputStream.SetVolume(newVolume * _volume);
|
||||
}
|
||||
|
||||
public override void Start()
|
||||
@ -111,7 +122,9 @@ namespace Ryujinx.Audio.Backends.SoundIo
|
||||
|
||||
int channelCount = areas.Length;
|
||||
|
||||
byte[] samples = new byte[frameCount * bytesPerFrame];
|
||||
using IMemoryOwner<byte> samplesOwner = ByteMemoryPool.Rent(frameCount * bytesPerFrame);
|
||||
|
||||
Span<byte> samples = samplesOwner.Memory.Span;
|
||||
|
||||
_ringBuffer.Read(samples, 0, samples.Length);
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Memory;
|
||||
using System;
|
||||
using System.Buffers;
|
||||
|
||||
namespace Ryujinx.Audio.Backends.Common
|
||||
{
|
||||
@ -12,7 +14,8 @@ namespace Ryujinx.Audio.Backends.Common
|
||||
|
||||
private readonly object _lock = new();
|
||||
|
||||
private byte[] _buffer;
|
||||
private IMemoryOwner<byte> _bufferOwner;
|
||||
private Memory<byte> _buffer;
|
||||
private int _size;
|
||||
private int _headOffset;
|
||||
private int _tailOffset;
|
||||
@ -21,7 +24,8 @@ namespace Ryujinx.Audio.Backends.Common
|
||||
|
||||
public DynamicRingBuffer(int initialCapacity = RingBufferAlignment)
|
||||
{
|
||||
_buffer = new byte[initialCapacity];
|
||||
_bufferOwner = ByteMemoryPool.RentCleared(initialCapacity);
|
||||
_buffer = _bufferOwner.Memory;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
@ -33,6 +37,11 @@ namespace Ryujinx.Audio.Backends.Common
|
||||
|
||||
public void Clear(int size)
|
||||
{
|
||||
if (size == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (size > _size)
|
||||
@ -40,11 +49,6 @@ namespace Ryujinx.Audio.Backends.Common
|
||||
size = _size;
|
||||
}
|
||||
|
||||
if (size == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_headOffset = (_headOffset + size) % _buffer.Length;
|
||||
_size -= size;
|
||||
|
||||
@ -58,28 +62,31 @@ namespace Ryujinx.Audio.Backends.Common
|
||||
|
||||
private void SetCapacityLocked(int capacity)
|
||||
{
|
||||
byte[] buffer = new byte[capacity];
|
||||
IMemoryOwner<byte> newBufferOwner = ByteMemoryPool.RentCleared(capacity);
|
||||
Memory<byte> newBuffer = newBufferOwner.Memory;
|
||||
|
||||
if (_size > 0)
|
||||
{
|
||||
if (_headOffset < _tailOffset)
|
||||
{
|
||||
Buffer.BlockCopy(_buffer, _headOffset, buffer, 0, _size);
|
||||
_buffer.Slice(_headOffset, _size).CopyTo(newBuffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
Buffer.BlockCopy(_buffer, _headOffset, buffer, 0, _buffer.Length - _headOffset);
|
||||
Buffer.BlockCopy(_buffer, 0, buffer, _buffer.Length - _headOffset, _tailOffset);
|
||||
_buffer[_headOffset..].CopyTo(newBuffer);
|
||||
_buffer[.._tailOffset].CopyTo(newBuffer[(_buffer.Length - _headOffset)..]);
|
||||
}
|
||||
}
|
||||
|
||||
_buffer = buffer;
|
||||
_bufferOwner.Dispose();
|
||||
|
||||
_bufferOwner = newBufferOwner;
|
||||
_buffer = newBuffer;
|
||||
_headOffset = 0;
|
||||
_tailOffset = _size;
|
||||
}
|
||||
|
||||
|
||||
public void Write<T>(T[] buffer, int index, int count)
|
||||
public void Write(ReadOnlySpan<byte> buffer, int index, int count)
|
||||
{
|
||||
if (count == 0)
|
||||
{
|
||||
@ -99,17 +106,17 @@ namespace Ryujinx.Audio.Backends.Common
|
||||
|
||||
if (tailLength >= count)
|
||||
{
|
||||
Buffer.BlockCopy(buffer, index, _buffer, _tailOffset, count);
|
||||
buffer.Slice(index, count).CopyTo(_buffer.Span[_tailOffset..]);
|
||||
}
|
||||
else
|
||||
{
|
||||
Buffer.BlockCopy(buffer, index, _buffer, _tailOffset, tailLength);
|
||||
Buffer.BlockCopy(buffer, index + tailLength, _buffer, 0, count - tailLength);
|
||||
buffer.Slice(index, tailLength).CopyTo(_buffer.Span[_tailOffset..]);
|
||||
buffer.Slice(index + tailLength, count - tailLength).CopyTo(_buffer.Span);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Buffer.BlockCopy(buffer, index, _buffer, _tailOffset, count);
|
||||
buffer.Slice(index, count).CopyTo(_buffer.Span[_tailOffset..]);
|
||||
}
|
||||
|
||||
_size += count;
|
||||
@ -117,8 +124,13 @@ namespace Ryujinx.Audio.Backends.Common
|
||||
}
|
||||
}
|
||||
|
||||
public int Read<T>(T[] buffer, int index, int count)
|
||||
public int Read(Span<byte> buffer, int index, int count)
|
||||
{
|
||||
if (count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (count > _size)
|
||||
@ -126,14 +138,9 @@ namespace Ryujinx.Audio.Backends.Common
|
||||
count = _size;
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (_headOffset < _tailOffset)
|
||||
{
|
||||
Buffer.BlockCopy(_buffer, _headOffset, buffer, index, count);
|
||||
_buffer.Span.Slice(_headOffset, count).CopyTo(buffer[index..]);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -141,12 +148,12 @@ namespace Ryujinx.Audio.Backends.Common
|
||||
|
||||
if (tailLength >= count)
|
||||
{
|
||||
Buffer.BlockCopy(_buffer, _headOffset, buffer, index, count);
|
||||
_buffer.Span.Slice(_headOffset, count).CopyTo(buffer[index..]);
|
||||
}
|
||||
else
|
||||
{
|
||||
Buffer.BlockCopy(_buffer, _headOffset, buffer, index, tailLength);
|
||||
Buffer.BlockCopy(_buffer, 0, buffer, index + tailLength, count - tailLength);
|
||||
_buffer.Span.Slice(_headOffset, tailLength).CopyTo(buffer[index..]);
|
||||
_buffer.Span[..(count - tailLength)].CopyTo(buffer[(index + tailLength)..]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,12 @@ namespace Ryujinx.Audio.Backends.CompatLayer
|
||||
|
||||
public static bool IsSupported => true;
|
||||
|
||||
public float Volume
|
||||
{
|
||||
get => _realDriver.Volume;
|
||||
set => _realDriver.Volume = value;
|
||||
}
|
||||
|
||||
public CompatLayerHardwareDeviceDriver(IHardwareDeviceDriver realDevice)
|
||||
{
|
||||
_realDriver = realDevice;
|
||||
@ -90,7 +96,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer
|
||||
throw new ArgumentException("No valid sample format configuration found!");
|
||||
}
|
||||
|
||||
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
|
||||
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
|
||||
{
|
||||
if (channelCount == 0)
|
||||
{
|
||||
@ -102,8 +108,6 @@ namespace Ryujinx.Audio.Backends.CompatLayer
|
||||
sampleRate = Constants.TargetSampleRate;
|
||||
}
|
||||
|
||||
volume = Math.Clamp(volume, 0, 1);
|
||||
|
||||
if (!_realDriver.SupportsDirection(direction))
|
||||
{
|
||||
if (direction == Direction.Input)
|
||||
@ -119,7 +123,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer
|
||||
SampleFormat hardwareSampleFormat = SelectHardwareSampleFormat(sampleFormat);
|
||||
uint hardwareChannelCount = SelectHardwareChannelCount(channelCount);
|
||||
|
||||
IHardwareDeviceSession realSession = _realDriver.OpenDeviceSession(direction, memoryManager, hardwareSampleFormat, sampleRate, hardwareChannelCount, volume);
|
||||
IHardwareDeviceSession realSession = _realDriver.OpenDeviceSession(direction, memoryManager, hardwareSampleFormat, sampleRate, hardwareChannelCount);
|
||||
|
||||
if (hardwareChannelCount == channelCount && hardwareSampleFormat == sampleFormat)
|
||||
{
|
||||
|
@ -31,7 +31,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer
|
||||
private const int Minus6dBInQ15 = (int)(0.501f * RawQ15One);
|
||||
private const int Minus12dBInQ15 = (int)(0.251f * RawQ15One);
|
||||
|
||||
private static readonly int[] _defaultSurroundToStereoCoefficients = new int[4]
|
||||
private static readonly long[] _defaultSurroundToStereoCoefficients = new long[4]
|
||||
{
|
||||
RawQ15One,
|
||||
Minus3dBInQ15,
|
||||
@ -39,7 +39,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer
|
||||
Minus3dBInQ15,
|
||||
};
|
||||
|
||||
private static readonly int[] _defaultStereoToMonoCoefficients = new int[2]
|
||||
private static readonly long[] _defaultStereoToMonoCoefficients = new long[2]
|
||||
{
|
||||
Minus6dBInQ15,
|
||||
Minus6dBInQ15,
|
||||
@ -62,19 +62,23 @@ namespace Ryujinx.Audio.Backends.CompatLayer
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static short DownMixStereoToMono(ReadOnlySpan<int> coefficients, short left, short right)
|
||||
private static short DownMixStereoToMono(ReadOnlySpan<long> coefficients, short left, short right)
|
||||
{
|
||||
return (short)((left * coefficients[0] + right * coefficients[1]) >> Q15Bits);
|
||||
return (short)Math.Clamp((left * coefficients[0] + right * coefficients[1]) >> Q15Bits, short.MinValue, short.MaxValue);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static short DownMixSurroundToStereo(ReadOnlySpan<int> coefficients, short back, short lfe, short center, short front)
|
||||
private static short DownMixSurroundToStereo(ReadOnlySpan<long> coefficients, short back, short lfe, short center, short front)
|
||||
{
|
||||
return (short)((coefficients[3] * back + coefficients[2] * lfe + coefficients[1] * center + coefficients[0] * front + RawQ15HalfOne) >> Q15Bits);
|
||||
return (short)Math.Clamp(
|
||||
(coefficients[3] * back +
|
||||
coefficients[2] * lfe +
|
||||
coefficients[1] * center +
|
||||
coefficients[0] * front + RawQ15HalfOne) >> Q15Bits, short.MinValue, short.MaxValue);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static short[] DownMixSurroundToStereo(ReadOnlySpan<int> coefficients, ReadOnlySpan<short> data)
|
||||
private static short[] DownMixSurroundToStereo(ReadOnlySpan<long> coefficients, ReadOnlySpan<short> data)
|
||||
{
|
||||
int samplePerChannelCount = data.Length / SurroundChannelCount;
|
||||
|
||||
@ -94,7 +98,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static short[] DownMixStereoToMono(ReadOnlySpan<int> coefficients, ReadOnlySpan<short> data)
|
||||
private static short[] DownMixStereoToMono(ReadOnlySpan<long> coefficients, ReadOnlySpan<short> data)
|
||||
{
|
||||
int samplePerChannelCount = data.Length / StereoChannelCount;
|
||||
|
||||
|
@ -14,13 +14,17 @@ namespace Ryujinx.Audio.Backends.Dummy
|
||||
|
||||
public static bool IsSupported => true;
|
||||
|
||||
public float Volume { get; set; }
|
||||
|
||||
public DummyHardwareDeviceDriver()
|
||||
{
|
||||
_updateRequiredEvent = new ManualResetEvent(false);
|
||||
_pauseEvent = new ManualResetEvent(true);
|
||||
|
||||
Volume = 1f;
|
||||
}
|
||||
|
||||
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
|
||||
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
|
||||
{
|
||||
if (sampleRate == 0)
|
||||
{
|
||||
@ -34,7 +38,7 @@ namespace Ryujinx.Audio.Backends.Dummy
|
||||
|
||||
if (direction == Direction.Output)
|
||||
{
|
||||
return new DummyHardwareDeviceSessionOutput(this, memoryManager, sampleFormat, sampleRate, channelCount, volume);
|
||||
return new DummyHardwareDeviceSessionOutput(this, memoryManager, sampleFormat, sampleRate, channelCount);
|
||||
}
|
||||
|
||||
return new DummyHardwareDeviceSessionInput(this, memoryManager);
|
||||
|
@ -13,9 +13,9 @@ namespace Ryujinx.Audio.Backends.Dummy
|
||||
|
||||
private ulong _playedSampleCount;
|
||||
|
||||
public DummyHardwareDeviceSessionOutput(IHardwareDeviceDriver manager, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, float requestedVolume) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
|
||||
public DummyHardwareDeviceSessionOutput(IHardwareDeviceDriver manager, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
|
||||
{
|
||||
_volume = requestedVolume;
|
||||
_volume = 1f;
|
||||
_manager = manager;
|
||||
}
|
||||
|
||||
|
@ -166,7 +166,6 @@ namespace Ryujinx.Audio.Input
|
||||
/// </summary>
|
||||
/// <param name="filtered">If true, filter disconnected devices</param>
|
||||
/// <returns>The list of all audio inputs name</returns>
|
||||
#pragma warning disable CA1822 // Mark member as static
|
||||
public string[] ListAudioIns(bool filtered)
|
||||
{
|
||||
if (filtered)
|
||||
@ -176,7 +175,6 @@ namespace Ryujinx.Audio.Input
|
||||
|
||||
return new[] { Constants.DefaultDeviceInputName };
|
||||
}
|
||||
#pragma warning restore CA1822
|
||||
|
||||
/// <summary>
|
||||
/// Open a new <see cref="AudioInputSystem"/>.
|
||||
@ -188,8 +186,6 @@ namespace Ryujinx.Audio.Input
|
||||
/// <param name="inputDeviceName">The input device name wanted by the user</param>
|
||||
/// <param name="sampleFormat">The sample format to use</param>
|
||||
/// <param name="parameter">The user configuration</param>
|
||||
/// <param name="appletResourceUserId">The applet resource user id of the application</param>
|
||||
/// <param name="processHandle">The process handle of the application</param>
|
||||
/// <returns>A <see cref="ResultCode"/> reporting an error or a success</returns>
|
||||
public ResultCode OpenAudioIn(out string outputDeviceName,
|
||||
out AudioOutputConfiguration outputConfiguration,
|
||||
@ -197,9 +193,7 @@ namespace Ryujinx.Audio.Input
|
||||
IVirtualMemoryManager memoryManager,
|
||||
string inputDeviceName,
|
||||
SampleFormat sampleFormat,
|
||||
ref AudioInputConfiguration parameter,
|
||||
ulong appletResourceUserId,
|
||||
uint processHandle)
|
||||
ref AudioInputConfiguration parameter)
|
||||
{
|
||||
int sessionId = AcquireSessionId();
|
||||
|
||||
|
@ -13,9 +13,9 @@ namespace Ryujinx.Audio.Integration
|
||||
|
||||
private readonly byte[] _buffer;
|
||||
|
||||
public HardwareDeviceImpl(IHardwareDeviceDriver deviceDriver, uint channelCount, uint sampleRate, float volume)
|
||||
public HardwareDeviceImpl(IHardwareDeviceDriver deviceDriver, uint channelCount, uint sampleRate)
|
||||
{
|
||||
_session = deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, null, SampleFormat.PcmInt16, sampleRate, channelCount, volume);
|
||||
_session = deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, null, SampleFormat.PcmInt16, sampleRate, channelCount);
|
||||
_channelCount = channelCount;
|
||||
_sampleRate = sampleRate;
|
||||
_currentBufferTag = 0;
|
||||
|
@ -16,7 +16,9 @@ namespace Ryujinx.Audio.Integration
|
||||
Output,
|
||||
}
|
||||
|
||||
IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume = 1f);
|
||||
float Volume { get; set; }
|
||||
|
||||
IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount);
|
||||
|
||||
ManualResetEvent GetUpdateRequiredEvent();
|
||||
ManualResetEvent GetPauseEvent();
|
||||
|
@ -165,12 +165,10 @@ namespace Ryujinx.Audio.Output
|
||||
/// Get the list of all audio outputs name.
|
||||
/// </summary>
|
||||
/// <returns>The list of all audio outputs name</returns>
|
||||
#pragma warning disable CA1822 // Mark member as static
|
||||
public string[] ListAudioOuts()
|
||||
{
|
||||
return new[] { Constants.DefaultDeviceOutputName };
|
||||
}
|
||||
#pragma warning restore CA1822
|
||||
|
||||
/// <summary>
|
||||
/// Open a new <see cref="AudioOutputSystem"/>.
|
||||
@ -182,9 +180,6 @@ namespace Ryujinx.Audio.Output
|
||||
/// <param name="inputDeviceName">The input device name wanted by the user</param>
|
||||
/// <param name="sampleFormat">The sample format to use</param>
|
||||
/// <param name="parameter">The user configuration</param>
|
||||
/// <param name="appletResourceUserId">The applet resource user id of the application</param>
|
||||
/// <param name="processHandle">The process handle of the application</param>
|
||||
/// <param name="volume">The volume level to request</param>
|
||||
/// <returns>A <see cref="ResultCode"/> reporting an error or a success</returns>
|
||||
public ResultCode OpenAudioOut(out string outputDeviceName,
|
||||
out AudioOutputConfiguration outputConfiguration,
|
||||
@ -192,16 +187,13 @@ namespace Ryujinx.Audio.Output
|
||||
IVirtualMemoryManager memoryManager,
|
||||
string inputDeviceName,
|
||||
SampleFormat sampleFormat,
|
||||
ref AudioInputConfiguration parameter,
|
||||
ulong appletResourceUserId,
|
||||
uint processHandle,
|
||||
float volume)
|
||||
ref AudioInputConfiguration parameter)
|
||||
{
|
||||
int sessionId = AcquireSessionId();
|
||||
|
||||
_sessionsBufferEvents[sessionId].Clear();
|
||||
|
||||
IHardwareDeviceSession deviceSession = _deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, memoryManager, sampleFormat, parameter.SampleRate, parameter.ChannelCount, volume);
|
||||
IHardwareDeviceSession deviceSession = _deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, memoryManager, sampleFormat, parameter.SampleRate, parameter.ChannelCount);
|
||||
|
||||
AudioOutputSystem audioOut = new(this, _lock, deviceSession, _sessionsBufferEvents[sessionId]);
|
||||
|
||||
@ -234,41 +226,6 @@ namespace Ryujinx.Audio.Output
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the volume for all output devices.
|
||||
/// </summary>
|
||||
/// <param name="volume">The volume to set.</param>
|
||||
public void SetVolume(float volume)
|
||||
{
|
||||
if (_sessions != null)
|
||||
{
|
||||
foreach (AudioOutputSystem session in _sessions)
|
||||
{
|
||||
session?.SetVolume(volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the volume for all output devices.
|
||||
/// </summary>
|
||||
/// <returns>A float indicating the volume level.</returns>
|
||||
public float GetVolume()
|
||||
{
|
||||
if (_sessions != null)
|
||||
{
|
||||
foreach (AudioOutputSystem session in _sessions)
|
||||
{
|
||||
if (session != null)
|
||||
{
|
||||
return session.GetVolume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
|
@ -25,7 +25,7 @@ namespace Ryujinx.Audio.Renderer.Common
|
||||
public ulong Flags;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an error during <see cref="Server.AudioRenderSystem.Update(System.Memory{byte}, System.Memory{byte}, System.ReadOnlyMemory{byte})"/>.
|
||||
/// Represents an error during <see cref="Server.AudioRenderSystem.Update(System.Memory{byte}, System.Memory{byte}, System.Buffers.ReadOnlySequence{byte})"/>.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct ErrorInfo
|
||||
|
@ -4,7 +4,7 @@ using System.Runtime.CompilerServices;
|
||||
namespace Ryujinx.Audio.Renderer.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Update data header used for input and output of <see cref="Server.AudioRenderSystem.Update(System.Memory{byte}, System.Memory{byte}, System.ReadOnlyMemory{byte})"/>.
|
||||
/// Update data header used for input and output of <see cref="Server.AudioRenderSystem.Update(System.Memory{byte}, System.Memory{byte}, System.Buffers.ReadOnlySequence{byte})"/>.
|
||||
/// </summary>
|
||||
public struct UpdateDataHeader
|
||||
{
|
||||
|
@ -45,7 +45,6 @@ namespace Ryujinx.Audio.Renderer.Dsp
|
||||
_event = new ManualResetEvent(false);
|
||||
}
|
||||
|
||||
#pragma warning disable IDE0051 // Remove unused private member
|
||||
private static uint GetHardwareChannelCount(IHardwareDeviceDriver deviceDriver)
|
||||
{
|
||||
// Get the real device driver (In case the compat layer is on top of it).
|
||||
@ -59,9 +58,8 @@ namespace Ryujinx.Audio.Renderer.Dsp
|
||||
// NOTE: We default to stereo as this will get downmixed to mono by the compat layer if it's not compatible.
|
||||
return 2;
|
||||
}
|
||||
#pragma warning restore IDE0051
|
||||
|
||||
public void Start(IHardwareDeviceDriver deviceDriver, float volume)
|
||||
public void Start(IHardwareDeviceDriver deviceDriver)
|
||||
{
|
||||
OutputDevices = new IHardwareDevice[Constants.AudioRendererSessionCountMax];
|
||||
|
||||
@ -70,7 +68,7 @@ namespace Ryujinx.Audio.Renderer.Dsp
|
||||
for (int i = 0; i < OutputDevices.Length; i++)
|
||||
{
|
||||
// TODO: Don't hardcode sample rate.
|
||||
OutputDevices[i] = new HardwareDeviceImpl(deviceDriver, channelCount, Constants.TargetSampleRate, volume);
|
||||
OutputDevices[i] = new HardwareDeviceImpl(deviceDriver, channelCount, Constants.TargetSampleRate);
|
||||
}
|
||||
|
||||
_mailbox = new Mailbox<MailboxMessage>();
|
||||
@ -231,33 +229,6 @@ namespace Ryujinx.Audio.Renderer.Dsp
|
||||
_mailbox.SendResponse(MailboxMessage.Stop);
|
||||
}
|
||||
|
||||
public float GetVolume()
|
||||
{
|
||||
if (OutputDevices != null)
|
||||
{
|
||||
foreach (IHardwareDevice outputDevice in OutputDevices)
|
||||
{
|
||||
if (outputDevice != null)
|
||||
{
|
||||
return outputDevice.GetVolume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0f;
|
||||
}
|
||||
|
||||
public void SetVolume(float volume)
|
||||
{
|
||||
if (OutputDevices != null)
|
||||
{
|
||||
foreach (IHardwareDevice outputDevice in OutputDevices)
|
||||
{
|
||||
outputDevice?.SetVolume(volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
@ -269,6 +240,7 @@ namespace Ryujinx.Audio.Renderer.Dsp
|
||||
if (disposing)
|
||||
{
|
||||
_event.Dispose();
|
||||
_mailbox?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ namespace Ryujinx.Audio.Renderer.Parameter
|
||||
/// <summary>
|
||||
/// Output information for behaviour.
|
||||
/// </summary>
|
||||
/// <remarks>This is used to report errors to the user during <see cref="Server.AudioRenderSystem.Update(Memory{byte}, Memory{byte}, ReadOnlyMemory{byte})"/> processing.</remarks>
|
||||
/// <remarks>This is used to report errors to the user during <see cref="Server.AudioRenderSystem.Update(Memory{byte}, Memory{byte}, System.Buffers.ReadOnlySequence{byte})"/> processing.</remarks>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct BehaviourErrorInfoOutStatus
|
||||
{
|
||||
|
@ -386,7 +386,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
}
|
||||
}
|
||||
|
||||
public ResultCode Update(Memory<byte> output, Memory<byte> performanceOutput, ReadOnlyMemory<byte> input)
|
||||
public ResultCode Update(Memory<byte> output, Memory<byte> performanceOutput, ReadOnlySequence<byte> input)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
@ -419,14 +419,16 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
return result;
|
||||
}
|
||||
|
||||
result = stateUpdater.UpdateVoices(_voiceContext, _memoryPools);
|
||||
PoolMapper poolMapper = new PoolMapper(_processHandle, _memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled());
|
||||
|
||||
result = stateUpdater.UpdateVoices(_voiceContext, poolMapper);
|
||||
|
||||
if (result != ResultCode.Success)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result = stateUpdater.UpdateEffects(_effectContext, _isActive, _memoryPools);
|
||||
result = stateUpdater.UpdateEffects(_effectContext, _isActive, poolMapper);
|
||||
|
||||
if (result != ResultCode.Success)
|
||||
{
|
||||
@ -450,7 +452,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
return result;
|
||||
}
|
||||
|
||||
result = stateUpdater.UpdateSinks(_sinkContext, _memoryPools);
|
||||
result = stateUpdater.UpdateSinks(_sinkContext, poolMapper);
|
||||
|
||||
if (result != ResultCode.Success)
|
||||
{
|
||||
|
@ -177,12 +177,12 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
/// <summary>
|
||||
/// Start the <see cref="AudioProcessor"/> and worker thread.
|
||||
/// </summary>
|
||||
private void StartLocked(float volume)
|
||||
private void StartLocked()
|
||||
{
|
||||
_isRunning = true;
|
||||
|
||||
// TODO: virtual device mapping (IAudioDevice)
|
||||
Processor.Start(_deviceDriver, volume);
|
||||
Processor.Start(_deviceDriver);
|
||||
|
||||
_workerThread = new Thread(SendCommands)
|
||||
{
|
||||
@ -254,7 +254,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
/// Register a new <see cref="AudioRenderSystem"/>.
|
||||
/// </summary>
|
||||
/// <param name="renderer">The <see cref="AudioRenderSystem"/> to register.</param>
|
||||
private void Register(AudioRenderSystem renderer, float volume)
|
||||
private void Register(AudioRenderSystem renderer)
|
||||
{
|
||||
lock (_sessionLock)
|
||||
{
|
||||
@ -265,7 +265,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
{
|
||||
if (!_isRunning)
|
||||
{
|
||||
StartLocked(volume);
|
||||
StartLocked();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -312,8 +312,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
ulong appletResourceUserId,
|
||||
ulong workBufferAddress,
|
||||
ulong workBufferSize,
|
||||
uint processHandle,
|
||||
float volume)
|
||||
uint processHandle)
|
||||
{
|
||||
int sessionId = AcquireSessionId();
|
||||
|
||||
@ -338,7 +337,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
{
|
||||
renderer = audioRenderer;
|
||||
|
||||
Register(renderer, volume);
|
||||
Register(renderer);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -350,21 +349,6 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
return result;
|
||||
}
|
||||
|
||||
public float GetVolume()
|
||||
{
|
||||
if (Processor != null)
|
||||
{
|
||||
return Processor.GetVolume();
|
||||
}
|
||||
|
||||
return 0f;
|
||||
}
|
||||
|
||||
public void SetVolume(float volume)
|
||||
{
|
||||
Processor?.SetVolume(volume);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using static Ryujinx.Audio.Renderer.Common.BehaviourParameter;
|
||||
|
||||
@ -211,7 +212,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
/// <summary>
|
||||
/// Check if the audio renderer should fix the GC-ADPCM context not being provided to the DSP.
|
||||
/// </summary>
|
||||
/// <returns>True if if the audio renderer should fix it.</returns>
|
||||
/// <returns>True if the audio renderer should fix it.</returns>
|
||||
public bool IsAdpcmLoopContextBugFixed()
|
||||
{
|
||||
return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision2);
|
||||
@ -273,7 +274,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the audio renderer should trust the user destination count in <see cref="Splitter.SplitterState.Update(Splitter.SplitterContext, ref Parameter.SplitterInParameter, ReadOnlySpan{byte})"/>.
|
||||
/// Check if the audio renderer should trust the user destination count in <see cref="Renderer.Server.Splitter.SplitterState.Update(Renderer.Server.Splitter.SplitterContext, Renderer.Parameter.SplitterInParameter, SequenceReader{byte})"/>.
|
||||
/// </summary>
|
||||
/// <returns>True if the audio renderer should trust the user destination count.</returns>
|
||||
public bool IsSplitterBugFixed()
|
||||
|
@ -33,21 +33,21 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
|
||||
return WorkBuffers[index].GetReference(true);
|
||||
}
|
||||
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
|
||||
{
|
||||
Update(out updateErrorInfo, ref parameter, mapper);
|
||||
Update(out updateErrorInfo, in parameter, mapper);
|
||||
}
|
||||
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
|
||||
{
|
||||
Update(out updateErrorInfo, ref parameter, mapper);
|
||||
Update(out updateErrorInfo, in parameter, mapper);
|
||||
}
|
||||
|
||||
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
|
||||
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
|
||||
{
|
||||
Debug.Assert(IsTypeValid(ref parameter));
|
||||
Debug.Assert(IsTypeValid(in parameter));
|
||||
|
||||
UpdateParameterBase(ref parameter);
|
||||
UpdateParameterBase(in parameter);
|
||||
|
||||
Parameter = MemoryMarshal.Cast<byte, AuxiliaryBufferParameter>(parameter.SpecificData)[0];
|
||||
IsEnabled = parameter.IsEnabled;
|
||||
|
@ -81,7 +81,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
|
||||
/// </summary>
|
||||
/// <param name="parameter">The user parameter.</param>
|
||||
/// <returns>Returns true if the <see cref="EffectType"/> sent by the user matches the internal <see cref="EffectType"/>.</returns>
|
||||
public bool IsTypeValid<T>(ref T parameter) where T : unmanaged, IEffectInParameter
|
||||
public bool IsTypeValid<T>(in T parameter) where T : unmanaged, IEffectInParameter
|
||||
{
|
||||
return parameter.Type == TargetEffectType;
|
||||
}
|
||||
@ -98,7 +98,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
|
||||
/// Update the internal common parameters from a user parameter.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The user parameter.</param>
|
||||
protected void UpdateParameterBase<T>(ref T parameter) where T : unmanaged, IEffectInParameter
|
||||
protected void UpdateParameterBase<T>(in T parameter) where T : unmanaged, IEffectInParameter
|
||||
{
|
||||
MixId = parameter.MixId;
|
||||
ProcessingOrder = parameter.ProcessingOrder;
|
||||
@ -139,7 +139,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
|
||||
/// <summary>
|
||||
/// Initialize the given <paramref name="state"/> result state.
|
||||
/// </summary>
|
||||
/// <param name="state">The state to initalize</param>
|
||||
/// <param name="state">The state to initialize</param>
|
||||
public virtual void InitializeResultState(ref EffectResultState state) { }
|
||||
|
||||
/// <summary>
|
||||
@ -155,9 +155,9 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
|
||||
/// <param name="updateErrorInfo">The possible <see cref="ErrorInfo"/> that was generated.</param>
|
||||
/// <param name="parameter">The user parameter.</param>
|
||||
/// <param name="mapper">The mapper to use.</param>
|
||||
public virtual void Update(out ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
|
||||
public virtual void Update(out ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
|
||||
{
|
||||
Debug.Assert(IsTypeValid(ref parameter));
|
||||
Debug.Assert(IsTypeValid(in parameter));
|
||||
|
||||
updateErrorInfo = new ErrorInfo();
|
||||
}
|
||||
@ -168,9 +168,9 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
|
||||
/// <param name="updateErrorInfo">The possible <see cref="ErrorInfo"/> that was generated.</param>
|
||||
/// <param name="parameter">The user parameter.</param>
|
||||
/// <param name="mapper">The mapper to use.</param>
|
||||
public virtual void Update(out ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
|
||||
public virtual void Update(out ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
|
||||
{
|
||||
Debug.Assert(IsTypeValid(ref parameter));
|
||||
Debug.Assert(IsTypeValid(in parameter));
|
||||
|
||||
updateErrorInfo = new ErrorInfo();
|
||||
}
|
||||
|
@ -35,21 +35,21 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
|
||||
|
||||
public override EffectType TargetEffectType => EffectType.BiquadFilter;
|
||||
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
|
||||
{
|
||||
Update(out updateErrorInfo, ref parameter, mapper);
|
||||
Update(out updateErrorInfo, in parameter, mapper);
|
||||
}
|
||||
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
|
||||
{
|
||||
Update(out updateErrorInfo, ref parameter, mapper);
|
||||
Update(out updateErrorInfo, in parameter, mapper);
|
||||
}
|
||||
|
||||
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
|
||||
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
|
||||
{
|
||||
Debug.Assert(IsTypeValid(ref parameter));
|
||||
Debug.Assert(IsTypeValid(in parameter));
|
||||
|
||||
UpdateParameterBase(ref parameter);
|
||||
UpdateParameterBase(in parameter);
|
||||
|
||||
Parameter = MemoryMarshal.Cast<byte, BiquadFilterEffectParameter>(parameter.SpecificData)[0];
|
||||
IsEnabled = parameter.IsEnabled;
|
||||
|
@ -19,21 +19,21 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
|
||||
|
||||
public override EffectType TargetEffectType => EffectType.BufferMix;
|
||||
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
|
||||
{
|
||||
Update(out updateErrorInfo, ref parameter, mapper);
|
||||
Update(out updateErrorInfo, in parameter, mapper);
|
||||
}
|
||||
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
|
||||
{
|
||||
Update(out updateErrorInfo, ref parameter, mapper);
|
||||
Update(out updateErrorInfo, in parameter, mapper);
|
||||
}
|
||||
|
||||
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
|
||||
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
|
||||
{
|
||||
Debug.Assert(IsTypeValid(ref parameter));
|
||||
Debug.Assert(IsTypeValid(in parameter));
|
||||
|
||||
UpdateParameterBase(ref parameter);
|
||||
UpdateParameterBase(in parameter);
|
||||
|
||||
Parameter = MemoryMarshal.Cast<byte, BufferMixParameter>(parameter.SpecificData)[0];
|
||||
IsEnabled = parameter.IsEnabled;
|
||||
|
@ -32,21 +32,21 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
|
||||
return WorkBuffers[index].GetReference(true);
|
||||
}
|
||||
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
|
||||
{
|
||||
Update(out updateErrorInfo, ref parameter, mapper);
|
||||
Update(out updateErrorInfo, in parameter, mapper);
|
||||
}
|
||||
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
|
||||
{
|
||||
Update(out updateErrorInfo, ref parameter, mapper);
|
||||
Update(out updateErrorInfo, in parameter, mapper);
|
||||
}
|
||||
|
||||
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
|
||||
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
|
||||
{
|
||||
Debug.Assert(IsTypeValid(ref parameter));
|
||||
Debug.Assert(IsTypeValid(in parameter));
|
||||
|
||||
UpdateParameterBase(ref parameter);
|
||||
UpdateParameterBase(in parameter);
|
||||
|
||||
Parameter = MemoryMarshal.Cast<byte, AuxiliaryBufferParameter>(parameter.SpecificData)[0];
|
||||
IsEnabled = parameter.IsEnabled;
|
||||
|
@ -39,17 +39,17 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
|
||||
return GetSingleBuffer();
|
||||
}
|
||||
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
|
||||
{
|
||||
// Nintendo doesn't do anything here but we still require updateErrorInfo to be initialised.
|
||||
updateErrorInfo = new BehaviourParameter.ErrorInfo();
|
||||
}
|
||||
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
|
||||
{
|
||||
Debug.Assert(IsTypeValid(ref parameter));
|
||||
Debug.Assert(IsTypeValid(in parameter));
|
||||
|
||||
UpdateParameterBase(ref parameter);
|
||||
UpdateParameterBase(in parameter);
|
||||
|
||||
Parameter = MemoryMarshal.Cast<byte, CompressorParameter>(parameter.SpecificData)[0];
|
||||
IsEnabled = parameter.IsEnabled;
|
||||
|
@ -37,19 +37,19 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
|
||||
return GetSingleBuffer();
|
||||
}
|
||||
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
|
||||
{
|
||||
Update(out updateErrorInfo, ref parameter, mapper);
|
||||
Update(out updateErrorInfo, in parameter, mapper);
|
||||
}
|
||||
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
|
||||
{
|
||||
Update(out updateErrorInfo, ref parameter, mapper);
|
||||
Update(out updateErrorInfo, in parameter, mapper);
|
||||
}
|
||||
|
||||
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
|
||||
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
|
||||
{
|
||||
Debug.Assert(IsTypeValid(ref parameter));
|
||||
Debug.Assert(IsTypeValid(in parameter));
|
||||
|
||||
ref DelayParameter delayParameter = ref MemoryMarshal.Cast<byte, DelayParameter>(parameter.SpecificData)[0];
|
||||
|
||||
@ -57,7 +57,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
|
||||
|
||||
if (delayParameter.IsChannelCountMaxValid())
|
||||
{
|
||||
UpdateParameterBase(ref parameter);
|
||||
UpdateParameterBase(in parameter);
|
||||
|
||||
UsageState oldParameterStatus = Parameter.Status;
|
||||
|
||||
|
@ -39,25 +39,25 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
|
||||
return GetSingleBuffer();
|
||||
}
|
||||
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
|
||||
{
|
||||
Update(out updateErrorInfo, ref parameter, mapper);
|
||||
Update(out updateErrorInfo, in parameter, mapper);
|
||||
}
|
||||
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
|
||||
{
|
||||
Update(out updateErrorInfo, ref parameter, mapper);
|
||||
Update(out updateErrorInfo, in parameter, mapper);
|
||||
}
|
||||
|
||||
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
|
||||
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
|
||||
{
|
||||
Debug.Assert(IsTypeValid(ref parameter));
|
||||
Debug.Assert(IsTypeValid(in parameter));
|
||||
|
||||
ref LimiterParameter limiterParameter = ref MemoryMarshal.Cast<byte, LimiterParameter>(parameter.SpecificData)[0];
|
||||
|
||||
updateErrorInfo = new BehaviourParameter.ErrorInfo();
|
||||
|
||||
UpdateParameterBase(ref parameter);
|
||||
UpdateParameterBase(in parameter);
|
||||
|
||||
Parameter = limiterParameter;
|
||||
|
||||
|
@ -36,19 +36,19 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
|
||||
return GetSingleBuffer();
|
||||
}
|
||||
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
|
||||
{
|
||||
Update(out updateErrorInfo, ref parameter, mapper);
|
||||
Update(out updateErrorInfo, in parameter, mapper);
|
||||
}
|
||||
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
|
||||
{
|
||||
Update(out updateErrorInfo, ref parameter, mapper);
|
||||
Update(out updateErrorInfo, in parameter, mapper);
|
||||
}
|
||||
|
||||
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
|
||||
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
|
||||
{
|
||||
Debug.Assert(IsTypeValid(ref parameter));
|
||||
Debug.Assert(IsTypeValid(in parameter));
|
||||
|
||||
ref Reverb3dParameter reverbParameter = ref MemoryMarshal.Cast<byte, Reverb3dParameter>(parameter.SpecificData)[0];
|
||||
|
||||
@ -56,7 +56,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
|
||||
|
||||
if (reverbParameter.IsChannelCountMaxValid())
|
||||
{
|
||||
UpdateParameterBase(ref parameter);
|
||||
UpdateParameterBase(in parameter);
|
||||
|
||||
UsageState oldParameterStatus = Parameter.ParameterStatus;
|
||||
|
||||
|
@ -39,19 +39,19 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
|
||||
return GetSingleBuffer();
|
||||
}
|
||||
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
|
||||
{
|
||||
Update(out updateErrorInfo, ref parameter, mapper);
|
||||
Update(out updateErrorInfo, in parameter, mapper);
|
||||
}
|
||||
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
|
||||
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
|
||||
{
|
||||
Update(out updateErrorInfo, ref parameter, mapper);
|
||||
Update(out updateErrorInfo, in parameter, mapper);
|
||||
}
|
||||
|
||||
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
|
||||
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
|
||||
{
|
||||
Debug.Assert(IsTypeValid(ref parameter));
|
||||
Debug.Assert(IsTypeValid(in parameter));
|
||||
|
||||
ref ReverbParameter reverbParameter = ref MemoryMarshal.Cast<byte, ReverbParameter>(parameter.SpecificData)[0];
|
||||
|
||||
@ -59,7 +59,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
|
||||
|
||||
if (reverbParameter.IsChannelCountMaxValid())
|
||||
{
|
||||
UpdateParameterBase(ref parameter);
|
||||
UpdateParameterBase(in parameter);
|
||||
|
||||
UsageState oldParameterStatus = Parameter.Status;
|
||||
|
||||
|
@ -249,7 +249,7 @@ namespace Ryujinx.Audio.Renderer.Server.MemoryPool
|
||||
/// <param name="inParameter">Input user parameter.</param>
|
||||
/// <param name="outStatus">Output user parameter.</param>
|
||||
/// <returns>Returns the <see cref="UpdateResult"/> of the operations performed.</returns>
|
||||
public UpdateResult Update(ref MemoryPoolState memoryPool, ref MemoryPoolInParameter inParameter, ref MemoryPoolOutStatus outStatus)
|
||||
public UpdateResult Update(ref MemoryPoolState memoryPool, in MemoryPoolInParameter inParameter, ref MemoryPoolOutStatus outStatus)
|
||||
{
|
||||
MemoryPoolUserState inputState = inParameter.State;
|
||||
|
||||
|
@ -195,7 +195,7 @@ namespace Ryujinx.Audio.Renderer.Server.Mix
|
||||
/// <param name="parameter">The input parameter of the mix.</param>
|
||||
/// <param name="splitterContext">The splitter context.</param>
|
||||
/// <returns>Return true, new connections were done on the adjacency matrix.</returns>
|
||||
private bool UpdateConnection(EdgeMatrix edgeMatrix, ref MixParameter parameter, ref SplitterContext splitterContext)
|
||||
private bool UpdateConnection(EdgeMatrix edgeMatrix, in MixParameter parameter, ref SplitterContext splitterContext)
|
||||
{
|
||||
bool hasNewConnections;
|
||||
|
||||
@ -259,7 +259,7 @@ namespace Ryujinx.Audio.Renderer.Server.Mix
|
||||
/// <param name="splitterContext">The splitter context.</param>
|
||||
/// <param name="behaviourContext">The behaviour context.</param>
|
||||
/// <returns>Return true if the mix was changed.</returns>
|
||||
public bool Update(EdgeMatrix edgeMatrix, ref MixParameter parameter, EffectContext effectContext, SplitterContext splitterContext, BehaviourContext behaviourContext)
|
||||
public bool Update(EdgeMatrix edgeMatrix, in MixParameter parameter, EffectContext effectContext, SplitterContext splitterContext, BehaviourContext behaviourContext)
|
||||
{
|
||||
bool isDirty;
|
||||
|
||||
@ -273,7 +273,7 @@ namespace Ryujinx.Audio.Renderer.Server.Mix
|
||||
|
||||
if (behaviourContext.IsSplitterSupported())
|
||||
{
|
||||
isDirty = UpdateConnection(edgeMatrix, ref parameter, ref splitterContext);
|
||||
isDirty = UpdateConnection(edgeMatrix, in parameter, ref splitterContext);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -59,7 +59,7 @@ namespace Ryujinx.Audio.Renderer.Server.Sink
|
||||
/// </summary>
|
||||
/// <param name="parameter">The user parameter.</param>
|
||||
/// <returns>Return true, if the <see cref="SinkType"/> sent by the user match the internal <see cref="SinkType"/>.</returns>
|
||||
public bool IsTypeValid(ref SinkInParameter parameter)
|
||||
public bool IsTypeValid(in SinkInParameter parameter)
|
||||
{
|
||||
return parameter.Type == TargetSinkType;
|
||||
}
|
||||
@ -76,7 +76,7 @@ namespace Ryujinx.Audio.Renderer.Server.Sink
|
||||
/// Update the internal common parameters from user parameter.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The user parameter.</param>
|
||||
protected void UpdateStandardParameter(ref SinkInParameter parameter)
|
||||
protected void UpdateStandardParameter(in SinkInParameter parameter)
|
||||
{
|
||||
if (IsUsed != parameter.IsUsed)
|
||||
{
|
||||
@ -92,9 +92,9 @@ namespace Ryujinx.Audio.Renderer.Server.Sink
|
||||
/// <param name="parameter">The user parameter.</param>
|
||||
/// <param name="outStatus">The user output status.</param>
|
||||
/// <param name="mapper">The mapper to use.</param>
|
||||
public virtual void Update(out ErrorInfo errorInfo, ref SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
|
||||
public virtual void Update(out ErrorInfo errorInfo, in SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
|
||||
{
|
||||
Debug.Assert(IsTypeValid(ref parameter));
|
||||
Debug.Assert(IsTypeValid(in parameter));
|
||||
|
||||
errorInfo = new ErrorInfo();
|
||||
}
|
||||
|
@ -44,18 +44,18 @@ namespace Ryujinx.Audio.Renderer.Server.Sink
|
||||
|
||||
public override SinkType TargetSinkType => SinkType.CircularBuffer;
|
||||
|
||||
public override void Update(out BehaviourParameter.ErrorInfo errorInfo, ref SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
|
||||
public override void Update(out BehaviourParameter.ErrorInfo errorInfo, in SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
|
||||
{
|
||||
errorInfo = new BehaviourParameter.ErrorInfo();
|
||||
outStatus = new SinkOutStatus();
|
||||
|
||||
Debug.Assert(IsTypeValid(ref parameter));
|
||||
Debug.Assert(IsTypeValid(in parameter));
|
||||
|
||||
ref CircularBufferParameter inputDeviceParameter = ref MemoryMarshal.Cast<byte, CircularBufferParameter>(parameter.SpecificData)[0];
|
||||
|
||||
if (parameter.IsUsed != IsUsed || ShouldSkip)
|
||||
{
|
||||
UpdateStandardParameter(ref parameter);
|
||||
UpdateStandardParameter(in parameter);
|
||||
|
||||
if (parameter.IsUsed)
|
||||
{
|
||||
|
@ -49,15 +49,15 @@ namespace Ryujinx.Audio.Renderer.Server.Sink
|
||||
|
||||
public override SinkType TargetSinkType => SinkType.Device;
|
||||
|
||||
public override void Update(out BehaviourParameter.ErrorInfo errorInfo, ref SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
|
||||
public override void Update(out BehaviourParameter.ErrorInfo errorInfo, in SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
|
||||
{
|
||||
Debug.Assert(IsTypeValid(ref parameter));
|
||||
Debug.Assert(IsTypeValid(in parameter));
|
||||
|
||||
ref DeviceParameter inputDeviceParameter = ref MemoryMarshal.Cast<byte, DeviceParameter>(parameter.SpecificData)[0];
|
||||
|
||||
if (parameter.IsUsed != IsUsed)
|
||||
{
|
||||
UpdateStandardParameter(ref parameter);
|
||||
UpdateStandardParameter(in parameter);
|
||||
Parameter = inputDeviceParameter;
|
||||
}
|
||||
else
|
||||
|
@ -2,10 +2,11 @@ using Ryujinx.Audio.Renderer.Common;
|
||||
using Ryujinx.Audio.Renderer.Parameter;
|
||||
using Ryujinx.Audio.Renderer.Utils;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Extensions;
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
{
|
||||
@ -25,7 +26,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
private Memory<SplitterDestination> _splitterDestinations;
|
||||
|
||||
/// <summary>
|
||||
/// If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, ref SplitterInParameter, ReadOnlySpan{byte})"/>.
|
||||
/// If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, in SplitterInParameter, ref SequenceReader{byte})"/>.
|
||||
/// </summary>
|
||||
public bool IsBugFixed { get; private set; }
|
||||
|
||||
@ -110,7 +111,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
/// </summary>
|
||||
/// <param name="splitters">The <see cref="SplitterState"/> storage.</param>
|
||||
/// <param name="splitterDestinations">The <see cref="SplitterDestination"/> storage.</param>
|
||||
/// <param name="isBugFixed">If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, ref SplitterInParameter, ReadOnlySpan{byte})"/>.</param>
|
||||
/// <param name="isBugFixed">If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, in SplitterInParameter, ref SequenceReader{byte})"/>.</param>
|
||||
private void Setup(Memory<SplitterState> splitters, Memory<SplitterDestination> splitterDestinations, bool isBugFixed)
|
||||
{
|
||||
_splitters = splitters;
|
||||
@ -148,11 +149,11 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
/// </summary>
|
||||
/// <param name="inputHeader">The splitter header.</param>
|
||||
/// <param name="input">The raw data after the splitter header.</param>
|
||||
private void UpdateState(scoped ref SplitterInParameterHeader inputHeader, ref ReadOnlySpan<byte> input)
|
||||
private void UpdateState(in SplitterInParameterHeader inputHeader, ref SequenceReader<byte> input)
|
||||
{
|
||||
for (int i = 0; i < inputHeader.SplitterCount; i++)
|
||||
{
|
||||
SplitterInParameter parameter = MemoryMarshal.Read<SplitterInParameter>(input);
|
||||
ref readonly SplitterInParameter parameter = ref input.GetRefOrRefToCopy<SplitterInParameter>(out _);
|
||||
|
||||
Debug.Assert(parameter.IsMagicValid());
|
||||
|
||||
@ -162,10 +163,16 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
{
|
||||
ref SplitterState splitter = ref GetState(parameter.Id);
|
||||
|
||||
splitter.Update(this, ref parameter, input[Unsafe.SizeOf<SplitterInParameter>()..]);
|
||||
splitter.Update(this, in parameter, ref input);
|
||||
}
|
||||
|
||||
input = input[(0x1C + parameter.DestinationCount * 4)..];
|
||||
// NOTE: there are 12 bytes of unused/unknown data after the destination IDs array.
|
||||
input.Advance(0xC);
|
||||
}
|
||||
else
|
||||
{
|
||||
input.Rewind(Unsafe.SizeOf<SplitterInParameter>());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -175,11 +182,11 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
/// </summary>
|
||||
/// <param name="inputHeader">The splitter header.</param>
|
||||
/// <param name="input">The raw data after the splitter header.</param>
|
||||
private void UpdateData(scoped ref SplitterInParameterHeader inputHeader, ref ReadOnlySpan<byte> input)
|
||||
private void UpdateData(in SplitterInParameterHeader inputHeader, ref SequenceReader<byte> input)
|
||||
{
|
||||
for (int i = 0; i < inputHeader.SplitterDestinationCount; i++)
|
||||
{
|
||||
SplitterDestinationInParameter parameter = MemoryMarshal.Read<SplitterDestinationInParameter>(input);
|
||||
ref readonly SplitterDestinationInParameter parameter = ref input.GetRefOrRefToCopy<SplitterDestinationInParameter>(out _);
|
||||
|
||||
Debug.Assert(parameter.IsMagicValid());
|
||||
|
||||
@ -191,8 +198,11 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
|
||||
destination.Update(parameter);
|
||||
}
|
||||
|
||||
input = input[Unsafe.SizeOf<SplitterDestinationInParameter>()..];
|
||||
}
|
||||
else
|
||||
{
|
||||
input.Rewind(Unsafe.SizeOf<SplitterDestinationInParameter>());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -201,36 +211,33 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
/// Update splitter from user parameters.
|
||||
/// </summary>
|
||||
/// <param name="input">The input raw user data.</param>
|
||||
/// <param name="consumedSize">The total consumed size.</param>
|
||||
/// <returns>Return true if the update was successful.</returns>
|
||||
public bool Update(ReadOnlySpan<byte> input, out int consumedSize)
|
||||
public bool Update(ref SequenceReader<byte> input)
|
||||
{
|
||||
if (_splitterDestinations.IsEmpty || _splitters.IsEmpty)
|
||||
{
|
||||
consumedSize = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int originalSize = input.Length;
|
||||
|
||||
SplitterInParameterHeader header = SpanIOHelper.Read<SplitterInParameterHeader>(ref input);
|
||||
ref readonly SplitterInParameterHeader header = ref input.GetRefOrRefToCopy<SplitterInParameterHeader>(out _);
|
||||
|
||||
if (header.IsMagicValid())
|
||||
{
|
||||
ClearAllNewConnectionFlag();
|
||||
|
||||
UpdateState(ref header, ref input);
|
||||
UpdateData(ref header, ref input);
|
||||
UpdateState(in header, ref input);
|
||||
UpdateData(in header, ref input);
|
||||
|
||||
consumedSize = BitUtils.AlignUp(originalSize - input.Length, 0x10);
|
||||
input.SetConsumed(BitUtils.AlignUp(input.Consumed, 0x10));
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
input.Rewind(Unsafe.SizeOf<SplitterInParameterHeader>());
|
||||
|
||||
consumedSize = 0;
|
||||
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Ryujinx.Audio.Renderer.Parameter;
|
||||
using Ryujinx.Common.Extensions;
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
@ -122,7 +123,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
/// <param name="context">The splitter context.</param>
|
||||
/// <param name="parameter">The user parameter.</param>
|
||||
/// <param name="input">The raw input data after the <paramref name="parameter"/>.</param>
|
||||
public void Update(SplitterContext context, ref SplitterInParameter parameter, ReadOnlySpan<byte> input)
|
||||
public void Update(SplitterContext context, in SplitterInParameter parameter, ref SequenceReader<byte> input)
|
||||
{
|
||||
ClearLinks();
|
||||
|
||||
@ -139,9 +140,9 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
|
||||
if (destinationCount > 0)
|
||||
{
|
||||
ReadOnlySpan<int> destinationIds = MemoryMarshal.Cast<byte, int>(input);
|
||||
input.ReadLittleEndian(out int destinationId);
|
||||
|
||||
Memory<SplitterDestination> destination = context.GetDestinationMemory(destinationIds[0]);
|
||||
Memory<SplitterDestination> destination = context.GetDestinationMemory(destinationId);
|
||||
|
||||
SetDestination(ref destination.Span[0]);
|
||||
|
||||
@ -149,13 +150,20 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
|
||||
for (int i = 1; i < destinationCount; i++)
|
||||
{
|
||||
Memory<SplitterDestination> nextDestination = context.GetDestinationMemory(destinationIds[i]);
|
||||
input.ReadLittleEndian(out destinationId);
|
||||
|
||||
Memory<SplitterDestination> nextDestination = context.GetDestinationMemory(destinationId);
|
||||
|
||||
destination.Span[0].Link(ref nextDestination.Span[0]);
|
||||
destination = nextDestination;
|
||||
}
|
||||
}
|
||||
|
||||
if (destinationCount < parameter.DestinationCount)
|
||||
{
|
||||
input.Advance((parameter.DestinationCount - destinationCount) * sizeof(int));
|
||||
}
|
||||
|
||||
Debug.Assert(parameter.Id == Id);
|
||||
|
||||
if (parameter.Id == Id)
|
||||
|
@ -9,41 +9,40 @@ using Ryujinx.Audio.Renderer.Server.Sink;
|
||||
using Ryujinx.Audio.Renderer.Server.Splitter;
|
||||
using Ryujinx.Audio.Renderer.Server.Voice;
|
||||
using Ryujinx.Audio.Renderer.Utils;
|
||||
using Ryujinx.Common.Extensions;
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using static Ryujinx.Audio.Renderer.Common.BehaviourParameter;
|
||||
|
||||
namespace Ryujinx.Audio.Renderer.Server
|
||||
{
|
||||
public class StateUpdater
|
||||
public ref struct StateUpdater
|
||||
{
|
||||
private readonly ReadOnlyMemory<byte> _inputOrigin;
|
||||
private SequenceReader<byte> _inputReader;
|
||||
|
||||
private readonly ReadOnlyMemory<byte> _outputOrigin;
|
||||
private ReadOnlyMemory<byte> _input;
|
||||
|
||||
private Memory<byte> _output;
|
||||
private readonly uint _processHandle;
|
||||
private BehaviourContext _behaviourContext;
|
||||
|
||||
private UpdateDataHeader _inputHeader;
|
||||
private readonly ref readonly UpdateDataHeader _inputHeader;
|
||||
private readonly Memory<UpdateDataHeader> _outputHeader;
|
||||
|
||||
private ref UpdateDataHeader OutputHeader => ref _outputHeader.Span[0];
|
||||
private readonly ref UpdateDataHeader OutputHeader => ref _outputHeader.Span[0];
|
||||
|
||||
public StateUpdater(ReadOnlyMemory<byte> input, Memory<byte> output, uint processHandle, BehaviourContext behaviourContext)
|
||||
public StateUpdater(ReadOnlySequence<byte> input, Memory<byte> output, uint processHandle, BehaviourContext behaviourContext)
|
||||
{
|
||||
_input = input;
|
||||
_inputOrigin = _input;
|
||||
_inputReader = new SequenceReader<byte>(input);
|
||||
_output = output;
|
||||
_outputOrigin = _output;
|
||||
_processHandle = processHandle;
|
||||
_behaviourContext = behaviourContext;
|
||||
|
||||
_inputHeader = SpanIOHelper.Read<UpdateDataHeader>(ref _input);
|
||||
_inputHeader = ref _inputReader.GetRefOrRefToCopy<UpdateDataHeader>(out _);
|
||||
|
||||
_outputHeader = SpanMemoryManager<UpdateDataHeader>.Cast(_output[..Unsafe.SizeOf<UpdateDataHeader>()]);
|
||||
OutputHeader.Initialize(_behaviourContext.UserRevision);
|
||||
@ -52,7 +51,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
|
||||
public ResultCode UpdateBehaviourContext()
|
||||
{
|
||||
BehaviourParameter parameter = SpanIOHelper.Read<BehaviourParameter>(ref _input);
|
||||
ref readonly BehaviourParameter parameter = ref _inputReader.GetRefOrRefToCopy<BehaviourParameter>(out _);
|
||||
|
||||
if (!BehaviourContext.CheckValidRevision(parameter.UserRevision) || parameter.UserRevision != _behaviourContext.UserRevision)
|
||||
{
|
||||
@ -81,11 +80,11 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
|
||||
foreach (ref MemoryPoolState memoryPool in memoryPools)
|
||||
{
|
||||
MemoryPoolInParameter parameter = SpanIOHelper.Read<MemoryPoolInParameter>(ref _input);
|
||||
ref readonly MemoryPoolInParameter parameter = ref _inputReader.GetRefOrRefToCopy<MemoryPoolInParameter>(out _);
|
||||
|
||||
ref MemoryPoolOutStatus outStatus = ref SpanIOHelper.GetWriteRef<MemoryPoolOutStatus>(ref _output)[0];
|
||||
|
||||
PoolMapper.UpdateResult updateResult = mapper.Update(ref memoryPool, ref parameter, ref outStatus);
|
||||
PoolMapper.UpdateResult updateResult = mapper.Update(ref memoryPool, in parameter, ref outStatus);
|
||||
|
||||
if (updateResult != PoolMapper.UpdateResult.Success &&
|
||||
updateResult != PoolMapper.UpdateResult.MapError &&
|
||||
@ -115,7 +114,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
|
||||
for (int i = 0; i < context.GetCount(); i++)
|
||||
{
|
||||
VoiceChannelResourceInParameter parameter = SpanIOHelper.Read<VoiceChannelResourceInParameter>(ref _input);
|
||||
ref readonly VoiceChannelResourceInParameter parameter = ref _inputReader.GetRefOrRefToCopy<VoiceChannelResourceInParameter>(out _);
|
||||
|
||||
ref VoiceChannelResource resource = ref context.GetChannelResource(i);
|
||||
|
||||
@ -127,7 +126,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
public ResultCode UpdateVoices(VoiceContext context, Memory<MemoryPoolState> memoryPools)
|
||||
public ResultCode UpdateVoices(VoiceContext context, PoolMapper mapper)
|
||||
{
|
||||
if (context.GetCount() * Unsafe.SizeOf<VoiceInParameter>() != _inputHeader.VoicesSize)
|
||||
{
|
||||
@ -136,11 +135,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
|
||||
int initialOutputSize = _output.Length;
|
||||
|
||||
ReadOnlySpan<VoiceInParameter> parameters = MemoryMarshal.Cast<byte, VoiceInParameter>(_input[..(int)_inputHeader.VoicesSize].Span);
|
||||
|
||||
_input = _input[(int)_inputHeader.VoicesSize..];
|
||||
|
||||
PoolMapper mapper = new(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled());
|
||||
long initialInputConsumed = _inputReader.Consumed;
|
||||
|
||||
// First make everything not in use.
|
||||
for (int i = 0; i < context.GetCount(); i++)
|
||||
@ -157,7 +152,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
// Start processing
|
||||
for (int i = 0; i < context.GetCount(); i++)
|
||||
{
|
||||
VoiceInParameter parameter = parameters[i];
|
||||
ref readonly VoiceInParameter parameter = ref _inputReader.GetRefOrRefToCopy<VoiceInParameter>(out _);
|
||||
|
||||
voiceUpdateStates.Fill(Memory<VoiceUpdateState>.Empty);
|
||||
|
||||
@ -181,14 +176,14 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
currentVoiceState.Initialize();
|
||||
}
|
||||
|
||||
currentVoiceState.UpdateParameters(out ErrorInfo updateParameterError, ref parameter, ref mapper, ref _behaviourContext);
|
||||
currentVoiceState.UpdateParameters(out ErrorInfo updateParameterError, in parameter, mapper, ref _behaviourContext);
|
||||
|
||||
if (updateParameterError.ErrorCode != ResultCode.Success)
|
||||
{
|
||||
_behaviourContext.AppendError(ref updateParameterError);
|
||||
}
|
||||
|
||||
currentVoiceState.UpdateWaveBuffers(out ErrorInfo[] waveBufferUpdateErrorInfos, ref parameter, voiceUpdateStates, ref mapper, ref _behaviourContext);
|
||||
currentVoiceState.UpdateWaveBuffers(out ErrorInfo[] waveBufferUpdateErrorInfos, in parameter, voiceUpdateStates, mapper, ref _behaviourContext);
|
||||
|
||||
foreach (ref ErrorInfo errorInfo in waveBufferUpdateErrorInfos.AsSpan())
|
||||
{
|
||||
@ -198,7 +193,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
}
|
||||
}
|
||||
|
||||
currentVoiceState.WriteOutStatus(ref outStatus, ref parameter, voiceUpdateStates);
|
||||
currentVoiceState.WriteOutStatus(ref outStatus, in parameter, voiceUpdateStates);
|
||||
}
|
||||
}
|
||||
|
||||
@ -211,10 +206,12 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
|
||||
Debug.Assert((initialOutputSize - currentOutputSize) == OutputHeader.VoicesSize);
|
||||
|
||||
_inputReader.SetConsumed(initialInputConsumed + _inputHeader.VoicesSize);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
private static void ResetEffect<T>(ref BaseEffect effect, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
|
||||
private static void ResetEffect<T>(ref BaseEffect effect, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
|
||||
{
|
||||
effect.ForceUnmapBuffers(mapper);
|
||||
|
||||
@ -234,17 +231,17 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
};
|
||||
}
|
||||
|
||||
public ResultCode UpdateEffects(EffectContext context, bool isAudioRendererActive, Memory<MemoryPoolState> memoryPools)
|
||||
public ResultCode UpdateEffects(EffectContext context, bool isAudioRendererActive, PoolMapper mapper)
|
||||
{
|
||||
if (_behaviourContext.IsEffectInfoVersion2Supported())
|
||||
{
|
||||
return UpdateEffectsVersion2(context, isAudioRendererActive, memoryPools);
|
||||
return UpdateEffectsVersion2(context, isAudioRendererActive, mapper);
|
||||
}
|
||||
|
||||
return UpdateEffectsVersion1(context, isAudioRendererActive, memoryPools);
|
||||
return UpdateEffectsVersion1(context, isAudioRendererActive, mapper);
|
||||
}
|
||||
|
||||
public ResultCode UpdateEffectsVersion2(EffectContext context, bool isAudioRendererActive, Memory<MemoryPoolState> memoryPools)
|
||||
public ResultCode UpdateEffectsVersion2(EffectContext context, bool isAudioRendererActive, PoolMapper mapper)
|
||||
{
|
||||
if (context.GetCount() * Unsafe.SizeOf<EffectInParameterVersion2>() != _inputHeader.EffectsSize)
|
||||
{
|
||||
@ -253,26 +250,22 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
|
||||
int initialOutputSize = _output.Length;
|
||||
|
||||
ReadOnlySpan<EffectInParameterVersion2> parameters = MemoryMarshal.Cast<byte, EffectInParameterVersion2>(_input[..(int)_inputHeader.EffectsSize].Span);
|
||||
|
||||
_input = _input[(int)_inputHeader.EffectsSize..];
|
||||
|
||||
PoolMapper mapper = new(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled());
|
||||
long initialInputConsumed = _inputReader.Consumed;
|
||||
|
||||
for (int i = 0; i < context.GetCount(); i++)
|
||||
{
|
||||
EffectInParameterVersion2 parameter = parameters[i];
|
||||
ref readonly EffectInParameterVersion2 parameter = ref _inputReader.GetRefOrRefToCopy<EffectInParameterVersion2>(out _);
|
||||
|
||||
ref EffectOutStatusVersion2 outStatus = ref SpanIOHelper.GetWriteRef<EffectOutStatusVersion2>(ref _output)[0];
|
||||
|
||||
ref BaseEffect effect = ref context.GetEffect(i);
|
||||
|
||||
if (!effect.IsTypeValid(ref parameter))
|
||||
if (!effect.IsTypeValid(in parameter))
|
||||
{
|
||||
ResetEffect(ref effect, ref parameter, mapper);
|
||||
ResetEffect(ref effect, in parameter, mapper);
|
||||
}
|
||||
|
||||
effect.Update(out ErrorInfo updateErrorInfo, ref parameter, mapper);
|
||||
effect.Update(out ErrorInfo updateErrorInfo, in parameter, mapper);
|
||||
|
||||
if (updateErrorInfo.ErrorCode != ResultCode.Success)
|
||||
{
|
||||
@ -297,10 +290,12 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
|
||||
Debug.Assert((initialOutputSize - currentOutputSize) == OutputHeader.EffectsSize);
|
||||
|
||||
_inputReader.SetConsumed(initialInputConsumed + _inputHeader.EffectsSize);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
public ResultCode UpdateEffectsVersion1(EffectContext context, bool isAudioRendererActive, Memory<MemoryPoolState> memoryPools)
|
||||
public ResultCode UpdateEffectsVersion1(EffectContext context, bool isAudioRendererActive, PoolMapper mapper)
|
||||
{
|
||||
if (context.GetCount() * Unsafe.SizeOf<EffectInParameterVersion1>() != _inputHeader.EffectsSize)
|
||||
{
|
||||
@ -309,26 +304,22 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
|
||||
int initialOutputSize = _output.Length;
|
||||
|
||||
ReadOnlySpan<EffectInParameterVersion1> parameters = MemoryMarshal.Cast<byte, EffectInParameterVersion1>(_input[..(int)_inputHeader.EffectsSize].Span);
|
||||
|
||||
_input = _input[(int)_inputHeader.EffectsSize..];
|
||||
|
||||
PoolMapper mapper = new(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled());
|
||||
long initialInputConsumed = _inputReader.Consumed;
|
||||
|
||||
for (int i = 0; i < context.GetCount(); i++)
|
||||
{
|
||||
EffectInParameterVersion1 parameter = parameters[i];
|
||||
ref readonly EffectInParameterVersion1 parameter = ref _inputReader.GetRefOrRefToCopy<EffectInParameterVersion1>(out _);
|
||||
|
||||
ref EffectOutStatusVersion1 outStatus = ref SpanIOHelper.GetWriteRef<EffectOutStatusVersion1>(ref _output)[0];
|
||||
|
||||
ref BaseEffect effect = ref context.GetEffect(i);
|
||||
|
||||
if (!effect.IsTypeValid(ref parameter))
|
||||
if (!effect.IsTypeValid(in parameter))
|
||||
{
|
||||
ResetEffect(ref effect, ref parameter, mapper);
|
||||
ResetEffect(ref effect, in parameter, mapper);
|
||||
}
|
||||
|
||||
effect.Update(out ErrorInfo updateErrorInfo, ref parameter, mapper);
|
||||
effect.Update(out ErrorInfo updateErrorInfo, in parameter, mapper);
|
||||
|
||||
if (updateErrorInfo.ErrorCode != ResultCode.Success)
|
||||
{
|
||||
@ -345,38 +336,40 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
|
||||
Debug.Assert((initialOutputSize - currentOutputSize) == OutputHeader.EffectsSize);
|
||||
|
||||
_inputReader.SetConsumed(initialInputConsumed + _inputHeader.EffectsSize);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
public ResultCode UpdateSplitter(SplitterContext context)
|
||||
{
|
||||
if (context.Update(_input.Span, out int consumedSize))
|
||||
if (context.Update(ref _inputReader))
|
||||
{
|
||||
_input = _input[consumedSize..];
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
return ResultCode.InvalidUpdateInfo;
|
||||
}
|
||||
|
||||
private static bool CheckMixParametersValidity(MixContext mixContext, uint mixBufferCount, uint inputMixCount, ReadOnlySpan<MixParameter> parameters)
|
||||
private static bool CheckMixParametersValidity(MixContext mixContext, uint mixBufferCount, uint inputMixCount, SequenceReader<byte> parameters)
|
||||
{
|
||||
uint maxMixStateCount = mixContext.GetCount();
|
||||
uint totalRequiredMixBufferCount = 0;
|
||||
|
||||
for (int i = 0; i < inputMixCount; i++)
|
||||
{
|
||||
if (parameters[i].IsUsed)
|
||||
ref readonly MixParameter parameter = ref parameters.GetRefOrRefToCopy<MixParameter>(out _);
|
||||
|
||||
if (parameter.IsUsed)
|
||||
{
|
||||
if (parameters[i].DestinationMixId != Constants.UnusedMixId &&
|
||||
parameters[i].DestinationMixId > maxMixStateCount &&
|
||||
parameters[i].MixId != Constants.FinalMixId)
|
||||
if (parameter.DestinationMixId != Constants.UnusedMixId &&
|
||||
parameter.DestinationMixId > maxMixStateCount &&
|
||||
parameter.MixId != Constants.FinalMixId)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
totalRequiredMixBufferCount += parameters[i].BufferCount;
|
||||
totalRequiredMixBufferCount += parameter.BufferCount;
|
||||
}
|
||||
}
|
||||
|
||||
@ -391,7 +384,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
|
||||
if (_behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported())
|
||||
{
|
||||
MixInParameterDirtyOnlyUpdate parameter = MemoryMarshal.Cast<byte, MixInParameterDirtyOnlyUpdate>(_input.Span)[0];
|
||||
ref readonly MixInParameterDirtyOnlyUpdate parameter = ref _inputReader.GetRefOrRefToCopy<MixInParameterDirtyOnlyUpdate>(out _);
|
||||
|
||||
mixCount = parameter.MixCount;
|
||||
|
||||
@ -411,25 +404,20 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
return ResultCode.InvalidUpdateInfo;
|
||||
}
|
||||
|
||||
if (_behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported())
|
||||
{
|
||||
_input = _input[Unsafe.SizeOf<MixInParameterDirtyOnlyUpdate>()..];
|
||||
}
|
||||
long initialInputConsumed = _inputReader.Consumed;
|
||||
|
||||
ReadOnlySpan<MixParameter> parameters = MemoryMarshal.Cast<byte, MixParameter>(_input.Span[..(int)inputMixSize]);
|
||||
int parameterCount = (int)inputMixSize / Unsafe.SizeOf<MixParameter>();
|
||||
|
||||
_input = _input[(int)inputMixSize..];
|
||||
|
||||
if (CheckMixParametersValidity(mixContext, mixBufferCount, mixCount, parameters))
|
||||
if (CheckMixParametersValidity(mixContext, mixBufferCount, mixCount, _inputReader))
|
||||
{
|
||||
return ResultCode.InvalidUpdateInfo;
|
||||
}
|
||||
|
||||
bool isMixContextDirty = false;
|
||||
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
for (int i = 0; i < parameterCount; i++)
|
||||
{
|
||||
MixParameter parameter = parameters[i];
|
||||
ref readonly MixParameter parameter = ref _inputReader.GetRefOrRefToCopy<MixParameter>(out _);
|
||||
|
||||
int mixId = i;
|
||||
|
||||
@ -454,7 +442,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
|
||||
if (mix.IsUsed)
|
||||
{
|
||||
isMixContextDirty |= mix.Update(mixContext.EdgeMatrix, ref parameter, effectContext, splitterContext, _behaviourContext);
|
||||
isMixContextDirty |= mix.Update(mixContext.EdgeMatrix, in parameter, effectContext, splitterContext, _behaviourContext);
|
||||
}
|
||||
}
|
||||
|
||||
@ -473,10 +461,12 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
}
|
||||
}
|
||||
|
||||
_inputReader.SetConsumed(initialInputConsumed + inputMixSize);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
private static void ResetSink(ref BaseSink sink, ref SinkInParameter parameter)
|
||||
private static void ResetSink(ref BaseSink sink, in SinkInParameter parameter)
|
||||
{
|
||||
sink.CleanUp();
|
||||
|
||||
@ -489,10 +479,8 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
};
|
||||
}
|
||||
|
||||
public ResultCode UpdateSinks(SinkContext context, Memory<MemoryPoolState> memoryPools)
|
||||
public ResultCode UpdateSinks(SinkContext context, PoolMapper mapper)
|
||||
{
|
||||
PoolMapper mapper = new(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled());
|
||||
|
||||
if (context.GetCount() * Unsafe.SizeOf<SinkInParameter>() != _inputHeader.SinksSize)
|
||||
{
|
||||
return ResultCode.InvalidUpdateInfo;
|
||||
@ -500,22 +488,20 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
|
||||
int initialOutputSize = _output.Length;
|
||||
|
||||
ReadOnlySpan<SinkInParameter> parameters = MemoryMarshal.Cast<byte, SinkInParameter>(_input[..(int)_inputHeader.SinksSize].Span);
|
||||
|
||||
_input = _input[(int)_inputHeader.SinksSize..];
|
||||
long initialInputConsumed = _inputReader.Consumed;
|
||||
|
||||
for (int i = 0; i < context.GetCount(); i++)
|
||||
{
|
||||
SinkInParameter parameter = parameters[i];
|
||||
ref readonly SinkInParameter parameter = ref _inputReader.GetRefOrRefToCopy<SinkInParameter>(out _);
|
||||
ref SinkOutStatus outStatus = ref SpanIOHelper.GetWriteRef<SinkOutStatus>(ref _output)[0];
|
||||
ref BaseSink sink = ref context.GetSink(i);
|
||||
|
||||
if (!sink.IsTypeValid(ref parameter))
|
||||
if (!sink.IsTypeValid(in parameter))
|
||||
{
|
||||
ResetSink(ref sink, ref parameter);
|
||||
ResetSink(ref sink, in parameter);
|
||||
}
|
||||
|
||||
sink.Update(out ErrorInfo updateErrorInfo, ref parameter, ref outStatus, mapper);
|
||||
sink.Update(out ErrorInfo updateErrorInfo, in parameter, ref outStatus, mapper);
|
||||
|
||||
if (updateErrorInfo.ErrorCode != ResultCode.Success)
|
||||
{
|
||||
@ -530,6 +516,8 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
|
||||
Debug.Assert((initialOutputSize - currentOutputSize) == OutputHeader.SinksSize);
|
||||
|
||||
_inputReader.SetConsumed(initialInputConsumed + _inputHeader.SinksSize);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
@ -540,7 +528,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
return ResultCode.InvalidUpdateInfo;
|
||||
}
|
||||
|
||||
PerformanceInParameter parameter = SpanIOHelper.Read<PerformanceInParameter>(ref _input);
|
||||
ref readonly PerformanceInParameter parameter = ref _inputReader.GetRefOrRefToCopy<PerformanceInParameter>(out _);
|
||||
|
||||
ref PerformanceOutStatus outStatus = ref SpanIOHelper.GetWriteRef<PerformanceOutStatus>(ref _output)[0];
|
||||
|
||||
@ -585,9 +573,9 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
public ResultCode CheckConsumedSize()
|
||||
public readonly ResultCode CheckConsumedSize()
|
||||
{
|
||||
int consumedInputSize = _inputOrigin.Length - _input.Length;
|
||||
long consumedInputSize = _inputReader.Consumed;
|
||||
int consumedOutputSize = _outputOrigin.Length - _output.Length;
|
||||
|
||||
if (consumedInputSize != _inputHeader.TotalSize)
|
||||
|
@ -254,7 +254,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
|
||||
/// </summary>
|
||||
/// <param name="parameter">The user parameter.</param>
|
||||
/// <returns>Return true, if the server voice information needs to be updated.</returns>
|
||||
private readonly bool ShouldUpdateParameters(ref VoiceInParameter parameter)
|
||||
private readonly bool ShouldUpdateParameters(in VoiceInParameter parameter)
|
||||
{
|
||||
if (DataSourceStateAddressInfo.CpuAddress == parameter.DataSourceStateAddress)
|
||||
{
|
||||
@ -273,7 +273,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
|
||||
/// <param name="parameter">The user parameter.</param>
|
||||
/// <param name="poolMapper">The mapper to use.</param>
|
||||
/// <param name="behaviourContext">The behaviour context.</param>
|
||||
public void UpdateParameters(out ErrorInfo outErrorInfo, ref VoiceInParameter parameter, ref PoolMapper poolMapper, ref BehaviourContext behaviourContext)
|
||||
public void UpdateParameters(out ErrorInfo outErrorInfo, in VoiceInParameter parameter, PoolMapper poolMapper, ref BehaviourContext behaviourContext)
|
||||
{
|
||||
InUse = parameter.InUse;
|
||||
Id = parameter.Id;
|
||||
@ -326,7 +326,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
|
||||
VoiceDropFlag = false;
|
||||
}
|
||||
|
||||
if (ShouldUpdateParameters(ref parameter))
|
||||
if (ShouldUpdateParameters(in parameter))
|
||||
{
|
||||
DataSourceStateUnmapped = !poolMapper.TryAttachBuffer(out outErrorInfo, ref DataSourceStateAddressInfo, parameter.DataSourceStateAddress, parameter.DataSourceStateSize);
|
||||
}
|
||||
@ -380,7 +380,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
|
||||
/// <param name="outStatus">The given user output.</param>
|
||||
/// <param name="parameter">The user parameter.</param>
|
||||
/// <param name="voiceUpdateStates">The voice states associated to the <see cref="VoiceState"/>.</param>
|
||||
public void WriteOutStatus(ref VoiceOutStatus outStatus, ref VoiceInParameter parameter, ReadOnlySpan<Memory<VoiceUpdateState>> voiceUpdateStates)
|
||||
public void WriteOutStatus(ref VoiceOutStatus outStatus, in VoiceInParameter parameter, ReadOnlySpan<Memory<VoiceUpdateState>> voiceUpdateStates)
|
||||
{
|
||||
#if DEBUG
|
||||
// Sanity check in debug mode of the internal state
|
||||
@ -426,7 +426,12 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
|
||||
/// <param name="voiceUpdateStates">The voice states associated to the <see cref="VoiceState"/>.</param>
|
||||
/// <param name="mapper">The mapper to use.</param>
|
||||
/// <param name="behaviourContext">The behaviour context.</param>
|
||||
public void UpdateWaveBuffers(out ErrorInfo[] errorInfos, ref VoiceInParameter parameter, ReadOnlySpan<Memory<VoiceUpdateState>> voiceUpdateStates, ref PoolMapper mapper, ref BehaviourContext behaviourContext)
|
||||
public void UpdateWaveBuffers(
|
||||
out ErrorInfo[] errorInfos,
|
||||
in VoiceInParameter parameter,
|
||||
ReadOnlySpan<Memory<VoiceUpdateState>> voiceUpdateStates,
|
||||
PoolMapper mapper,
|
||||
ref BehaviourContext behaviourContext)
|
||||
{
|
||||
errorInfos = new ErrorInfo[Constants.VoiceWaveBufferCount * 2];
|
||||
|
||||
@ -444,7 +449,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
|
||||
|
||||
for (int i = 0; i < Constants.VoiceWaveBufferCount; i++)
|
||||
{
|
||||
UpdateWaveBuffer(errorInfos.AsSpan(i * 2, 2), ref WaveBuffers[i], ref parameter.WaveBuffers[i], parameter.SampleFormat, voiceUpdateState.IsWaveBufferValid[i], ref mapper, ref behaviourContext);
|
||||
UpdateWaveBuffer(errorInfos.AsSpan(i * 2, 2), ref WaveBuffers[i], ref parameter.WaveBuffers[i], parameter.SampleFormat, voiceUpdateState.IsWaveBufferValid[i], mapper, ref behaviourContext);
|
||||
}
|
||||
}
|
||||
|
||||
@ -458,7 +463,14 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
|
||||
/// <param name="isValid">If set to true, the server side wavebuffer is considered valid.</param>
|
||||
/// <param name="mapper">The mapper to use.</param>
|
||||
/// <param name="behaviourContext">The behaviour context.</param>
|
||||
private void UpdateWaveBuffer(Span<ErrorInfo> errorInfos, ref WaveBuffer waveBuffer, ref WaveBufferInternal inputWaveBuffer, SampleFormat sampleFormat, bool isValid, ref PoolMapper mapper, ref BehaviourContext behaviourContext)
|
||||
private void UpdateWaveBuffer(
|
||||
Span<ErrorInfo> errorInfos,
|
||||
ref WaveBuffer waveBuffer,
|
||||
ref WaveBufferInternal inputWaveBuffer,
|
||||
SampleFormat sampleFormat,
|
||||
bool isValid,
|
||||
PoolMapper mapper,
|
||||
ref BehaviourContext behaviourContext)
|
||||
{
|
||||
if (!isValid && waveBuffer.IsSendToAudioProcessor && waveBuffer.BufferAddressInfo.CpuAddress != 0)
|
||||
{
|
||||
|
@ -1,656 +0,0 @@
|
||||
{
|
||||
"Language": "简体中文",
|
||||
"MenuBarFileOpenApplet": "打开小程序",
|
||||
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "打开独立的 Mii 小程序",
|
||||
"SettingsTabInputDirectMouseAccess": "直通鼠标操作",
|
||||
"SettingsTabSystemMemoryManagerMode": "内存管理模式:",
|
||||
"SettingsTabSystemMemoryManagerModeSoftware": "软件",
|
||||
"SettingsTabSystemMemoryManagerModeHost": "本机 (快速)",
|
||||
"SettingsTabSystemMemoryManagerModeHostUnchecked": "跳过检查的本机 (最快)",
|
||||
"SettingsTabSystemUseHypervisor": "使用 Hypervisor 虚拟化",
|
||||
"MenuBarFile": "文件",
|
||||
"MenuBarFileOpenFromFile": "加载文件",
|
||||
"MenuBarFileOpenUnpacked": "加载解包后的游戏",
|
||||
"MenuBarFileOpenEmuFolder": "打开 Ryujinx 文件夹",
|
||||
"MenuBarFileOpenLogsFolder": "打开日志文件夹",
|
||||
"MenuBarFileExit": "退出",
|
||||
"MenuBarOptions": "选项",
|
||||
"MenuBarOptionsToggleFullscreen": "切换全屏",
|
||||
"MenuBarOptionsStartGamesInFullscreen": "全屏模式启动游戏",
|
||||
"MenuBarOptionsStopEmulation": "停止模拟",
|
||||
"MenuBarOptionsSettings": "设置",
|
||||
"MenuBarOptionsManageUserProfiles": "管理用户账户",
|
||||
"MenuBarActions": "操作",
|
||||
"MenuBarOptionsSimulateWakeUpMessage": "模拟唤醒消息",
|
||||
"MenuBarActionsScanAmiibo": "扫描 Amiibo",
|
||||
"MenuBarTools": "工具",
|
||||
"MenuBarToolsInstallFirmware": "安装固件",
|
||||
"MenuBarFileToolsInstallFirmwareFromFile": "从 XCI 或 ZIP 安装固件",
|
||||
"MenuBarFileToolsInstallFirmwareFromDirectory": "从文件夹安装固件",
|
||||
"MenuBarToolsManageFileTypes": "管理文件扩展名",
|
||||
"MenuBarToolsInstallFileTypes": "关联文件扩展名",
|
||||
"MenuBarToolsUninstallFileTypes": "取消关联扩展名",
|
||||
"MenuBarHelp": "帮助",
|
||||
"MenuBarHelpCheckForUpdates": "检查更新",
|
||||
"MenuBarHelpAbout": "关于",
|
||||
"MenuSearch": "搜索…",
|
||||
"GameListHeaderFavorite": "收藏",
|
||||
"GameListHeaderIcon": "图标",
|
||||
"GameListHeaderApplication": "名称",
|
||||
"GameListHeaderDeveloper": "制作商",
|
||||
"GameListHeaderVersion": "版本",
|
||||
"GameListHeaderTimePlayed": "游玩时长",
|
||||
"GameListHeaderLastPlayed": "最近游玩",
|
||||
"GameListHeaderFileExtension": "扩展名",
|
||||
"GameListHeaderFileSize": "大小",
|
||||
"GameListHeaderPath": "路径",
|
||||
"GameListContextMenuOpenUserSaveDirectory": "打开用户存档目录",
|
||||
"GameListContextMenuOpenUserSaveDirectoryToolTip": "打开储存游戏存档的目录",
|
||||
"GameListContextMenuOpenDeviceSaveDirectory": "打开系统目录",
|
||||
"GameListContextMenuOpenDeviceSaveDirectoryToolTip": "打开包含游戏系统设置的目录",
|
||||
"GameListContextMenuOpenBcatSaveDirectory": "打开 BCAT 目录",
|
||||
"GameListContextMenuOpenBcatSaveDirectoryToolTip": "打开包含游戏 BCAT 数据的目录",
|
||||
"GameListContextMenuManageTitleUpdates": "管理游戏更新",
|
||||
"GameListContextMenuManageTitleUpdatesToolTip": "打开更新管理器",
|
||||
"GameListContextMenuManageDlc": "管理 DLC",
|
||||
"GameListContextMenuManageDlcToolTip": "打开 DLC 管理窗口",
|
||||
"GameListContextMenuOpenModsDirectory": "打开 MOD 目录",
|
||||
"GameListContextMenuOpenModsDirectoryToolTip": "打开存放游戏 MOD 的目录",
|
||||
"GameListContextMenuCacheManagement": "缓存管理",
|
||||
"GameListContextMenuCacheManagementPurgePptc": "清除已编译的 PPTC 文件",
|
||||
"GameListContextMenuCacheManagementPurgePptcToolTip": "仅删除 PPTC 转换后的文件,下次打开游戏时将根据 .info 文件重新生成 PPTC 文件。\n如想彻底清除缓存,请进入目录把 .info 文件一并删除",
|
||||
"GameListContextMenuCacheManagementPurgeShaderCache": "清除着色器缓存",
|
||||
"GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "删除游戏的着色器缓存",
|
||||
"GameListContextMenuCacheManagementOpenPptcDirectory": "打开 PPTC 目录",
|
||||
"GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "打开包含游戏 PPTC 缓存的目录",
|
||||
"GameListContextMenuCacheManagementOpenShaderCacheDirectory": "打开着色器缓存目录",
|
||||
"GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "打开包含游戏着色器缓存的目录",
|
||||
"GameListContextMenuExtractData": "提取数据",
|
||||
"GameListContextMenuExtractDataExeFS": "ExeFS",
|
||||
"GameListContextMenuExtractDataExeFSToolTip": "从游戏的当前状态中提取 ExeFS 分区 (包括更新)",
|
||||
"GameListContextMenuExtractDataRomFS": "RomFS",
|
||||
"GameListContextMenuExtractDataRomFSToolTip": "从游戏的当前状态中提取 RomFS 分区 (包括更新)",
|
||||
"GameListContextMenuExtractDataLogo": "图标",
|
||||
"GameListContextMenuExtractDataLogoToolTip": "从游戏的当前状态中提取图标 (包括更新)",
|
||||
"StatusBarGamesLoaded": "{0}/{1} 游戏加载完成",
|
||||
"StatusBarSystemVersion": "系统版本:{0}",
|
||||
"LinuxVmMaxMapCountDialogTitle": "检测到内存映射的限制过低",
|
||||
"LinuxVmMaxMapCountDialogTextPrimary": "你想要将 vm.max_map_count 的值增加到 {0} 吗",
|
||||
"LinuxVmMaxMapCountDialogTextSecondary": "有些游戏可能会试图创建超过当前允许的内存映射数量。当超过此限制时,Ryujinx会立即崩溃。",
|
||||
"LinuxVmMaxMapCountDialogButtonUntilRestart": "确定,关闭后重置",
|
||||
"LinuxVmMaxMapCountDialogButtonPersistent": "确定,永久保存",
|
||||
"LinuxVmMaxMapCountWarningTextPrimary": "内存映射的最大数量低于推荐值。",
|
||||
"LinuxVmMaxMapCountWarningTextSecondary": "vm.max_map_count ({0}) 的当前值小于 {1}。 有些游戏可能会试图创建超过当前允许的内存映射量。当大于此限制时,Ryujinx 会立即崩溃。\n\n你可以手动增加内存映射限制或者安装 pkexec,它可以辅助Ryujinx解决该问题。",
|
||||
"Settings": "设置",
|
||||
"SettingsTabGeneral": "用户界面",
|
||||
"SettingsTabGeneralGeneral": "常规",
|
||||
"SettingsTabGeneralEnableDiscordRichPresence": "启用 Discord 在线状态展示",
|
||||
"SettingsTabGeneralCheckUpdatesOnLaunch": "自动检查更新",
|
||||
"SettingsTabGeneralShowConfirmExitDialog": "显示 \"确认退出\" 对话框",
|
||||
"SettingsTabGeneralHideCursor": "隐藏鼠标指针:",
|
||||
"SettingsTabGeneralHideCursorNever": "从不",
|
||||
"SettingsTabGeneralHideCursorOnIdle": "自动隐藏",
|
||||
"SettingsTabGeneralHideCursorAlways": "始终",
|
||||
"SettingsTabGeneralGameDirectories": "游戏目录",
|
||||
"SettingsTabGeneralAdd": "添加",
|
||||
"SettingsTabGeneralRemove": "删除",
|
||||
"SettingsTabSystem": "系统",
|
||||
"SettingsTabSystemCore": "核心",
|
||||
"SettingsTabSystemSystemRegion": "系统区域:",
|
||||
"SettingsTabSystemSystemRegionJapan": "日本",
|
||||
"SettingsTabSystemSystemRegionUSA": "美国",
|
||||
"SettingsTabSystemSystemRegionEurope": "欧洲",
|
||||
"SettingsTabSystemSystemRegionAustralia": "澳大利亚",
|
||||
"SettingsTabSystemSystemRegionChina": "中国",
|
||||
"SettingsTabSystemSystemRegionKorea": "韩国",
|
||||
"SettingsTabSystemSystemRegionTaiwan": "台湾地区",
|
||||
"SettingsTabSystemSystemLanguage": "系统语言:",
|
||||
"SettingsTabSystemSystemLanguageJapanese": "日语",
|
||||
"SettingsTabSystemSystemLanguageAmericanEnglish": "美式英语",
|
||||
"SettingsTabSystemSystemLanguageFrench": "法语",
|
||||
"SettingsTabSystemSystemLanguageGerman": "德语",
|
||||
"SettingsTabSystemSystemLanguageItalian": "意大利语",
|
||||
"SettingsTabSystemSystemLanguageSpanish": "西班牙语",
|
||||
"SettingsTabSystemSystemLanguageChinese": "简体中文",
|
||||
"SettingsTabSystemSystemLanguageKorean": "韩语",
|
||||
"SettingsTabSystemSystemLanguageDutch": "荷兰语",
|
||||
"SettingsTabSystemSystemLanguagePortuguese": "葡萄牙语",
|
||||
"SettingsTabSystemSystemLanguageRussian": "俄语",
|
||||
"SettingsTabSystemSystemLanguageTaiwanese": "繁体中文(台湾)",
|
||||
"SettingsTabSystemSystemLanguageBritishEnglish": "英式英语",
|
||||
"SettingsTabSystemSystemLanguageCanadianFrench": "加拿大法语",
|
||||
"SettingsTabSystemSystemLanguageLatinAmericanSpanish": "拉美西班牙语",
|
||||
"SettingsTabSystemSystemLanguageSimplifiedChinese": "简体中文(推荐)",
|
||||
"SettingsTabSystemSystemLanguageTraditionalChinese": "繁体中文(推荐)",
|
||||
"SettingsTabSystemSystemTimeZone": "系统时区:",
|
||||
"SettingsTabSystemSystemTime": "系统时钟:",
|
||||
"SettingsTabSystemEnableVsync": "启用垂直同步",
|
||||
"SettingsTabSystemEnablePptc": "开启 PPTC 缓存",
|
||||
"SettingsTabSystemEnableFsIntegrityChecks": "文件系统完整性检查",
|
||||
"SettingsTabSystemAudioBackend": "音频后端:",
|
||||
"SettingsTabSystemAudioBackendDummy": "无",
|
||||
"SettingsTabSystemAudioBackendOpenAL": "OpenAL",
|
||||
"SettingsTabSystemAudioBackendSoundIO": "音频输入/输出",
|
||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||
"SettingsTabSystemHacks": "修正",
|
||||
"SettingsTabSystemHacksNote": "(会引起模拟器不稳定)",
|
||||
"SettingsTabSystemExpandDramSize": "使用开发机的内存布局",
|
||||
"SettingsTabSystemIgnoreMissingServices": "忽略缺失的服务",
|
||||
"SettingsTabGraphics": "图形",
|
||||
"SettingsTabGraphicsAPI": "图形 API",
|
||||
"SettingsTabGraphicsEnableShaderCache": "启用着色器缓存",
|
||||
"SettingsTabGraphicsAnisotropicFiltering": "各向异性过滤:",
|
||||
"SettingsTabGraphicsAnisotropicFilteringAuto": "自动",
|
||||
"SettingsTabGraphicsAnisotropicFiltering2x": "2x",
|
||||
"SettingsTabGraphicsAnisotropicFiltering4x": "4x",
|
||||
"SettingsTabGraphicsAnisotropicFiltering8x": "8x",
|
||||
"SettingsTabGraphicsAnisotropicFiltering16x": "16x",
|
||||
"SettingsTabGraphicsResolutionScale": "分辨率缩放:",
|
||||
"SettingsTabGraphicsResolutionScaleCustom": "自定义(不推荐)",
|
||||
"SettingsTabGraphicsResolutionScaleNative": "原生 (720p/1080p)",
|
||||
"SettingsTabGraphicsResolutionScale2x": "2x (1440p/2160p)",
|
||||
"SettingsTabGraphicsResolutionScale3x": "3x (2160p/3240p)",
|
||||
"SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p)",
|
||||
"SettingsTabGraphicsAspectRatio": "宽高比:",
|
||||
"SettingsTabGraphicsAspectRatio4x3": "4:3",
|
||||
"SettingsTabGraphicsAspectRatio16x9": "16:9",
|
||||
"SettingsTabGraphicsAspectRatio16x10": "16:10",
|
||||
"SettingsTabGraphicsAspectRatio21x9": "21:9",
|
||||
"SettingsTabGraphicsAspectRatio32x9": "32:9",
|
||||
"SettingsTabGraphicsAspectRatioStretch": "拉伸以适应窗口",
|
||||
"SettingsTabGraphicsDeveloperOptions": "开发者选项",
|
||||
"SettingsTabGraphicsShaderDumpPath": "图形着色器转储路径:",
|
||||
"SettingsTabLogging": "日志",
|
||||
"SettingsTabLoggingLogging": "日志",
|
||||
"SettingsTabLoggingEnableLoggingToFile": "保存日志为文件",
|
||||
"SettingsTabLoggingEnableStubLogs": "启用 Stub 日志",
|
||||
"SettingsTabLoggingEnableInfoLogs": "启用信息日志",
|
||||
"SettingsTabLoggingEnableWarningLogs": "启用警告日志",
|
||||
"SettingsTabLoggingEnableErrorLogs": "启用错误日志",
|
||||
"SettingsTabLoggingEnableTraceLogs": "启用跟踪日志",
|
||||
"SettingsTabLoggingEnableGuestLogs": "启用来宾日志",
|
||||
"SettingsTabLoggingEnableFsAccessLogs": "启用访问日志",
|
||||
"SettingsTabLoggingFsGlobalAccessLogMode": "全局访问日志模式:",
|
||||
"SettingsTabLoggingDeveloperOptions": "开发者选项",
|
||||
"SettingsTabLoggingDeveloperOptionsNote": "警告:会降低性能",
|
||||
"SettingsTabLoggingGraphicsBackendLogLevel": "图形后端日志级别:",
|
||||
"SettingsTabLoggingGraphicsBackendLogLevelNone": "无",
|
||||
"SettingsTabLoggingGraphicsBackendLogLevelError": "错误",
|
||||
"SettingsTabLoggingGraphicsBackendLogLevelPerformance": "减速",
|
||||
"SettingsTabLoggingGraphicsBackendLogLevelAll": "全部",
|
||||
"SettingsTabLoggingEnableDebugLogs": "启用调试日志",
|
||||
"SettingsTabInput": "输入",
|
||||
"SettingsTabInputEnableDockedMode": "主机模式",
|
||||
"SettingsTabInputDirectKeyboardAccess": "直通键盘控制",
|
||||
"SettingsButtonSave": "保存",
|
||||
"SettingsButtonClose": "取消",
|
||||
"SettingsButtonOk": "确定",
|
||||
"SettingsButtonCancel": "取消",
|
||||
"SettingsButtonApply": "应用",
|
||||
"ControllerSettingsPlayer": "玩家",
|
||||
"ControllerSettingsPlayer1": "玩家 1",
|
||||
"ControllerSettingsPlayer2": "玩家 2",
|
||||
"ControllerSettingsPlayer3": "玩家 3",
|
||||
"ControllerSettingsPlayer4": "玩家 4",
|
||||
"ControllerSettingsPlayer5": "玩家 5",
|
||||
"ControllerSettingsPlayer6": "玩家 6",
|
||||
"ControllerSettingsPlayer7": "玩家 7",
|
||||
"ControllerSettingsPlayer8": "玩家 8",
|
||||
"ControllerSettingsHandheld": "掌机模式",
|
||||
"ControllerSettingsInputDevice": "输入设备",
|
||||
"ControllerSettingsRefresh": "刷新",
|
||||
"ControllerSettingsDeviceDisabled": "关闭",
|
||||
"ControllerSettingsControllerType": "手柄类型",
|
||||
"ControllerSettingsControllerTypeHandheld": "掌机",
|
||||
"ControllerSettingsControllerTypeProController": "Pro 手柄",
|
||||
"ControllerSettingsControllerTypeJoyConPair": "JoyCon 组合",
|
||||
"ControllerSettingsControllerTypeJoyConLeft": "左 JoyCon",
|
||||
"ControllerSettingsControllerTypeJoyConRight": "右 JoyCon",
|
||||
"ControllerSettingsProfile": "预设",
|
||||
"ControllerSettingsProfileDefault": "默认布局",
|
||||
"ControllerSettingsLoad": "加载",
|
||||
"ControllerSettingsAdd": "新建",
|
||||
"ControllerSettingsRemove": "删除",
|
||||
"ControllerSettingsButtons": "按键",
|
||||
"ControllerSettingsButtonA": "A",
|
||||
"ControllerSettingsButtonB": "B",
|
||||
"ControllerSettingsButtonX": "X",
|
||||
"ControllerSettingsButtonY": "Y",
|
||||
"ControllerSettingsButtonPlus": "+",
|
||||
"ControllerSettingsButtonMinus": "-",
|
||||
"ControllerSettingsDPad": "方向键",
|
||||
"ControllerSettingsDPadUp": "上",
|
||||
"ControllerSettingsDPadDown": "下",
|
||||
"ControllerSettingsDPadLeft": "左",
|
||||
"ControllerSettingsDPadRight": "右",
|
||||
"ControllerSettingsStickButton": "按下摇杆",
|
||||
"ControllerSettingsStickUp": "上",
|
||||
"ControllerSettingsStickDown": "下",
|
||||
"ControllerSettingsStickLeft": "左",
|
||||
"ControllerSettingsStickRight": "右",
|
||||
"ControllerSettingsStickStick": "摇杆",
|
||||
"ControllerSettingsStickInvertXAxis": "反转 X 轴方向",
|
||||
"ControllerSettingsStickInvertYAxis": "反转 Y 轴方向",
|
||||
"ControllerSettingsStickDeadzone": "死区:",
|
||||
"ControllerSettingsLStick": "左摇杆",
|
||||
"ControllerSettingsRStick": "右摇杆",
|
||||
"ControllerSettingsTriggersLeft": "左扳机",
|
||||
"ControllerSettingsTriggersRight": "右扳机",
|
||||
"ControllerSettingsTriggersButtonsLeft": "左扳机键",
|
||||
"ControllerSettingsTriggersButtonsRight": "右扳机键",
|
||||
"ControllerSettingsTriggers": "扳机",
|
||||
"ControllerSettingsTriggerL": "L",
|
||||
"ControllerSettingsTriggerR": "R",
|
||||
"ControllerSettingsTriggerZL": "ZL",
|
||||
"ControllerSettingsTriggerZR": "ZR",
|
||||
"ControllerSettingsLeftSL": "SL",
|
||||
"ControllerSettingsLeftSR": "SR",
|
||||
"ControllerSettingsRightSL": "SL",
|
||||
"ControllerSettingsRightSR": "SR",
|
||||
"ControllerSettingsExtraButtonsLeft": "左背键",
|
||||
"ControllerSettingsExtraButtonsRight": "右背键",
|
||||
"ControllerSettingsMisc": "其他",
|
||||
"ControllerSettingsTriggerThreshold": "扳机阈值:",
|
||||
"ControllerSettingsMotion": "体感",
|
||||
"ControllerSettingsMotionUseCemuhookCompatibleMotion": "使用 CemuHook 体感协议",
|
||||
"ControllerSettingsMotionControllerSlot": "手柄:",
|
||||
"ControllerSettingsMotionMirrorInput": "镜像操作",
|
||||
"ControllerSettingsMotionRightJoyConSlot": "右JoyCon:",
|
||||
"ControllerSettingsMotionServerHost": "服务器Host:",
|
||||
"ControllerSettingsMotionGyroSensitivity": "陀螺仪敏感度:",
|
||||
"ControllerSettingsMotionGyroDeadzone": "陀螺仪死区:",
|
||||
"ControllerSettingsSave": "保存",
|
||||
"ControllerSettingsClose": "关闭",
|
||||
"UserProfilesSelectedUserProfile": "选择的用户账户:",
|
||||
"UserProfilesSaveProfileName": "保存名称",
|
||||
"UserProfilesChangeProfileImage": "更换头像",
|
||||
"UserProfilesAvailableUserProfiles": "现有账户:",
|
||||
"UserProfilesAddNewProfile": "新建账户",
|
||||
"UserProfilesDelete": "删除",
|
||||
"UserProfilesClose": "关闭",
|
||||
"ProfileNameSelectionWatermark": "选择昵称",
|
||||
"ProfileImageSelectionTitle": "选择头像",
|
||||
"ProfileImageSelectionHeader": "选择合适的头像图片",
|
||||
"ProfileImageSelectionNote": "您可以导入自定义头像,或从系统中选择头像",
|
||||
"ProfileImageSelectionImportImage": "导入图像文件",
|
||||
"ProfileImageSelectionSelectAvatar": "选择系统头像",
|
||||
"InputDialogTitle": "输入对话框",
|
||||
"InputDialogOk": "完成",
|
||||
"InputDialogCancel": "取消",
|
||||
"InputDialogAddNewProfileTitle": "选择用户名称",
|
||||
"InputDialogAddNewProfileHeader": "请输入账户名称",
|
||||
"InputDialogAddNewProfileSubtext": "(最大长度:{0})",
|
||||
"AvatarChoose": "选择头像",
|
||||
"AvatarSetBackgroundColor": "设置背景色",
|
||||
"AvatarClose": "关闭",
|
||||
"ControllerSettingsLoadProfileToolTip": "加载预设",
|
||||
"ControllerSettingsAddProfileToolTip": "新增预设",
|
||||
"ControllerSettingsRemoveProfileToolTip": "删除预设",
|
||||
"ControllerSettingsSaveProfileToolTip": "保存预设",
|
||||
"MenuBarFileToolsTakeScreenshot": "保存截图",
|
||||
"MenuBarFileToolsHideUi": "隐藏界面",
|
||||
"GameListContextMenuRunApplication": "运行应用",
|
||||
"GameListContextMenuToggleFavorite": "收藏",
|
||||
"GameListContextMenuToggleFavoriteToolTip": "标记喜爱的游戏",
|
||||
"SettingsTabGeneralTheme": "主题",
|
||||
"SettingsTabGeneralThemeCustomTheme": "自选主题路径",
|
||||
"SettingsTabGeneralThemeBaseStyle": "主题色调",
|
||||
"SettingsTabGeneralThemeBaseStyleDark": "暗黑",
|
||||
"SettingsTabGeneralThemeBaseStyleLight": "浅色",
|
||||
"SettingsTabGeneralThemeEnableCustomTheme": "使用自选主题界面",
|
||||
"ButtonBrowse": "浏览",
|
||||
"ControllerSettingsConfigureGeneral": "配置",
|
||||
"ControllerSettingsRumble": "震动",
|
||||
"ControllerSettingsRumbleStrongMultiplier": "强震动幅度",
|
||||
"ControllerSettingsRumbleWeakMultiplier": "弱震动幅度",
|
||||
"DialogMessageSaveNotAvailableMessage": "没有{0} [{1:x16}]的游戏存档",
|
||||
"DialogMessageSaveNotAvailableCreateSaveMessage": "是否创建该游戏的存档文件夹?",
|
||||
"DialogConfirmationTitle": "Ryujinx - 设置",
|
||||
"DialogUpdaterTitle": "Ryujinx - 更新",
|
||||
"DialogErrorTitle": "Ryujinx - 错误",
|
||||
"DialogWarningTitle": "Ryujinx - 警告",
|
||||
"DialogExitTitle": "Ryujinx - 关闭",
|
||||
"DialogErrorMessage": "Ryujinx 发生错误",
|
||||
"DialogExitMessage": "是否关闭 Ryujinx?",
|
||||
"DialogExitSubMessage": "未保存的进度将会丢失!",
|
||||
"DialogMessageCreateSaveErrorMessage": "创建特定的存档时出错:{0}",
|
||||
"DialogMessageFindSaveErrorMessage": "查找特定的存档时出错:{0}",
|
||||
"FolderDialogExtractTitle": "选择要解压到的文件夹",
|
||||
"DialogNcaExtractionMessage": "提取 {1} 的 {0} 分区...",
|
||||
"DialogNcaExtractionTitle": "Ryujinx - NCA分区提取",
|
||||
"DialogNcaExtractionMainNcaNotFoundErrorMessage": "提取失败。所选文件中不含主NCA文件",
|
||||
"DialogNcaExtractionCheckLogErrorMessage": "提取失败。请查看日志文件获取详情。",
|
||||
"DialogNcaExtractionSuccessMessage": "提取成功。",
|
||||
"DialogUpdaterConvertFailedMessage": "无法转换当前 Ryujinx 版本。",
|
||||
"DialogUpdaterCancelUpdateMessage": "更新取消!",
|
||||
"DialogUpdaterAlreadyOnLatestVersionMessage": "您使用的 Ryujinx 是最新版本。",
|
||||
"DialogUpdaterFailedToGetVersionMessage": "尝试从 Github 获取版本信息时无效。\n可能由于 GitHub Actions 正在编译新版本。请过一会再试。",
|
||||
"DialogUpdaterConvertFailedGithubMessage": "无法转换从 Github 接收到的 Ryujinx 版本。",
|
||||
"DialogUpdaterDownloadingMessage": "下载新版本中...",
|
||||
"DialogUpdaterExtractionMessage": "正在提取更新...",
|
||||
"DialogUpdaterRenamingMessage": "正在删除旧文件...",
|
||||
"DialogUpdaterAddingFilesMessage": "安装更新中...",
|
||||
"DialogUpdaterCompleteMessage": "更新成功!",
|
||||
"DialogUpdaterRestartMessage": "立即重启 Ryujinx 完成更新?",
|
||||
"DialogUpdaterArchNotSupportedMessage": "您运行的系统架构不受支持!",
|
||||
"DialogUpdaterArchNotSupportedSubMessage": "(仅支持 x64 系统)",
|
||||
"DialogUpdaterNoInternetMessage": "没有连接到互联网",
|
||||
"DialogUpdaterNoInternetSubMessage": "请确保互联网连接正常。",
|
||||
"DialogUpdaterDirtyBuildMessage": "不能更新非官方版本的 Ryujinx!",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "如果希望使用受支持的版本,请您在 https://ryujinx.org/ 下载。",
|
||||
"DialogRestartRequiredMessage": "需要重启模拟器",
|
||||
"DialogThemeRestartMessage": "主题设置已保存。需要重新启动才能生效。",
|
||||
"DialogThemeRestartSubMessage": "您是否要重启?",
|
||||
"DialogFirmwareInstallEmbeddedMessage": "要安装游戏内置的固件吗?(固件 {0})",
|
||||
"DialogFirmwareInstallEmbeddedSuccessMessage": "未找到已安装的固件,但 Ryujinx 可以从现有的游戏安装固件{0}.\n模拟器现在可以运行。",
|
||||
"DialogFirmwareNoFirmwareInstalledMessage": "未安装固件",
|
||||
"DialogFirmwareInstalledMessage": "已安装固件 {0}",
|
||||
"DialogInstallFileTypesSuccessMessage": "关联文件类型成功!",
|
||||
"DialogInstallFileTypesErrorMessage": "关联文件类型失败。",
|
||||
"DialogUninstallFileTypesSuccessMessage": "成功解除文件类型关联!",
|
||||
"DialogUninstallFileTypesErrorMessage": "解除文件类型关联失败。",
|
||||
"DialogOpenSettingsWindowLabel": "打开设置窗口",
|
||||
"DialogControllerAppletTitle": "控制器小窗口",
|
||||
"DialogMessageDialogErrorExceptionMessage": "显示消息对话框时出错:{0}",
|
||||
"DialogSoftwareKeyboardErrorExceptionMessage": "显示软件键盘时出错:{0}",
|
||||
"DialogErrorAppletErrorExceptionMessage": "显示错误对话框时出错:{0}",
|
||||
"DialogUserErrorDialogMessage": "{0}: {1}",
|
||||
"DialogUserErrorDialogInfoMessage": "\n有关修复此错误的更多信息,可以遵循我们的设置指南。",
|
||||
"DialogUserErrorDialogTitle": "Ryujinx 错误 ({0})",
|
||||
"DialogAmiiboApiTitle": "Amiibo API",
|
||||
"DialogAmiiboApiFailFetchMessage": "从 API 获取信息时出错。",
|
||||
"DialogAmiiboApiConnectErrorMessage": "无法连接到 Amiibo API 服务器。服务器可能已关闭,或者您没有连接网络。",
|
||||
"DialogProfileInvalidProfileErrorMessage": "预设 {0} 与当前输入配置系统不兼容。",
|
||||
"DialogProfileDefaultProfileOverwriteErrorMessage": "默认预设不能被覆盖",
|
||||
"DialogProfileDeleteProfileTitle": "删除预设",
|
||||
"DialogProfileDeleteProfileMessage": "删除后不可恢复,确认删除吗?",
|
||||
"DialogWarning": "警告",
|
||||
"DialogPPTCDeletionMessage": "您即将删除:\n\n{0} 的 PPTC 缓存\n\n确定吗?",
|
||||
"DialogPPTCDeletionErrorMessage": "清除位于 {0} 的 PPTC 缓存时出错:{1}",
|
||||
"DialogShaderDeletionMessage": "您即将删除:\n\n{0} 的着色器缓存\n\n确定吗?",
|
||||
"DialogShaderDeletionErrorMessage": "清除位于 {0} 的着色器缓存时出错:{1}",
|
||||
"DialogRyujinxErrorMessage": "Ryujinx 遇到错误",
|
||||
"DialogInvalidTitleIdErrorMessage": "UI错误:所选游戏没有有效的标题ID",
|
||||
"DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "路径 {0} 找不到有效的系统固件。",
|
||||
"DialogFirmwareInstallerFirmwareInstallTitle": "固件 {0}",
|
||||
"DialogFirmwareInstallerFirmwareInstallMessage": "即将安装系统版本 {0} 。",
|
||||
"DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\n会替换当前系统版本 {0} 。",
|
||||
"DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\n是否确认继续?",
|
||||
"DialogFirmwareInstallerFirmwareInstallWaitMessage": "安装固件中...",
|
||||
"DialogFirmwareInstallerFirmwareInstallSuccessMessage": "成功安装系统版本 {0} 。",
|
||||
"DialogUserProfileDeletionWarningMessage": "删除后将没有可选择的用户账户",
|
||||
"DialogUserProfileDeletionConfirmMessage": "是否删除选择的账户",
|
||||
"DialogUserProfileUnsavedChangesTitle": "警告 - 未保存的更改",
|
||||
"DialogUserProfileUnsavedChangesMessage": "您为该用户做出的部分改动尚未保存。",
|
||||
"DialogUserProfileUnsavedChangesSubMessage": "是否舍弃这些改动?",
|
||||
"DialogControllerSettingsModifiedConfirmMessage": "目前的输入预设已更新",
|
||||
"DialogControllerSettingsModifiedConfirmSubMessage": "要保存吗?",
|
||||
"DialogLoadNcaErrorMessage": "{0}. 错误的文件:{1}",
|
||||
"DialogDlcNoDlcErrorMessage": "选择的文件不包含所选游戏的 DLC!",
|
||||
"DialogPerformanceCheckLoggingEnabledMessage": "您启用了跟踪日志,仅供开发人员使用。",
|
||||
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "为了获得最佳性能,建议禁用跟踪日志记录。您是否要立即禁用?",
|
||||
"DialogPerformanceCheckShaderDumpEnabledMessage": "您启用了着色器转储,仅供开发人员使用。",
|
||||
"DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "为了获得最佳性能,建议禁用着色器转储。您是否要立即禁用?",
|
||||
"DialogLoadAppGameAlreadyLoadedMessage": "已有游戏正在运行",
|
||||
"DialogLoadAppGameAlreadyLoadedSubMessage": "请停止模拟或关闭程序,再启动另一个游戏。",
|
||||
"DialogUpdateAddUpdateErrorMessage": "选择的文件不包含所选游戏的更新!",
|
||||
"DialogSettingsBackendThreadingWarningTitle": "警告 - 后端多线程",
|
||||
"DialogSettingsBackendThreadingWarningMessage": "改变此选项后必须重启 Ryujinx 才能生效。\n\n取决于您的硬件,可能需要手动禁用驱动面板中的线程优化。",
|
||||
"SettingsTabGraphicsFeaturesOptions": "功能",
|
||||
"SettingsTabGraphicsBackendMultithreading": "多线程图形后端:",
|
||||
"CommonAuto": "自动(推荐)",
|
||||
"CommonOff": "关闭",
|
||||
"CommonOn": "打开",
|
||||
"InputDialogYes": "是",
|
||||
"InputDialogNo": "否",
|
||||
"DialogProfileInvalidProfileNameErrorMessage": "文件名包含无效字符,请重试。",
|
||||
"MenuBarOptionsPauseEmulation": "暂停",
|
||||
"MenuBarOptionsResumeEmulation": "继续",
|
||||
"AboutUrlTooltipMessage": "在浏览器中打开 Ryujinx 官网。",
|
||||
"AboutDisclaimerMessage": "Ryujinx 以任何方式与 Nintendo™ 及其任何商业伙伴都没有关联",
|
||||
"AboutAmiiboDisclaimerMessage": "我们的 Amiibo 模拟使用了\nAmiiboAPI (www.amiiboapi.com) ",
|
||||
"AboutPatreonUrlTooltipMessage": "在浏览器中打开 Ryujinx 的 Patreon 赞助页。",
|
||||
"AboutGithubUrlTooltipMessage": "在浏览器中打开 Ryujinx 的 GitHub 代码库。",
|
||||
"AboutDiscordUrlTooltipMessage": "在浏览器中打开 Ryujinx 的 Discord 邀请链接。",
|
||||
"AboutTwitterUrlTooltipMessage": "在浏览器中打开 Ryujinx 的 Twitter 主页。",
|
||||
"AboutRyujinxAboutTitle": "关于:",
|
||||
"AboutRyujinxAboutContent": "Ryujinx 是一款 Nintendo Switch™ 模拟器。\n您可以在 Patreon 上赞助 Ryujinx。\n关注 Twitter 或 Discord 可以获取模拟器最新动态。\n如果您对开发感兴趣,欢迎来 GitHub 或 Discord 加入我们!",
|
||||
"AboutRyujinxMaintainersTitle": "由以下作者维护:",
|
||||
"AboutRyujinxMaintainersContentTooltipMessage": "在浏览器中打开贡献者页面",
|
||||
"AboutRyujinxSupprtersTitle": "感谢 Patreon 的赞助者:",
|
||||
"AmiiboSeriesLabel": "Amiibo 系列",
|
||||
"AmiiboCharacterLabel": "角色",
|
||||
"AmiiboScanButtonLabel": "扫描",
|
||||
"AmiiboOptionsShowAllLabel": "显示所有 Amiibo 系列",
|
||||
"AmiiboOptionsUsRandomTagLabel": "修复:使用随机标记的 UUID",
|
||||
"DlcManagerTableHeadingEnabledLabel": "启用",
|
||||
"DlcManagerTableHeadingTitleIdLabel": "游戏ID",
|
||||
"DlcManagerTableHeadingContainerPathLabel": "文件夹路径",
|
||||
"DlcManagerTableHeadingFullPathLabel": "完整路径",
|
||||
"DlcManagerRemoveAllButton": "全部删除",
|
||||
"DlcManagerEnableAllButton": "全部启用",
|
||||
"DlcManagerDisableAllButton": "全部禁用",
|
||||
"MenuBarOptionsChangeLanguage": "更改语言",
|
||||
"MenuBarShowFileTypes": "主页显示的文件类型",
|
||||
"CommonSort": "排序",
|
||||
"CommonShowNames": "显示名称",
|
||||
"CommonFavorite": "收藏",
|
||||
"OrderAscending": "从小到大",
|
||||
"OrderDescending": "从大到小",
|
||||
"SettingsTabGraphicsFeatures": "功能与增强",
|
||||
"ErrorWindowTitle": "错误窗口",
|
||||
"ToggleDiscordTooltip": "控制是否在 Discord 中显示您的游玩状态",
|
||||
"AddGameDirBoxTooltip": "输入要添加的游戏目录",
|
||||
"AddGameDirTooltip": "添加游戏目录到列表中",
|
||||
"RemoveGameDirTooltip": "移除选中的目录",
|
||||
"CustomThemeCheckTooltip": "使用自定义UI主题来更改模拟器的外观样式",
|
||||
"CustomThemePathTooltip": "自定义主题的目录",
|
||||
"CustomThemeBrowseTooltip": "查找自定义主题",
|
||||
"DockModeToggleTooltip": "启用 Switch 的主机模式。\n绝大多数游戏画质会提高,略微增加性能消耗。\n在掌机和主机模式切换的过程中,您可能需要重新设置手柄类型。",
|
||||
"DirectKeyboardTooltip": "开启 \"直连键盘访问(HID)支持\"\n(部分游戏可以使用您的键盘输入文字)",
|
||||
"DirectMouseTooltip": "开启 \"直连鼠标访问(HID)支持\"\n(部分游戏可以使用您的鼠标导航)",
|
||||
"RegionTooltip": "更改系统区域",
|
||||
"LanguageTooltip": "更改系统语言",
|
||||
"TimezoneTooltip": "更改系统时区",
|
||||
"TimeTooltip": "更改系统时钟",
|
||||
"VSyncToggleTooltip": "关闭后,小部分游戏可以超过60FPS帧率,以获得高帧率体验。\n但是可能出现软锁或读盘时间增加。\n如不确定,就请保持开启状态。",
|
||||
"PptcToggleTooltip": "缓存编译完成的游戏CPU指令。减少启动时间和卡顿,提高游戏响应速度。\n如不确定,就请保持开启状态。",
|
||||
"FsIntegrityToggleTooltip": "检查游戏文件内容的完整性。\n遇到损坏的文件则记录到日志文件,有助于排查错误。\n对性能没有影响。\n如不确定,就请保持开启状态。",
|
||||
"AudioBackendTooltip": "默认推荐SDL2,但每种音频后端对各类游戏兼容性不同,遇到音频问题可以尝试切换后端。",
|
||||
"MemoryManagerTooltip": "改变 Switch 内存映射到电脑内存的方式,会影响CPU性能消耗。",
|
||||
"MemoryManagerSoftwareTooltip": "使用软件内存页管理,最精确但是速度最慢。",
|
||||
"MemoryManagerHostTooltip": "直接映射内存页到电脑内存,使得即时编译效率更高。",
|
||||
"MemoryManagerUnsafeTooltip": "直接映射内存页,但不检查内存溢出,使得即时编译效率更高。\nRyujinx 可以访问任何位置的内存,因而相对不安全。\n此模式下只应运行您信任的游戏或软件(即官方游戏)。",
|
||||
"UseHypervisorTooltip": "使用 Hypervisor 虚拟机代替即时编译。在可用的情况下能大幅提高性能。但目前可能不稳定。",
|
||||
"DRamTooltip": "使用Switch开发机的内存布局。\n不会提高任何性能,某些高清纹理包或 4k 分辨率 MOD 可能需要此选项。\n如果不确定,请始终关闭该选项。",
|
||||
"IgnoreMissingServicesTooltip": "开启后,游戏会忽略未实现的系统服务,从而继续运行。\n少部分新发布的游戏由于使用新的未知系统服务,可能需要此选项来避免闪退。\n模拟器更新完善系统服务之后,则无需开启选项。\n如您的游戏已经正常运行,请保持此选项关闭。",
|
||||
"GraphicsBackendThreadingTooltip": "在第二个线程上执行图形后端命令。\n\n加速着色器编译,减少卡顿,提高 GPU 的性能。\n\n如果不确定,请设置为自动。",
|
||||
"GalThreadingTooltip": "在第二个线程上执行图形后端命令。\n\n加速着色器编译,减少卡顿,提高 GPU 的性能。\n\n如果不确定,请设置为自动。",
|
||||
"ShaderCacheToggleTooltip": "开启后,模拟器会保存编译完成的着色器到磁盘,减少游戏渲染新特效和场景时的卡顿。",
|
||||
"ResolutionScaleTooltip": "缩放渲染的分辨率",
|
||||
"ResolutionScaleEntryTooltip": "尽可能使用例如1.5的浮点倍数。非整数的倍率易引起 BUG。",
|
||||
"AnisotropyTooltip": "各向异性过滤等级。提高倾斜视角纹理的清晰度\n('自动'使用游戏默认的等级)",
|
||||
"AspectRatioTooltip": "渲染窗口的宽高比。",
|
||||
"ShaderDumpPathTooltip": "转储图形着色器的路径",
|
||||
"FileLogTooltip": "保存日志文件到硬盘。不会影响性能。",
|
||||
"StubLogTooltip": "在控制台中打印 stub 日志消息。不影响性能。",
|
||||
"InfoLogTooltip": "在控制台中打印信息日志消息。不影响性能。",
|
||||
"WarnLogTooltip": "在控制台中打印警告日志消息。不影响性能。",
|
||||
"ErrorLogTooltip": "打印控制台中的错误日志消息。不影响性能。",
|
||||
"TraceLogTooltip": "在控制台中打印跟踪日志消息。不影响性能。",
|
||||
"GuestLogTooltip": "在控制台中打印访客日志消息。不影响性能。",
|
||||
"FileAccessLogTooltip": "在控制台中打印文件访问日志信息。",
|
||||
"FSAccessLogModeTooltip": "启用访问日志输出到控制台。可能的模式为 0-3",
|
||||
"DeveloperOptionTooltip": "谨慎使用",
|
||||
"OpenGlLogLevel": "需要打开适当的日志等级",
|
||||
"DebugLogTooltip": "记录Debug消息",
|
||||
"LoadApplicationFileTooltip": "选择 Switch 支持的游戏格式并加载",
|
||||
"LoadApplicationFolderTooltip": "选择解包后的 Switch 游戏并加载",
|
||||
"OpenRyujinxFolderTooltip": "打开 Ryujinx 系统目录",
|
||||
"OpenRyujinxLogsTooltip": "打开日志存放的目录",
|
||||
"ExitTooltip": "关闭 Ryujinx",
|
||||
"OpenSettingsTooltip": "打开设置窗口",
|
||||
"OpenProfileManagerTooltip": "打开用户账户管理界面",
|
||||
"StopEmulationTooltip": "停止运行当前游戏并回到主界面",
|
||||
"CheckUpdatesTooltip": "检查 Ryujinx 新版本",
|
||||
"OpenAboutTooltip": "打开“关于”窗口",
|
||||
"GridSize": "网格尺寸",
|
||||
"GridSizeTooltip": "调整网格模式的大小",
|
||||
"SettingsTabSystemSystemLanguageBrazilianPortuguese": "巴西葡萄牙语",
|
||||
"AboutRyujinxContributorsButtonHeader": "查看所有参与者",
|
||||
"SettingsTabSystemAudioVolume": "音量:",
|
||||
"AudioVolumeTooltip": "调节音量",
|
||||
"SettingsTabSystemEnableInternetAccess": "允许网络访问/局域网模式",
|
||||
"EnableInternetAccessTooltip": "允许模拟的游戏进程访问互联网。\n当多个模拟器/真实的 Switch 连接到同一个局域网时,带有 LAN 模式的游戏可以相互通信。\n即使开启选项也无法访问 Nintendo 服务器。此外可能导致某些尝试联网的游戏崩溃。\n如果您不确定,请关闭该选项。",
|
||||
"GameListContextMenuManageCheatToolTip": "管理金手指",
|
||||
"GameListContextMenuManageCheat": "管理金手指",
|
||||
"ControllerSettingsStickRange": "范围:",
|
||||
"DialogStopEmulationTitle": "Ryujinx - 停止模拟",
|
||||
"DialogStopEmulationMessage": "是否确定停止模拟?",
|
||||
"SettingsTabCpu": "CPU",
|
||||
"SettingsTabAudio": "音频",
|
||||
"SettingsTabNetwork": "网络",
|
||||
"SettingsTabNetworkConnection": "网络连接",
|
||||
"SettingsTabCpuCache": "CPU 缓存",
|
||||
"SettingsTabCpuMemory": "CPU 内存",
|
||||
"DialogUpdaterFlatpakNotSupportedMessage": "请通过 FlatHub 更新 Ryujinx。",
|
||||
"UpdaterDisabledWarningTitle": "更新已禁用!",
|
||||
"GameListContextMenuOpenSdModsDirectory": "打开 Atmosphere MOD 目录",
|
||||
"GameListContextMenuOpenSdModsDirectoryToolTip": "打开适用于 Atmosphere 自制系统的 MOD 目录",
|
||||
"ControllerSettingsRotate90": "顺时针旋转 90°",
|
||||
"IconSize": "图标尺寸",
|
||||
"IconSizeTooltip": "更改游戏图标大小",
|
||||
"MenuBarOptionsShowConsole": "显示控制台",
|
||||
"ShaderCachePurgeError": "清除着色器缓存时出错:{0}: {1}",
|
||||
"UserErrorNoKeys": "找不到密钥",
|
||||
"UserErrorNoFirmware": "找不到固件",
|
||||
"UserErrorFirmwareParsingFailed": "固件解析错误",
|
||||
"UserErrorApplicationNotFound": "找不到应用程序",
|
||||
"UserErrorUnknown": "未知错误",
|
||||
"UserErrorUndefined": "未定义错误",
|
||||
"UserErrorNoKeysDescription": "Ryujinx 找不到 'prod.keys' 文件",
|
||||
"UserErrorNoFirmwareDescription": "Ryujinx 找不到任何已安装的固件",
|
||||
"UserErrorFirmwareParsingFailedDescription": "Ryujinx 无法解密选择的固件。这通常是由于使用了过旧的密钥。",
|
||||
"UserErrorApplicationNotFoundDescription": "Ryujinx 在选中路径找不到有效的应用程序。",
|
||||
"UserErrorUnknownDescription": "发生未知错误!",
|
||||
"UserErrorUndefinedDescription": "发生了未定义错误!此类错误不应出现,请联系开发人员!",
|
||||
"OpenSetupGuideMessage": "打开设置教程",
|
||||
"NoUpdate": "无更新",
|
||||
"TitleUpdateVersionLabel": "版本 {0}",
|
||||
"RyujinxInfo": "Ryujinx - 信息",
|
||||
"RyujinxConfirm": "Ryujinx - 确认",
|
||||
"FileDialogAllTypes": "全部类型",
|
||||
"Never": "从不",
|
||||
"SwkbdMinCharacters": "至少应为 {0} 个字长",
|
||||
"SwkbdMinRangeCharacters": "必须为 {0}-{1} 个字长",
|
||||
"SoftwareKeyboard": "软件键盘",
|
||||
"SoftwareKeyboardModeNumbersOnly": "只接受数字",
|
||||
"SoftwareKeyboardModeAlphabet": "只接受非中日韩文字",
|
||||
"SoftwareKeyboardModeASCII": "只接受 ASCII 符号",
|
||||
"DialogControllerAppletMessagePlayerRange": "游戏需要 {0} 个玩家并满足以下要求:\n\n手柄类型:{1}\n\n玩家类型:{2}\n\n{3} 请打开设置窗口,重新配置手柄输入;或者关闭返回。",
|
||||
"DialogControllerAppletMessage": "游戏需要刚好 {0} 个玩家并满足以下要求:\n\n手柄类型:{1}\n\n玩家类型:{2}\n\n{3} 请打开设置窗口,重新配置手柄输入;或者关闭返回。",
|
||||
"DialogControllerAppletDockModeSet": "目前处于主机模式,无法使用掌机操作方式",
|
||||
"UpdaterRenaming": "正在删除旧文件...",
|
||||
"UpdaterRenameFailed": "更新过程中无法重命名文件:{0}",
|
||||
"UpdaterAddingFiles": "安装更新中...",
|
||||
"UpdaterExtracting": "正在提取更新...",
|
||||
"UpdaterDownloading": "下载新版本中...",
|
||||
"Game": "游戏",
|
||||
"Docked": "主机模式",
|
||||
"Handheld": "掌机模式",
|
||||
"ConnectionError": "连接错误。",
|
||||
"AboutPageDeveloperListMore": "{0} 等开发者...",
|
||||
"ApiError": "API错误。",
|
||||
"LoadingHeading": "正在启动 {0}",
|
||||
"CompilingPPTC": "编译PPTC缓存中",
|
||||
"CompilingShaders": "编译着色器中",
|
||||
"AllKeyboards": "所有键盘",
|
||||
"OpenFileDialogTitle": "选择一个支持的文件以打开",
|
||||
"OpenFolderDialogTitle": "选择一个包含解包游戏的文件夹",
|
||||
"AllSupportedFormats": "所有支持的格式",
|
||||
"RyujinxUpdater": "Ryujinx 更新程序",
|
||||
"SettingsTabHotkeys": "快捷键",
|
||||
"SettingsTabHotkeysHotkeys": "键盘快捷键",
|
||||
"SettingsTabHotkeysToggleVsyncHotkey": "切换垂直同步:",
|
||||
"SettingsTabHotkeysScreenshotHotkey": "截屏:",
|
||||
"SettingsTabHotkeysShowUiHotkey": "隐藏 界面:",
|
||||
"SettingsTabHotkeysPauseHotkey": "暂停:",
|
||||
"SettingsTabHotkeysToggleMuteHotkey": "静音:",
|
||||
"ControllerMotionTitle": "体感操作设置",
|
||||
"ControllerRumbleTitle": "震动设置",
|
||||
"SettingsSelectThemeFileDialogTitle": "选择主题文件",
|
||||
"SettingsXamlThemeFile": "Xaml 主题文件",
|
||||
"AvatarWindowTitle": "管理账户 - 头像",
|
||||
"Amiibo": "Amiibo",
|
||||
"Unknown": "未知",
|
||||
"Usage": "扫描可获得",
|
||||
"Writable": "可写入",
|
||||
"SelectDlcDialogTitle": "选择 DLC 文件",
|
||||
"SelectUpdateDialogTitle": "选择更新文件",
|
||||
"UserProfileWindowTitle": "管理用户账户",
|
||||
"CheatWindowTitle": "金手指管理器",
|
||||
"DlcWindowTitle": "管理 {0} ({1}) 的 DLC",
|
||||
"UpdateWindowTitle": "游戏更新管理器",
|
||||
"CheatWindowHeading": "适用于 {0} [{1}] 的金手指",
|
||||
"BuildId": "游戏版本ID:",
|
||||
"DlcWindowHeading": "{0} 个适用于 {1} ({2}) 的 DLC",
|
||||
"UserProfilesEditProfile": "编辑选中账户",
|
||||
"Cancel": "取消",
|
||||
"Save": "保存",
|
||||
"Discard": "返回",
|
||||
"UserProfilesSetProfileImage": "选择头像",
|
||||
"UserProfileEmptyNameError": "必须输入名称",
|
||||
"UserProfileNoImageError": "请选择您的头像",
|
||||
"GameUpdateWindowHeading": "管理 {0} ({1}) 的更新",
|
||||
"SettingsTabHotkeysResScaleUpHotkey": "提高分辨率:",
|
||||
"SettingsTabHotkeysResScaleDownHotkey": "降低分辨率:",
|
||||
"UserProfilesName": "名称:",
|
||||
"UserProfilesUserId": "用户ID:",
|
||||
"SettingsTabGraphicsBackend": "图形后端",
|
||||
"SettingsTabGraphicsBackendTooltip": "显卡使用的图形后端",
|
||||
"SettingsEnableTextureRecompression": "启用纹理重压缩",
|
||||
"SettingsEnableTextureRecompressionTooltip": "压缩某些纹理以减少显存的使用。\n适合显存小于 4GiB 的 GPU 开启。\n如果您不确定,请保持此项关闭。",
|
||||
"SettingsTabGraphicsPreferredGpu": "首选 GPU",
|
||||
"SettingsTabGraphicsPreferredGpuTooltip": "选择 Vulkan API 使用的显卡。\n此选项不会影响 OpenGL API。\n如果您不确定,建议选择\"dGPU(独立显卡)\"。如果没有独立显卡,则无需改动此选项。",
|
||||
"SettingsAppRequiredRestartMessage": "Ryujinx 需要重启",
|
||||
"SettingsGpuBackendRestartMessage": "您修改了图形 API 或显卡设置。需要重新启动才能生效",
|
||||
"SettingsGpuBackendRestartSubMessage": "是否重启模拟器?",
|
||||
"RyujinxUpdaterMessage": "是否更新 Ryujinx 到最新的版本?",
|
||||
"SettingsTabHotkeysVolumeUpHotkey": "音量加:",
|
||||
"SettingsTabHotkeysVolumeDownHotkey": "音量减:",
|
||||
"SettingsEnableMacroHLE": "启用 HLE 宏",
|
||||
"SettingsEnableMacroHLETooltip": "GPU 宏代码的高级模拟。\n提高性能表现,但可能在某些游戏中引起图形错误。\n如果您不确定,请保持此项开启。",
|
||||
"SettingsEnableColorSpacePassthrough": "颜色空间穿透",
|
||||
"SettingsEnableColorSpacePassthroughTooltip": "指示 Vulkan 后端在不指定颜色空间的情况下传递颜色信息。对于具有宽色域显示器的用户来说,这可能会以颜色正确性为代价,产生更鲜艳的颜色。",
|
||||
"VolumeShort": "音量",
|
||||
"UserProfilesManageSaves": "管理存档",
|
||||
"DeleteUserSave": "确定删除这个游戏的存档吗?",
|
||||
"IrreversibleActionNote": "删除后不可恢复。",
|
||||
"SaveManagerHeading": "管理 {0} ({1}) 的存档",
|
||||
"SaveManagerTitle": "存档管理器",
|
||||
"Name": "名称",
|
||||
"Size": "大小",
|
||||
"Search": "搜索",
|
||||
"UserProfilesRecoverLostAccounts": "恢复丢失的账户",
|
||||
"Recover": "恢复",
|
||||
"UserProfilesRecoverHeading": "找到了这些用户的存档数据",
|
||||
"UserProfilesRecoverEmptyList": "没有可以恢复的配置文件",
|
||||
"GraphicsAATooltip": "将抗锯齿使用到游戏渲染中",
|
||||
"GraphicsAALabel": "抗锯齿:",
|
||||
"GraphicsScalingFilterLabel": "缩放过滤:",
|
||||
"GraphicsScalingFilterTooltip": "对帧缓冲区进行缩放",
|
||||
"GraphicsScalingFilterLevelLabel": "等级",
|
||||
"GraphicsScalingFilterLevelTooltip": "设置缩放过滤级别",
|
||||
"SmaaLow": "SMAA 低质量",
|
||||
"SmaaMedium": "SMAA 中质量",
|
||||
"SmaaHigh": "SMAA 高质量",
|
||||
"SmaaUltra": "SMAA 极致质量",
|
||||
"UserEditorTitle": "编辑用户",
|
||||
"UserEditorTitleCreate": "创建用户",
|
||||
"SettingsTabNetworkInterface": "网络接口:",
|
||||
"NetworkInterfaceTooltip": "用于局域网功能的网络接口",
|
||||
"NetworkInterfaceDefault": "默认",
|
||||
"PackagingShaders": "整合着色器中",
|
||||
"AboutChangelogButton": "在Github上查看更新日志",
|
||||
"AboutChangelogButtonTooltipMessage": "点击这里在您的默认浏览器中打开此版本的更新日志。"
|
||||
}
|
@ -1,656 +0,0 @@
|
||||
{
|
||||
"Language": "英文 (美國)",
|
||||
"MenuBarFileOpenApplet": "開啟 Applet 應用程序",
|
||||
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "開啟獨立的Mii修改器應用程序",
|
||||
"SettingsTabInputDirectMouseAccess": "滑鼠直接操作",
|
||||
"SettingsTabSystemMemoryManagerMode": "記憶體管理模式:",
|
||||
"SettingsTabSystemMemoryManagerModeSoftware": "軟體",
|
||||
"SettingsTabSystemMemoryManagerModeHost": "主機模式 (快速)",
|
||||
"SettingsTabSystemMemoryManagerModeHostUnchecked": "主機略過檢查模式 (最快, 但不安全)",
|
||||
"SettingsTabSystemUseHypervisor": "使用 Hypervisor",
|
||||
"MenuBarFile": "_檔案",
|
||||
"MenuBarFileOpenFromFile": "_載入檔案",
|
||||
"MenuBarFileOpenUnpacked": "載入_已解開封裝的遊戲",
|
||||
"MenuBarFileOpenEmuFolder": "開啟 Ryujinx 資料夾",
|
||||
"MenuBarFileOpenLogsFolder": "開啟日誌資料夾",
|
||||
"MenuBarFileExit": "_退出",
|
||||
"MenuBarOptions": "選項",
|
||||
"MenuBarOptionsToggleFullscreen": "切換全螢幕模式",
|
||||
"MenuBarOptionsStartGamesInFullscreen": "使用全螢幕模式啟動遊戲",
|
||||
"MenuBarOptionsStopEmulation": "停止模擬",
|
||||
"MenuBarOptionsSettings": "_設定",
|
||||
"MenuBarOptionsManageUserProfiles": "_管理使用者帳戶",
|
||||
"MenuBarActions": "_動作",
|
||||
"MenuBarOptionsSimulateWakeUpMessage": "模擬喚醒訊息",
|
||||
"MenuBarActionsScanAmiibo": "掃描 Amiibo",
|
||||
"MenuBarTools": "_工具",
|
||||
"MenuBarToolsInstallFirmware": "安裝韌體",
|
||||
"MenuBarFileToolsInstallFirmwareFromFile": "從 XCI 或 ZIP 安裝韌體",
|
||||
"MenuBarFileToolsInstallFirmwareFromDirectory": "從資料夾安裝韌體",
|
||||
"MenuBarToolsManageFileTypes": "管理檔案類型",
|
||||
"MenuBarToolsInstallFileTypes": "註冊檔案類型",
|
||||
"MenuBarToolsUninstallFileTypes": "取消註冊檔案類型",
|
||||
"MenuBarHelp": "幫助",
|
||||
"MenuBarHelpCheckForUpdates": "檢查更新",
|
||||
"MenuBarHelpAbout": "關於",
|
||||
"MenuSearch": "搜尋...",
|
||||
"GameListHeaderFavorite": "收藏",
|
||||
"GameListHeaderIcon": "圖示",
|
||||
"GameListHeaderApplication": "名稱",
|
||||
"GameListHeaderDeveloper": "開發人員",
|
||||
"GameListHeaderVersion": "版本",
|
||||
"GameListHeaderTimePlayed": "遊玩時數",
|
||||
"GameListHeaderLastPlayed": "最近遊玩",
|
||||
"GameListHeaderFileExtension": "副檔名",
|
||||
"GameListHeaderFileSize": "檔案大小",
|
||||
"GameListHeaderPath": "路徑",
|
||||
"GameListContextMenuOpenUserSaveDirectory": "開啟使用者存檔資料夾",
|
||||
"GameListContextMenuOpenUserSaveDirectoryToolTip": "開啟此遊戲的存檔資料夾",
|
||||
"GameListContextMenuOpenDeviceSaveDirectory": "開啟系統資料夾",
|
||||
"GameListContextMenuOpenDeviceSaveDirectoryToolTip": "開啟此遊戲的系統設定資料夾",
|
||||
"GameListContextMenuOpenBcatSaveDirectory": "開啟 BCAT 資料夾",
|
||||
"GameListContextMenuOpenBcatSaveDirectoryToolTip": "開啟此遊戲的 BCAT 資料夾\n",
|
||||
"GameListContextMenuManageTitleUpdates": "管理遊戲更新",
|
||||
"GameListContextMenuManageTitleUpdatesToolTip": "開啟遊戲更新管理視窗",
|
||||
"GameListContextMenuManageDlc": "管理 DLC",
|
||||
"GameListContextMenuManageDlcToolTip": "開啟 DLC 管理視窗",
|
||||
"GameListContextMenuOpenModsDirectory": "開啟模組資料夾",
|
||||
"GameListContextMenuOpenModsDirectoryToolTip": "開啟此遊戲的模組資料夾",
|
||||
"GameListContextMenuCacheManagement": "快取管理",
|
||||
"GameListContextMenuCacheManagementPurgePptc": "清除 PPTC 快取",
|
||||
"GameListContextMenuCacheManagementPurgePptcToolTip": "刪除遊戲的 PPTC 快取",
|
||||
"GameListContextMenuCacheManagementPurgeShaderCache": "清除著色器快取",
|
||||
"GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "刪除遊戲的著色器快取",
|
||||
"GameListContextMenuCacheManagementOpenPptcDirectory": "開啟 PPTC 資料夾",
|
||||
"GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "開啟此遊戲的 PPTC 快取資料夾",
|
||||
"GameListContextMenuCacheManagementOpenShaderCacheDirectory": "開啟著色器快取資料夾",
|
||||
"GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "開啟此遊戲的著色器快取資料夾",
|
||||
"GameListContextMenuExtractData": "提取資料",
|
||||
"GameListContextMenuExtractDataExeFS": "ExeFS",
|
||||
"GameListContextMenuExtractDataExeFSToolTip": "從遊戲的目前狀態中提取 ExeFS 分區(包含更新)",
|
||||
"GameListContextMenuExtractDataRomFS": "RomFS",
|
||||
"GameListContextMenuExtractDataRomFSToolTip": "從遊戲的目前狀態中提取 RomFS 分區(包含更新)",
|
||||
"GameListContextMenuExtractDataLogo": "圖示",
|
||||
"GameListContextMenuExtractDataLogoToolTip": "從遊戲的目前狀態中提取圖示(包含更新)",
|
||||
"StatusBarGamesLoaded": "{0}/{1} 遊戲載入完成",
|
||||
"StatusBarSystemVersion": "系統版本: {0}",
|
||||
"LinuxVmMaxMapCountDialogTitle": "檢測到映射的記憶體上限過低",
|
||||
"LinuxVmMaxMapCountDialogTextPrimary": "你願意增加 vm.max_map_count to {0} 的數值嗎?",
|
||||
"LinuxVmMaxMapCountDialogTextSecondary": "遊戲佔用的記憶體超出了映射的上限. Ryujinx的處理程序即將面臨崩潰.",
|
||||
"LinuxVmMaxMapCountDialogButtonUntilRestart": "碓定 (直至下一次重新啟動)",
|
||||
"LinuxVmMaxMapCountDialogButtonPersistent": "碓定 (永遠設定)",
|
||||
"LinuxVmMaxMapCountWarningTextPrimary": "映射記憶體的最大值少於目前建議的下限.",
|
||||
"LinuxVmMaxMapCountWarningTextSecondary": "目前 vm.max_map_count ({0}) 的數值少於 {1}. 遊戲佔用的記憶體超出了映射的上限. Ryujinx的處理程序即將面臨崩潰.\n\n你可能需要手動增加上限或安裝 pkexec, 這些都能協助Ryujinx完成此操作.",
|
||||
"Settings": "設定",
|
||||
"SettingsTabGeneral": "使用者介面",
|
||||
"SettingsTabGeneralGeneral": "一般",
|
||||
"SettingsTabGeneralEnableDiscordRichPresence": "啟用 Discord 動態狀態展示",
|
||||
"SettingsTabGeneralCheckUpdatesOnLaunch": "自動檢查更新",
|
||||
"SettingsTabGeneralShowConfirmExitDialog": "顯示「確認離開」對話框",
|
||||
"SettingsTabGeneralHideCursor": "隱藏滑鼠遊標:",
|
||||
"SettingsTabGeneralHideCursorNever": "永不",
|
||||
"SettingsTabGeneralHideCursorOnIdle": "自動隱藏滑鼠",
|
||||
"SettingsTabGeneralHideCursorAlways": "總是",
|
||||
"SettingsTabGeneralGameDirectories": "遊戲資料夾",
|
||||
"SettingsTabGeneralAdd": "新增",
|
||||
"SettingsTabGeneralRemove": "刪除",
|
||||
"SettingsTabSystem": "系統",
|
||||
"SettingsTabSystemCore": "核心",
|
||||
"SettingsTabSystemSystemRegion": "系統區域:",
|
||||
"SettingsTabSystemSystemRegionJapan": "日本",
|
||||
"SettingsTabSystemSystemRegionUSA": "美國",
|
||||
"SettingsTabSystemSystemRegionEurope": "歐洲",
|
||||
"SettingsTabSystemSystemRegionAustralia": "澳洲",
|
||||
"SettingsTabSystemSystemRegionChina": "中國",
|
||||
"SettingsTabSystemSystemRegionKorea": "韓國",
|
||||
"SettingsTabSystemSystemRegionTaiwan": "台灣",
|
||||
"SettingsTabSystemSystemLanguage": "系統語言:",
|
||||
"SettingsTabSystemSystemLanguageJapanese": "日文",
|
||||
"SettingsTabSystemSystemLanguageAmericanEnglish": "英文 (美國)",
|
||||
"SettingsTabSystemSystemLanguageFrench": "法文",
|
||||
"SettingsTabSystemSystemLanguageGerman": "德文",
|
||||
"SettingsTabSystemSystemLanguageItalian": "義大利文",
|
||||
"SettingsTabSystemSystemLanguageSpanish": "西班牙文",
|
||||
"SettingsTabSystemSystemLanguageChinese": "中文 (中國)",
|
||||
"SettingsTabSystemSystemLanguageKorean": "韓文",
|
||||
"SettingsTabSystemSystemLanguageDutch": "荷蘭文",
|
||||
"SettingsTabSystemSystemLanguagePortuguese": "葡萄牙文",
|
||||
"SettingsTabSystemSystemLanguageRussian": "俄文",
|
||||
"SettingsTabSystemSystemLanguageTaiwanese": "中文 (台灣)",
|
||||
"SettingsTabSystemSystemLanguageBritishEnglish": "英文 (英國)",
|
||||
"SettingsTabSystemSystemLanguageCanadianFrench": "加拿大法語",
|
||||
"SettingsTabSystemSystemLanguageLatinAmericanSpanish": "拉丁美洲西班牙文",
|
||||
"SettingsTabSystemSystemLanguageSimplifiedChinese": "簡體中文",
|
||||
"SettingsTabSystemSystemLanguageTraditionalChinese": "繁體中文",
|
||||
"SettingsTabSystemSystemTimeZone": "系統時區:",
|
||||
"SettingsTabSystemSystemTime": "系統時鐘:",
|
||||
"SettingsTabSystemEnableVsync": "垂直同步",
|
||||
"SettingsTabSystemEnablePptc": "啟用 PPTC 快取",
|
||||
"SettingsTabSystemEnableFsIntegrityChecks": "開啟檔案系統完整性檢查",
|
||||
"SettingsTabSystemAudioBackend": "音效處理後台架構:",
|
||||
"SettingsTabSystemAudioBackendDummy": "模擬",
|
||||
"SettingsTabSystemAudioBackendOpenAL": "OpenAL",
|
||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||
"SettingsTabSystemHacks": "修正",
|
||||
"SettingsTabSystemHacksNote": " (會引起模擬器不穩定)",
|
||||
"SettingsTabSystemExpandDramSize": "使用額外的記憶體佈局 (開發人員)",
|
||||
"SettingsTabSystemIgnoreMissingServices": "忽略缺少的服務",
|
||||
"SettingsTabGraphics": "圖像",
|
||||
"SettingsTabGraphicsAPI": "圖像處理應用程式介面",
|
||||
"SettingsTabGraphicsEnableShaderCache": "啟用著色器快取",
|
||||
"SettingsTabGraphicsAnisotropicFiltering": "各向異性過濾:",
|
||||
"SettingsTabGraphicsAnisotropicFilteringAuto": "自動",
|
||||
"SettingsTabGraphicsAnisotropicFiltering2x": "2 倍",
|
||||
"SettingsTabGraphicsAnisotropicFiltering4x": "4 倍",
|
||||
"SettingsTabGraphicsAnisotropicFiltering8x": "8 倍",
|
||||
"SettingsTabGraphicsAnisotropicFiltering16x": "16倍",
|
||||
"SettingsTabGraphicsResolutionScale": "解析度比例:",
|
||||
"SettingsTabGraphicsResolutionScaleCustom": "自訂 (不建議使用)",
|
||||
"SettingsTabGraphicsResolutionScaleNative": "原生 (720p/1080p)",
|
||||
"SettingsTabGraphicsResolutionScale2x": "2 倍 (1440p/2160p)",
|
||||
"SettingsTabGraphicsResolutionScale3x": "3 倍 (2160p/3240p)",
|
||||
"SettingsTabGraphicsResolutionScale4x": "4 倍 (2880p/4320p)",
|
||||
"SettingsTabGraphicsAspectRatio": "螢幕長寬比例:",
|
||||
"SettingsTabGraphicsAspectRatio4x3": "4:3",
|
||||
"SettingsTabGraphicsAspectRatio16x9": "16:9",
|
||||
"SettingsTabGraphicsAspectRatio16x10": "16:10",
|
||||
"SettingsTabGraphicsAspectRatio21x9": "21:9",
|
||||
"SettingsTabGraphicsAspectRatio32x9": "32:9",
|
||||
"SettingsTabGraphicsAspectRatioStretch": "伸展至螢幕大小",
|
||||
"SettingsTabGraphicsDeveloperOptions": "開發者選項",
|
||||
"SettingsTabGraphicsShaderDumpPath": "圖形著色器轉存路徑:",
|
||||
"SettingsTabLogging": "日誌",
|
||||
"SettingsTabLoggingLogging": "日誌",
|
||||
"SettingsTabLoggingEnableLoggingToFile": "儲存記錄日誌為檔案",
|
||||
"SettingsTabLoggingEnableStubLogs": "啟用 Stub 記錄",
|
||||
"SettingsTabLoggingEnableInfoLogs": "啟用資訊記錄",
|
||||
"SettingsTabLoggingEnableWarningLogs": "啟用警告記錄",
|
||||
"SettingsTabLoggingEnableErrorLogs": "啟用錯誤記錄",
|
||||
"SettingsTabLoggingEnableTraceLogs": "啟用追蹤記錄",
|
||||
"SettingsTabLoggingEnableGuestLogs": "啟用賓客記錄",
|
||||
"SettingsTabLoggingEnableFsAccessLogs": "啟用檔案存取記錄",
|
||||
"SettingsTabLoggingFsGlobalAccessLogMode": "記錄全域檔案存取模式:",
|
||||
"SettingsTabLoggingDeveloperOptions": "開發者選項",
|
||||
"SettingsTabLoggingDeveloperOptionsNote": "警告:此操作會降低效能",
|
||||
"SettingsTabLoggingGraphicsBackendLogLevel": "圖像處理後台記錄等級:",
|
||||
"SettingsTabLoggingGraphicsBackendLogLevelNone": "無",
|
||||
"SettingsTabLoggingGraphicsBackendLogLevelError": "錯誤",
|
||||
"SettingsTabLoggingGraphicsBackendLogLevelPerformance": "減速",
|
||||
"SettingsTabLoggingGraphicsBackendLogLevelAll": "全部",
|
||||
"SettingsTabLoggingEnableDebugLogs": "啟用除錯記錄",
|
||||
"SettingsTabInput": "輸入",
|
||||
"SettingsTabInputEnableDockedMode": "Docked 模式",
|
||||
"SettingsTabInputDirectKeyboardAccess": "鍵盤直接操作",
|
||||
"SettingsButtonSave": "儲存",
|
||||
"SettingsButtonClose": "關閉",
|
||||
"SettingsButtonOk": "確定",
|
||||
"SettingsButtonCancel": "取消",
|
||||
"SettingsButtonApply": "套用",
|
||||
"ControllerSettingsPlayer": "玩家",
|
||||
"ControllerSettingsPlayer1": "玩家 1",
|
||||
"ControllerSettingsPlayer2": "玩家 2",
|
||||
"ControllerSettingsPlayer3": "玩家 3",
|
||||
"ControllerSettingsPlayer4": "玩家 4",
|
||||
"ControllerSettingsPlayer5": "玩家 5",
|
||||
"ControllerSettingsPlayer6": "玩家 6",
|
||||
"ControllerSettingsPlayer7": "玩家 7",
|
||||
"ControllerSettingsPlayer8": "玩家 8",
|
||||
"ControllerSettingsHandheld": "掌機模式",
|
||||
"ControllerSettingsInputDevice": "輸入裝置",
|
||||
"ControllerSettingsRefresh": "更新",
|
||||
"ControllerSettingsDeviceDisabled": "關閉",
|
||||
"ControllerSettingsControllerType": "控制器類型",
|
||||
"ControllerSettingsControllerTypeHandheld": "掌機",
|
||||
"ControllerSettingsControllerTypeProController": "Nintendo Switch Pro控制器",
|
||||
"ControllerSettingsControllerTypeJoyConPair": "JoyCon",
|
||||
"ControllerSettingsControllerTypeJoyConLeft": "左 JoyCon",
|
||||
"ControllerSettingsControllerTypeJoyConRight": "右 JoyCon",
|
||||
"ControllerSettingsProfile": "配置檔案",
|
||||
"ControllerSettingsProfileDefault": "預設",
|
||||
"ControllerSettingsLoad": "載入",
|
||||
"ControllerSettingsAdd": "建立",
|
||||
"ControllerSettingsRemove": "刪除",
|
||||
"ControllerSettingsButtons": "按鍵",
|
||||
"ControllerSettingsButtonA": "A",
|
||||
"ControllerSettingsButtonB": "B",
|
||||
"ControllerSettingsButtonX": "X",
|
||||
"ControllerSettingsButtonY": "Y",
|
||||
"ControllerSettingsButtonPlus": "+",
|
||||
"ControllerSettingsButtonMinus": "-",
|
||||
"ControllerSettingsDPad": "方向鍵",
|
||||
"ControllerSettingsDPadUp": "上",
|
||||
"ControllerSettingsDPadDown": "下",
|
||||
"ControllerSettingsDPadLeft": "左",
|
||||
"ControllerSettingsDPadRight": "右",
|
||||
"ControllerSettingsStickButton": "按鍵",
|
||||
"ControllerSettingsStickUp": "上",
|
||||
"ControllerSettingsStickDown": "下",
|
||||
"ControllerSettingsStickLeft": "左",
|
||||
"ControllerSettingsStickRight": "右",
|
||||
"ControllerSettingsStickStick": "搖桿",
|
||||
"ControllerSettingsStickInvertXAxis": "搖桿左右反向",
|
||||
"ControllerSettingsStickInvertYAxis": "搖桿上下反向",
|
||||
"ControllerSettingsStickDeadzone": "盲區:",
|
||||
"ControllerSettingsLStick": "左搖桿",
|
||||
"ControllerSettingsRStick": "右搖桿",
|
||||
"ControllerSettingsTriggersLeft": "左 Triggers",
|
||||
"ControllerSettingsTriggersRight": "右 Triggers",
|
||||
"ControllerSettingsTriggersButtonsLeft": "左 Triggers 鍵",
|
||||
"ControllerSettingsTriggersButtonsRight": "右 Triggers 鍵",
|
||||
"ControllerSettingsTriggers": "板機",
|
||||
"ControllerSettingsTriggerL": "L",
|
||||
"ControllerSettingsTriggerR": "R",
|
||||
"ControllerSettingsTriggerZL": "ZL",
|
||||
"ControllerSettingsTriggerZR": "ZR",
|
||||
"ControllerSettingsLeftSL": "SL",
|
||||
"ControllerSettingsLeftSR": "SR",
|
||||
"ControllerSettingsRightSL": "SL",
|
||||
"ControllerSettingsRightSR": "SR",
|
||||
"ControllerSettingsExtraButtonsLeft": "左按鍵",
|
||||
"ControllerSettingsExtraButtonsRight": "右按鍵",
|
||||
"ControllerSettingsMisc": "其他",
|
||||
"ControllerSettingsTriggerThreshold": "Triggers 閾值:",
|
||||
"ControllerSettingsMotion": "傳感器",
|
||||
"ControllerSettingsMotionUseCemuhookCompatibleMotion": "使用 CemuHook 相容性傳感協定",
|
||||
"ControllerSettingsMotionControllerSlot": "控制器插槽:",
|
||||
"ControllerSettingsMotionMirrorInput": "鏡像輸入",
|
||||
"ControllerSettingsMotionRightJoyConSlot": "右 JoyCon:",
|
||||
"ControllerSettingsMotionServerHost": "伺服器IP地址:",
|
||||
"ControllerSettingsMotionGyroSensitivity": "陀螺儀敏感度:",
|
||||
"ControllerSettingsMotionGyroDeadzone": "陀螺儀盲區:",
|
||||
"ControllerSettingsSave": "儲存",
|
||||
"ControllerSettingsClose": "關閉",
|
||||
"UserProfilesSelectedUserProfile": "選擇使用者帳戶:",
|
||||
"UserProfilesSaveProfileName": "儲存帳戶名稱",
|
||||
"UserProfilesChangeProfileImage": "更換帳戶頭像",
|
||||
"UserProfilesAvailableUserProfiles": "現有的使用者帳戶:",
|
||||
"UserProfilesAddNewProfile": "建立帳戶",
|
||||
"UserProfilesDelete": "刪除",
|
||||
"UserProfilesClose": "關閉",
|
||||
"ProfileNameSelectionWatermark": "選擇一個暱稱",
|
||||
"ProfileImageSelectionTitle": "帳戶頭像選擇",
|
||||
"ProfileImageSelectionHeader": "選擇帳戶頭像",
|
||||
"ProfileImageSelectionNote": "你可以導入自訂頭像,或從系統中選擇頭像",
|
||||
"ProfileImageSelectionImportImage": "導入圖片檔案",
|
||||
"ProfileImageSelectionSelectAvatar": "選擇系統頭像",
|
||||
"InputDialogTitle": "輸入對話框",
|
||||
"InputDialogOk": "完成",
|
||||
"InputDialogCancel": "取消",
|
||||
"InputDialogAddNewProfileTitle": "選擇帳戶名稱",
|
||||
"InputDialogAddNewProfileHeader": "請輸入帳戶名稱",
|
||||
"InputDialogAddNewProfileSubtext": "(最大長度:{0})",
|
||||
"AvatarChoose": "選擇",
|
||||
"AvatarSetBackgroundColor": "設定背景顏色",
|
||||
"AvatarClose": "關閉",
|
||||
"ControllerSettingsLoadProfileToolTip": "載入配置檔案",
|
||||
"ControllerSettingsAddProfileToolTip": "新增配置檔案",
|
||||
"ControllerSettingsRemoveProfileToolTip": "刪除配置檔案",
|
||||
"ControllerSettingsSaveProfileToolTip": "儲存配置檔案",
|
||||
"MenuBarFileToolsTakeScreenshot": "儲存截圖",
|
||||
"MenuBarFileToolsHideUi": "隱藏使用者介面",
|
||||
"GameListContextMenuRunApplication": "執行程式",
|
||||
"GameListContextMenuToggleFavorite": "標記為收藏",
|
||||
"GameListContextMenuToggleFavoriteToolTip": "啟用或取消收藏標記",
|
||||
"SettingsTabGeneralTheme": "佈景主題",
|
||||
"SettingsTabGeneralThemeCustomTheme": "自訂佈景主題路徑",
|
||||
"SettingsTabGeneralThemeBaseStyle": "基本佈景主題式樣",
|
||||
"SettingsTabGeneralThemeBaseStyleDark": "深色模式",
|
||||
"SettingsTabGeneralThemeBaseStyleLight": "淺色模式",
|
||||
"SettingsTabGeneralThemeEnableCustomTheme": "使用自訂佈景主題",
|
||||
"ButtonBrowse": "瀏覽",
|
||||
"ControllerSettingsConfigureGeneral": "配置",
|
||||
"ControllerSettingsRumble": "震動",
|
||||
"ControllerSettingsRumbleStrongMultiplier": "強震動調節",
|
||||
"ControllerSettingsRumbleWeakMultiplier": "弱震動調節",
|
||||
"DialogMessageSaveNotAvailableMessage": "沒有{0} [{1:x16}]的遊戲存檔",
|
||||
"DialogMessageSaveNotAvailableCreateSaveMessage": "是否建立該遊戲的存檔資料夾?",
|
||||
"DialogConfirmationTitle": "Ryujinx - 設定",
|
||||
"DialogUpdaterTitle": "Ryujinx - 更新",
|
||||
"DialogErrorTitle": "Ryujinx - 錯誤",
|
||||
"DialogWarningTitle": "Ryujinx - 警告",
|
||||
"DialogExitTitle": "Ryujinx - 關閉",
|
||||
"DialogErrorMessage": "Ryujinx 遇到了錯誤",
|
||||
"DialogExitMessage": "你確定要關閉 Ryujinx 嗎?",
|
||||
"DialogExitSubMessage": "所有未儲存的資料將會遺失!",
|
||||
"DialogMessageCreateSaveErrorMessage": "建立特定的存檔時出現錯誤: {0}",
|
||||
"DialogMessageFindSaveErrorMessage": "查找特定的存檔時出現錯誤: {0}",
|
||||
"FolderDialogExtractTitle": "選擇要解壓到的資料夾",
|
||||
"DialogNcaExtractionMessage": "提取{1}的{0}分區...",
|
||||
"DialogNcaExtractionTitle": "Ryujinx - NCA分區提取",
|
||||
"DialogNcaExtractionMainNcaNotFoundErrorMessage": "提取失敗。所選檔案中不含主NCA檔案",
|
||||
"DialogNcaExtractionCheckLogErrorMessage": "提取失敗。請查看日誌檔案取得詳情。",
|
||||
"DialogNcaExtractionSuccessMessage": "提取成功。",
|
||||
"DialogUpdaterConvertFailedMessage": "無法轉換目前 Ryujinx 版本。",
|
||||
"DialogUpdaterCancelUpdateMessage": "更新取消!",
|
||||
"DialogUpdaterAlreadyOnLatestVersionMessage": "你使用的 Ryujinx 是最新版本。",
|
||||
"DialogUpdaterFailedToGetVersionMessage": "嘗試從 Github 取得版本訊息時失敗。可能是因為 GitHub Actions 正在編譯新版本。請於數分數後重試。",
|
||||
"DialogUpdaterConvertFailedGithubMessage": "無法轉換從 Github 接收到的 Ryujinx 版本。",
|
||||
"DialogUpdaterDownloadingMessage": "下載最新版本中...",
|
||||
"DialogUpdaterExtractionMessage": "正在提取更新...",
|
||||
"DialogUpdaterRenamingMessage": "正在刪除舊檔案...",
|
||||
"DialogUpdaterAddingFilesMessage": "安裝更新中...",
|
||||
"DialogUpdaterCompleteMessage": "更新成功!",
|
||||
"DialogUpdaterRestartMessage": "你確定要立即重新啟動 Ryujinx 嗎?",
|
||||
"DialogUpdaterArchNotSupportedMessage": "你執行的系統架構不被支援!",
|
||||
"DialogUpdaterArchNotSupportedSubMessage": "(僅支援 x64 系統)",
|
||||
"DialogUpdaterNoInternetMessage": "你沒有連接到網際網絡!",
|
||||
"DialogUpdaterNoInternetSubMessage": "請確保網際網絡連接正常!",
|
||||
"DialogUpdaterDirtyBuildMessage": "不能更新非官方版本的 Ryujinx!",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "如果你希望使用被受支援的Ryujinx版本,請你在官方網址 https://ryujinx.org/ 下載.",
|
||||
"DialogRestartRequiredMessage": "模擬器必須重新啟動",
|
||||
"DialogThemeRestartMessage": "佈景主題設定已儲存。需要重新啟動才能生效。",
|
||||
"DialogThemeRestartSubMessage": "你確定要現在重新啟動嗎?",
|
||||
"DialogFirmwareInstallEmbeddedMessage": "要安裝遊戲內建的韌體嗎?(韌體 {0})",
|
||||
"DialogFirmwareInstallEmbeddedSuccessMessage": "未找到已安裝的韌體,但 Ryujinx 可以從現有的遊戲安裝韌體{0}.\\n模擬器現在可以執行。",
|
||||
"DialogFirmwareNoFirmwareInstalledMessage": "未安裝韌體",
|
||||
"DialogFirmwareInstalledMessage": "已安裝韌體{0}",
|
||||
"DialogInstallFileTypesSuccessMessage": "成功註冊檔案類型!",
|
||||
"DialogInstallFileTypesErrorMessage": "註冊檔案類型失敗。",
|
||||
"DialogUninstallFileTypesSuccessMessage": "成功取消註冊檔案類型!",
|
||||
"DialogUninstallFileTypesErrorMessage": "取消註冊檔案類型失敗。",
|
||||
"DialogOpenSettingsWindowLabel": "開啟設定視窗",
|
||||
"DialogControllerAppletTitle": "控制器小視窗",
|
||||
"DialogMessageDialogErrorExceptionMessage": "顯示訊息對話框時出現錯誤: {0}",
|
||||
"DialogSoftwareKeyboardErrorExceptionMessage": "顯示軟體鍵盤時出現錯誤: {0}",
|
||||
"DialogErrorAppletErrorExceptionMessage": "顯示錯誤對話框時出現錯誤: {0}",
|
||||
"DialogUserErrorDialogMessage": "{0}: {1}",
|
||||
"DialogUserErrorDialogInfoMessage": "\n有關修復此錯誤的更多訊息,可以遵循我們的設定指南。",
|
||||
"DialogUserErrorDialogTitle": "Ryujinx 錯誤 ({0})",
|
||||
"DialogAmiiboApiTitle": "Amiibo 應用程式介面",
|
||||
"DialogAmiiboApiFailFetchMessage": "從 API 取得訊息時出錯。",
|
||||
"DialogAmiiboApiConnectErrorMessage": "無法連接到 Amiibo API 伺服器。伺服器可能已關閉,或你沒有連接到網際網路。",
|
||||
"DialogProfileInvalidProfileErrorMessage": "配置檔案 {0} 與目前輸入系統不相容。",
|
||||
"DialogProfileDefaultProfileOverwriteErrorMessage": "無法覆蓋預設的配置檔案",
|
||||
"DialogProfileDeleteProfileTitle": "刪除帳戶",
|
||||
"DialogProfileDeleteProfileMessage": "此操作不可撤銷, 您確定要繼續嗎?",
|
||||
"DialogWarning": "警告",
|
||||
"DialogPPTCDeletionMessage": "下一次重啟時將會重新建立以下遊戲的 PPTC 快取\n\n{0}\n\n你確定要繼續嗎?",
|
||||
"DialogPPTCDeletionErrorMessage": "清除位於{0}的 PPTC 快取時出錯: {1}",
|
||||
"DialogShaderDeletionMessage": "即將刪除以下遊戲的著色器快取:\n\n{0}\n\n你確定要繼續嗎?",
|
||||
"DialogShaderDeletionErrorMessage": "清除{0}的著色器快取時出現錯誤: {1}",
|
||||
"DialogRyujinxErrorMessage": "Ryujinx 遇到錯誤",
|
||||
"DialogInvalidTitleIdErrorMessage": "UI 錯誤:所選遊戲沒有有效的標題ID",
|
||||
"DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "路徑{0}找不到有效的系統韌體。",
|
||||
"DialogFirmwareInstallerFirmwareInstallTitle": "安裝韌體{0}",
|
||||
"DialogFirmwareInstallerFirmwareInstallMessage": "將安裝{0}版本的系統。",
|
||||
"DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\n這將替換目前系統版本{0}。",
|
||||
"DialogFirmwareInstallerFirmwareInstallConfirmMessage": "你確定要繼續嗎?",
|
||||
"DialogFirmwareInstallerFirmwareInstallWaitMessage": "安裝韌體中...",
|
||||
"DialogFirmwareInstallerFirmwareInstallSuccessMessage": "成功安裝系統版本{0}。",
|
||||
"DialogUserProfileDeletionWarningMessage": "刪除後將沒有可選擇的使用者帳戶",
|
||||
"DialogUserProfileDeletionConfirmMessage": "你確定要刪除選擇中的帳戶嗎?",
|
||||
"DialogUserProfileUnsavedChangesTitle": "警告 - 有未儲存的更改",
|
||||
"DialogUserProfileUnsavedChangesMessage": "你對此帳戶所做的更改尚未儲存.",
|
||||
"DialogUserProfileUnsavedChangesSubMessage": "你確定要捨棄更改嗎?",
|
||||
"DialogControllerSettingsModifiedConfirmMessage": "目前的輸入配置檔案已更新",
|
||||
"DialogControllerSettingsModifiedConfirmSubMessage": "你確定要儲存嗎?",
|
||||
"DialogLoadNcaErrorMessage": "{0}. 錯誤的檔案: {1}",
|
||||
"DialogDlcNoDlcErrorMessage": "選擇的檔案不包含所選遊戲的 DLC!",
|
||||
"DialogPerformanceCheckLoggingEnabledMessage": "你啟用了跟蹤記錄,它的設計僅限開發人員使用。",
|
||||
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "為了獲得最佳效能,建議停用追蹤記錄。你是否要立即停用?",
|
||||
"DialogPerformanceCheckShaderDumpEnabledMessage": "你啟用了著色器轉存,它的設計僅限開發人員使用。",
|
||||
"DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "為了獲得最佳效能,建議停用著色器轉存。你是否要立即停用?",
|
||||
"DialogLoadAppGameAlreadyLoadedMessage": "目前已載入此遊戲",
|
||||
"DialogLoadAppGameAlreadyLoadedSubMessage": "請停止模擬或關閉程式,再啟動另一個遊戲。",
|
||||
"DialogUpdateAddUpdateErrorMessage": "選擇的檔案不包含所選遊戲的更新!",
|
||||
"DialogSettingsBackendThreadingWarningTitle": "警告 - 後台多工執行中",
|
||||
"DialogSettingsBackendThreadingWarningMessage": "更改此選項後必須重啟 Ryujinx 才能生效。根據你的硬體,您開啟該選項時,可能需要手動停用驅動程式本身的GPU多執行緒。",
|
||||
"SettingsTabGraphicsFeaturesOptions": "功能",
|
||||
"SettingsTabGraphicsBackendMultithreading": "圖像處理後台多線程支援:",
|
||||
"CommonAuto": "自動(推薦)",
|
||||
"CommonOff": "關閉",
|
||||
"CommonOn": "打開",
|
||||
"InputDialogYes": "是",
|
||||
"InputDialogNo": "否",
|
||||
"DialogProfileInvalidProfileNameErrorMessage": "檔案名包含無效字元,請重試。",
|
||||
"MenuBarOptionsPauseEmulation": "暫停",
|
||||
"MenuBarOptionsResumeEmulation": "繼續",
|
||||
"AboutUrlTooltipMessage": "在瀏覽器中打開 Ryujinx 的官方網站。",
|
||||
"AboutDisclaimerMessage": "Ryujinx 與 Nintendo™ 並沒有任何關聯, 包括其合作伙伴, 及任何形式上。",
|
||||
"AboutAmiiboDisclaimerMessage": "我們的 Amiibo 模擬使用了\nAmiiboAPI (www.amiiboapi.com) ",
|
||||
"AboutPatreonUrlTooltipMessage": "在瀏覽器中打開 Ryujinx 的 Patreon 贊助網頁。",
|
||||
"AboutGithubUrlTooltipMessage": "在瀏覽器中打開 Ryujinx 的 GitHub 儲存庫。",
|
||||
"AboutDiscordUrlTooltipMessage": "在瀏覽器中打開 Ryujinx 的 Discord 伺服器邀請連結。",
|
||||
"AboutTwitterUrlTooltipMessage": "在瀏覽器中打開 Ryujinx 的 Twitter 首頁。",
|
||||
"AboutRyujinxAboutTitle": "關於:",
|
||||
"AboutRyujinxAboutContent": "Ryujinx 是 Nintendo Switch™ 的一款模擬器。\n懇請您在 Patreon 上贊助我們。\n關注 Twitter 或 Discord 可以取得我們的最新動態。\n如果您對開發本軟體感興趣,歡迎來 GitHub 和 Discord 加入我們!",
|
||||
"AboutRyujinxMaintainersTitle": "開發及維護名單:",
|
||||
"AboutRyujinxMaintainersContentTooltipMessage": "在瀏覽器中打開貢獻者的網頁",
|
||||
"AboutRyujinxSupprtersTitle": "Patreon 的贊助人:",
|
||||
"AmiiboSeriesLabel": "Amiibo 系列",
|
||||
"AmiiboCharacterLabel": "角色",
|
||||
"AmiiboScanButtonLabel": "掃描",
|
||||
"AmiiboOptionsShowAllLabel": "顯示所有 Amiibo",
|
||||
"AmiiboOptionsUsRandomTagLabel": "侵略:使用隨機標記的 Uuid 編碼",
|
||||
"DlcManagerTableHeadingEnabledLabel": "已啟用",
|
||||
"DlcManagerTableHeadingTitleIdLabel": "遊戲ID",
|
||||
"DlcManagerTableHeadingContainerPathLabel": "資料夾路徑",
|
||||
"DlcManagerTableHeadingFullPathLabel": "完整路徑",
|
||||
"DlcManagerRemoveAllButton": "全部刪除",
|
||||
"DlcManagerEnableAllButton": "啟用全部",
|
||||
"DlcManagerDisableAllButton": "停用全部",
|
||||
"MenuBarOptionsChangeLanguage": "更改語言",
|
||||
"MenuBarShowFileTypes": "顯示檔案類型",
|
||||
"CommonSort": "排序",
|
||||
"CommonShowNames": "顯示名稱",
|
||||
"CommonFavorite": "收藏",
|
||||
"OrderAscending": "從小到大",
|
||||
"OrderDescending": "從大到小",
|
||||
"SettingsTabGraphicsFeatures": "功能及優化",
|
||||
"ErrorWindowTitle": "錯誤視窗",
|
||||
"ToggleDiscordTooltip": "啟用或關閉 Discord 動態狀態展示",
|
||||
"AddGameDirBoxTooltip": "輸入要添加的遊戲資料夾",
|
||||
"AddGameDirTooltip": "添加遊戲資料夾到列表中",
|
||||
"RemoveGameDirTooltip": "移除選擇中的遊戲資料夾",
|
||||
"CustomThemeCheckTooltip": "啟用或關閉自訂佈景主題",
|
||||
"CustomThemePathTooltip": "自訂佈景主題的資料夾",
|
||||
"CustomThemeBrowseTooltip": "查找自訂佈景主題",
|
||||
"DockModeToggleTooltip": "是否開啟 Switch 的 Docked 模式",
|
||||
"DirectKeyboardTooltip": "支援鍵盤直接存取 (HID協定) . 可供給遊戲使用你的鍵盤作為輸入文字裝置.",
|
||||
"DirectMouseTooltip": "支援滑鼠直接存取 (HID協定) . 可供給遊戲使用你的滑鼠作為瞄準裝置.",
|
||||
"RegionTooltip": "更改系統區域",
|
||||
"LanguageTooltip": "更改系統語言",
|
||||
"TimezoneTooltip": "更改系統時區",
|
||||
"TimeTooltip": "更改系統時鐘",
|
||||
"VSyncToggleTooltip": "模擬遊戲主機垂直同步更新頻率. 重要地反映著遊戲本身的速度; 關閉它可能會令後使用動態更新率的遊戲速度過高, 或會引致載入錯誤等等.\n\n可在遊戲中利用自訂快速鍵開關此功能. 我們也建議使用快速鍵, 如果你計劃關上它.\n\n如果不確定請設定為\"開啟\".",
|
||||
"PptcToggleTooltip": "開啟以後減少遊戲啟動時間和卡頓",
|
||||
"FsIntegrityToggleTooltip": "是否檢查遊戲檔案內容的完整性",
|
||||
"AudioBackendTooltip": "更改音效處理後台架構.\n\n推薦使用SDL2架構, 而OpenAL及SoundIO架構用作後備. Dummy是沒有音效的.\n\n如果不確定請設定為\"SDL2\".",
|
||||
"MemoryManagerTooltip": "更改模擬器記憶體至電腦記憶體的映射和存取方式,極其影響CPU效能.\n\n如果不確定, 請設定為\"主機略過檢查模式\".",
|
||||
"MemoryManagerSoftwareTooltip": "使用軟體虛擬分頁表換算, 最精確但是速度最慢.",
|
||||
"MemoryManagerHostTooltip": "直接地映射模擬記憶體到電腦記憶體. 對 JIT 編譯和執行效率有顯著提升. ",
|
||||
"MemoryManagerUnsafeTooltip": "直接地映射模擬記憶體到電腦記憶體, 但是不檢查記憶體溢出. 由於 Ryujinx 的子程式可以存取任何位置的記憶體, 因而相對不安全. 故在此模式下只應執行你信任的遊戲或軟體.",
|
||||
"UseHypervisorTooltip": "使用 Hypervisor 代替 JIT。在本功能可用時就可以大幅增大效能,但目前狀態還不穩定。",
|
||||
"DRamTooltip": "利用可選擇性的記憶體模式來模擬Switch發展中型號.\n\n此選項只會對高畫質材質包或4K模組有用. 而這並不會提升效能. \n\n如果不確定請關閉本功能.",
|
||||
"IgnoreMissingServicesTooltip": "忽略某些未被實施的系統服務. 此功能有助於繞過當啟動遊戲時帶來的故障.\n\n如果不確定請關閉本功能。",
|
||||
"GraphicsBackendThreadingTooltip": "執行雙線程後台繪圖指令, 能夠減少著色器編譯斷續, 並提高GPU驅動效能, 即將它不支持多線程處理. 而對於多線程處理也有少量提升.\n\n如果你不確定請設定為\"自動\"",
|
||||
"GalThreadingTooltip": "執行雙線程後台繪圖指令.\n\n能夠加速著色器編譯及減少斷續, 並提高GPU驅動效能, 即將它不支持多線程處理. 而對於多線程處理也有少量提升.\n\n如果你不確定請設定為\"自動\"",
|
||||
"ShaderCacheToggleTooltip": "儲存著色器快取到硬碟,減少存取斷續。\n\n如果不確定請設定為\"開啟\"。",
|
||||
"ResolutionScaleTooltip": "解析度繪圖倍率",
|
||||
"ResolutionScaleEntryTooltip": "盡量使用如1.5的浮點倍數。非整數的倍率易引起錯誤",
|
||||
"AnisotropyTooltip": "各向異性過濾等級。提高傾斜視角材質的清晰度\n(選擇「自動」將使用遊戲預設指定的等級)",
|
||||
"AspectRatioTooltip": "模擬器視窗解析度的長寬比",
|
||||
"ShaderDumpPathTooltip": "圖形著色器轉存路徑",
|
||||
"FileLogTooltip": "是否儲存日誌檔案到硬碟",
|
||||
"StubLogTooltip": "在控制台顯示及記錄 Stub 訊息",
|
||||
"InfoLogTooltip": "在控制台顯示及記錄資訊訊息",
|
||||
"WarnLogTooltip": "在控制台顯示及記錄警告訊息\n",
|
||||
"ErrorLogTooltip": "在控制台顯示及記錄錯誤訊息",
|
||||
"TraceLogTooltip": "在控制台顯示及記錄追蹤訊息",
|
||||
"GuestLogTooltip": "在控制台顯示及記錄賓客訊息",
|
||||
"FileAccessLogTooltip": "在控制台顯示及記錄檔案存取訊息",
|
||||
"FSAccessLogModeTooltip": "在控制台顯示及記錄FS 存取訊息. 可選的模式是 0-3",
|
||||
"DeveloperOptionTooltip": "使用請謹慎",
|
||||
"OpenGlLogLevel": "需要打開適當的日誌等級",
|
||||
"DebugLogTooltip": "在控制台顯示及記錄除錯訊息.\n\n僅限於受訓的成員使用, 因為它很難理解而且令模擬的效能非常地差.\n",
|
||||
"LoadApplicationFileTooltip": "選擇 Switch 支援的遊戲格式並載入",
|
||||
"LoadApplicationFolderTooltip": "選擇解包後的 Switch 遊戲並載入",
|
||||
"OpenRyujinxFolderTooltip": "開啟 Ryujinx 系統資料夾",
|
||||
"OpenRyujinxLogsTooltip": "開啟存放日誌的資料夾",
|
||||
"ExitTooltip": "關閉 Ryujinx",
|
||||
"OpenSettingsTooltip": "開啟設定視窗",
|
||||
"OpenProfileManagerTooltip": "開啟使用者帳戶管理視窗",
|
||||
"StopEmulationTooltip": "停止執行目前遊戲並回到選擇界面",
|
||||
"CheckUpdatesTooltip": "檢查 Ryujinx 新版本",
|
||||
"OpenAboutTooltip": "開啟關於視窗",
|
||||
"GridSize": "網格尺寸",
|
||||
"GridSizeTooltip": "調整網格模式的大小",
|
||||
"SettingsTabSystemSystemLanguageBrazilianPortuguese": "巴西葡萄牙語",
|
||||
"AboutRyujinxContributorsButtonHeader": "查看所有參與者",
|
||||
"SettingsTabSystemAudioVolume": "音量:",
|
||||
"AudioVolumeTooltip": "調節音量",
|
||||
"SettingsTabSystemEnableInternetAccess": "啟用網路連接",
|
||||
"EnableInternetAccessTooltip": "開啟網路存取。此選項打開後,效果類似於 Switch 連接到網路的狀態。注意即使此選項關閉,應用程式偶爾也有可能連接到網路",
|
||||
"GameListContextMenuManageCheatToolTip": "管理金手指",
|
||||
"GameListContextMenuManageCheat": "管理金手指",
|
||||
"ControllerSettingsStickRange": "範圍:",
|
||||
"DialogStopEmulationTitle": "Ryujinx - 停止模擬",
|
||||
"DialogStopEmulationMessage": "你確定要停止模擬嗎?",
|
||||
"SettingsTabCpu": "處理器",
|
||||
"SettingsTabAudio": "音訊",
|
||||
"SettingsTabNetwork": "網路",
|
||||
"SettingsTabNetworkConnection": "網路連接",
|
||||
"SettingsTabCpuCache": "CPU 快取",
|
||||
"SettingsTabCpuMemory": "CPU 模式",
|
||||
"DialogUpdaterFlatpakNotSupportedMessage": "請透過 Flathub 更新 Ryujinx。",
|
||||
"UpdaterDisabledWarningTitle": "更新已停用!",
|
||||
"GameListContextMenuOpenSdModsDirectory": "開啟 Atmosphere 模組資料夾",
|
||||
"GameListContextMenuOpenSdModsDirectoryToolTip": "開啟此遊戲額外的SD記憶卡Atmosphere模組資料夾. 有用於包裝在真實主機上的模組.\n",
|
||||
"ControllerSettingsRotate90": "順時針旋轉 90°",
|
||||
"IconSize": "圖示尺寸",
|
||||
"IconSizeTooltip": "更改遊戲圖示大小",
|
||||
"MenuBarOptionsShowConsole": "顯示控制台",
|
||||
"ShaderCachePurgeError": "清除 {0} 著色器快取時出現錯誤: {1}",
|
||||
"UserErrorNoKeys": "找不到金鑰",
|
||||
"UserErrorNoFirmware": "找不到韌體",
|
||||
"UserErrorFirmwareParsingFailed": "韌體解析錯誤",
|
||||
"UserErrorApplicationNotFound": "找不到應用程式",
|
||||
"UserErrorUnknown": "未知錯誤",
|
||||
"UserErrorUndefined": "未定義錯誤",
|
||||
"UserErrorNoKeysDescription": "Ryujinx 找不到 『prod.keys』 檔案",
|
||||
"UserErrorNoFirmwareDescription": "Ryujinx 找不到任何已安裝的韌體",
|
||||
"UserErrorFirmwareParsingFailedDescription": "Ryujinx 無法解密選擇的韌體。這通常是由於金鑰過舊。",
|
||||
"UserErrorApplicationNotFoundDescription": "Ryujinx 在選中路徑找不到有效的應用程式。",
|
||||
"UserErrorUnknownDescription": "發生未知錯誤!",
|
||||
"UserErrorUndefinedDescription": "發生了未定義錯誤!此類錯誤不應出現,請聯絡開發人員!",
|
||||
"OpenSetupGuideMessage": "開啟設定教學",
|
||||
"NoUpdate": "沒有新版本",
|
||||
"TitleUpdateVersionLabel": "版本 {0} - {1}",
|
||||
"RyujinxInfo": "Ryujinx - 訊息",
|
||||
"RyujinxConfirm": "Ryujinx - 確認",
|
||||
"FileDialogAllTypes": "全部類型",
|
||||
"Never": "從不",
|
||||
"SwkbdMinCharacters": "至少應為 {0} 個字長",
|
||||
"SwkbdMinRangeCharacters": "必須為 {0}-{1} 個字長",
|
||||
"SoftwareKeyboard": "軟體鍵盤",
|
||||
"SoftwareKeyboardModeNumbersOnly": "只接受數字",
|
||||
"SoftwareKeyboardModeAlphabet": "不支援中日韓統一表意文字字元",
|
||||
"SoftwareKeyboardModeASCII": "只接受 ASCII 符號",
|
||||
"DialogControllerAppletMessagePlayerRange": "本遊戲需要 {0} 個玩家持有:\n\n類型:{1}\n\n玩家:{2}\n\n{3}請打開設定畫面並配置控制器,或者關閉本視窗。",
|
||||
"DialogControllerAppletMessage": "本遊戲需要剛好 {0} 個玩家持有:\n\n類型:{1}\n\n玩家:{2}\n\n{3}請打開設定畫面並配置控制器,或者關閉本視窗。",
|
||||
"DialogControllerAppletDockModeSet": "現在處於主機模式,無法使用掌機操作方式\n\n",
|
||||
"UpdaterRenaming": "正在重新命名舊檔案...",
|
||||
"UpdaterRenameFailed": "更新過程中無法重新命名檔案: {0}",
|
||||
"UpdaterAddingFiles": "安裝更新中...",
|
||||
"UpdaterExtracting": "正在提取更新...",
|
||||
"UpdaterDownloading": "下載新版本中...",
|
||||
"Game": "遊戲",
|
||||
"Docked": "主機模式",
|
||||
"Handheld": "掌機模式",
|
||||
"ConnectionError": "連接錯誤。",
|
||||
"AboutPageDeveloperListMore": "{0} 等開發者...",
|
||||
"ApiError": "API 錯誤",
|
||||
"LoadingHeading": "正在啟動 {0}",
|
||||
"CompilingPPTC": "編譯 PPTC 快取中",
|
||||
"CompilingShaders": "編譯著色器中",
|
||||
"AllKeyboards": "所有鍵盤",
|
||||
"OpenFileDialogTitle": "選擇支援的檔案格式",
|
||||
"OpenFolderDialogTitle": "選擇一個包含已解開封裝遊戲的資料夾\n",
|
||||
"AllSupportedFormats": "全部支援的格式",
|
||||
"RyujinxUpdater": "Ryujinx 更新程式",
|
||||
"SettingsTabHotkeys": "快捷鍵",
|
||||
"SettingsTabHotkeysHotkeys": "鍵盤快捷鍵",
|
||||
"SettingsTabHotkeysToggleVsyncHotkey": "切換垂直同步:",
|
||||
"SettingsTabHotkeysScreenshotHotkey": "截圖:",
|
||||
"SettingsTabHotkeysShowUiHotkey": "隱藏使用者介面:",
|
||||
"SettingsTabHotkeysPauseHotkey": "暫停:",
|
||||
"SettingsTabHotkeysToggleMuteHotkey": "靜音:",
|
||||
"ControllerMotionTitle": "體感操作設定",
|
||||
"ControllerRumbleTitle": "震動設定",
|
||||
"SettingsSelectThemeFileDialogTitle": "選擇主題檔案",
|
||||
"SettingsXamlThemeFile": "Xaml 主題檔案",
|
||||
"AvatarWindowTitle": "管理帳號 - 頭貼",
|
||||
"Amiibo": "Amiibo",
|
||||
"Unknown": "未知",
|
||||
"Usage": "用途",
|
||||
"Writable": "可寫入",
|
||||
"SelectDlcDialogTitle": "選擇 DLC 檔案",
|
||||
"SelectUpdateDialogTitle": "選擇更新檔",
|
||||
"UserProfileWindowTitle": "管理使用者帳戶",
|
||||
"CheatWindowTitle": "管理遊戲金手指",
|
||||
"DlcWindowTitle": "管理遊戲 DLC",
|
||||
"UpdateWindowTitle": "管理遊戲更新",
|
||||
"CheatWindowHeading": "金手指可用於 {0} [{1}]",
|
||||
"BuildId": "版本編號:",
|
||||
"DlcWindowHeading": "DLC 可用於 {0} [{1}]",
|
||||
"UserProfilesEditProfile": "編輯所選",
|
||||
"Cancel": "取消",
|
||||
"Save": "儲存",
|
||||
"Discard": "放棄更改",
|
||||
"UserProfilesSetProfileImage": "設定帳戶頭像",
|
||||
"UserProfileEmptyNameError": "使用者名稱為必填",
|
||||
"UserProfileNoImageError": "必須設定帳戶頭像",
|
||||
"GameUpdateWindowHeading": "更新可用於 {0} [{1}]",
|
||||
"SettingsTabHotkeysResScaleUpHotkey": "提高解析度:",
|
||||
"SettingsTabHotkeysResScaleDownHotkey": "降低解析度:",
|
||||
"UserProfilesName": "使用者名稱:",
|
||||
"UserProfilesUserId": "使用者 ID:",
|
||||
"SettingsTabGraphicsBackend": "圖像處理後台架構",
|
||||
"SettingsTabGraphicsBackendTooltip": "用來圖像處理的後台架構",
|
||||
"SettingsEnableTextureRecompression": "開啟材質重新壓縮",
|
||||
"SettingsEnableTextureRecompressionTooltip": "壓縮某些材質以減少 VRAM 使用。\n\n推薦用於小於 4GiB VRAM 的 GPU。\n\n如果不確定請關閉本功能。",
|
||||
"SettingsTabGraphicsPreferredGpu": "優先選取的 GPU",
|
||||
"SettingsTabGraphicsPreferredGpuTooltip": "選擇支持運行Vulkan圖像處理架構的GPU.\n\n此設定不會影響GPU運行OpenGL.\n\n如果不確定, 請設定標籤為\"dGPU\"的GPU. 如果沒有, 請保留原始設定.",
|
||||
"SettingsAppRequiredRestartMessage": "必須重啟 Ryujinx",
|
||||
"SettingsGpuBackendRestartMessage": "圖像處理後台架構或GPU相關設定已被修改。需要重新啟動才能套用。",
|
||||
"SettingsGpuBackendRestartSubMessage": "你確定要現在重新啟動嗎?",
|
||||
"RyujinxUpdaterMessage": "你確定要將 Ryujinx 更新到最新版本嗎?",
|
||||
"SettingsTabHotkeysVolumeUpHotkey": "增加音量:",
|
||||
"SettingsTabHotkeysVolumeDownHotkey": "降低音量:",
|
||||
"SettingsEnableMacroHLE": "啟用 Macro HLE",
|
||||
"SettingsEnableMacroHLETooltip": "GPU 微代碼的高階模擬。\n\n可以提升效能,但可能會導致某些遊戲出現圖形顯示故障。\n\n如果不確定請設定為\"開啟\"。",
|
||||
"SettingsEnableColorSpacePassthrough": "色彩空間直通",
|
||||
"SettingsEnableColorSpacePassthroughTooltip": "指揮Vulkan後端直通色彩資訊而不需指定色彩空間,對於廣色域顯示的使用者,這會造成較多抖色,犧牲色彩正確性。",
|
||||
"VolumeShort": "音量",
|
||||
"UserProfilesManageSaves": "管理遊戲存檔",
|
||||
"DeleteUserSave": "你確定要刪除此遊戲的存檔嗎?",
|
||||
"IrreversibleActionNote": "本動作將無法挽回。",
|
||||
"SaveManagerHeading": "管理 {0} 的遊戲存檔",
|
||||
"SaveManagerTitle": "遊戲存檔管理器",
|
||||
"Name": "名稱",
|
||||
"Size": "大小",
|
||||
"Search": "搜尋",
|
||||
"UserProfilesRecoverLostAccounts": "恢復遺失的帳戶",
|
||||
"Recover": "恢復",
|
||||
"UserProfilesRecoverHeading": "在以下帳戶找到了一些遊戲存檔",
|
||||
"UserProfilesRecoverEmptyList": "沒有可恢復的使用者帳戶",
|
||||
"GraphicsAATooltip": "在遊戲繪圖上套用抗鋸齒",
|
||||
"GraphicsAALabel": "抗鋸齒:",
|
||||
"GraphicsScalingFilterLabel": "縮放過濾器:",
|
||||
"GraphicsScalingFilterTooltip": "啟用畫幀緩衝區縮放",
|
||||
"GraphicsScalingFilterLevelLabel": "記錄檔等級",
|
||||
"GraphicsScalingFilterLevelTooltip": "設定縮放過濾器的強度",
|
||||
"SmaaLow": "低階 SMAA",
|
||||
"SmaaMedium": "中階 SMAA",
|
||||
"SmaaHigh": "高階 SMAA",
|
||||
"SmaaUltra": "超高階 SMAA",
|
||||
"UserEditorTitle": "編輯使用者",
|
||||
"UserEditorTitleCreate": "建立使用者",
|
||||
"SettingsTabNetworkInterface": "網路介面:",
|
||||
"NetworkInterfaceTooltip": "用於具有 LAN 功能的網路介面",
|
||||
"NetworkInterfaceDefault": "預設",
|
||||
"PackagingShaders": "著色器封裝",
|
||||
"AboutChangelogButton": "在 GitHub 查看更新日誌",
|
||||
"AboutChangelogButtonTooltipMessage": "在瀏覽器中打開此Ryujinx版本的更新日誌。"
|
||||
}
|
@ -1,777 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using ICSharpCode.SharpZipLib.GZip;
|
||||
using ICSharpCode.SharpZipLib.Tar;
|
||||
using ICSharpCode.SharpZipLib.Zip;
|
||||
using Ryujinx.Ava;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using Ryujinx.Ui.Common.Models.Github;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Modules
|
||||
{
|
||||
internal static class Updater
|
||||
{
|
||||
private const string GitHubApiUrl = "https://api.github.com";
|
||||
private static readonly GithubReleasesJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
|
||||
private static readonly string _homeDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||
private static readonly string _updateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update");
|
||||
private static readonly string _updatePublishDir = Path.Combine(_updateDir, "publish");
|
||||
private const int ConnectionCount = 4;
|
||||
|
||||
private static string _buildVer;
|
||||
private static string _platformExt;
|
||||
private static string _buildUrl;
|
||||
private static long _buildSize;
|
||||
private static bool _updateSuccessful;
|
||||
private static bool _running;
|
||||
|
||||
private static readonly string[] _windowsDependencyDirs = Array.Empty<string>();
|
||||
|
||||
public static async Task BeginParse(Window mainWindow, bool showVersionUpToDate)
|
||||
{
|
||||
if (_running)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_running = true;
|
||||
|
||||
// Detect current platform
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
_platformExt = "macos_universal.app.tar.gz";
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
_platformExt = "win_x64.zip";
|
||||
}
|
||||
else if (OperatingSystem.IsLinux())
|
||||
{
|
||||
_platformExt = "linux_x64.tar.gz";
|
||||
}
|
||||
|
||||
Version newVersion;
|
||||
Version currentVersion;
|
||||
|
||||
try
|
||||
{
|
||||
currentVersion = Version.Parse(Program.Version);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, "Failed to convert the current Ryujinx version!");
|
||||
|
||||
await ContentDialogHelper.CreateWarningDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedMessage],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
|
||||
|
||||
_running = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Get latest version number from GitHub API
|
||||
try
|
||||
{
|
||||
using HttpClient jsonClient = ConstructHttpClient();
|
||||
|
||||
string buildInfoUrl = $"{GitHubApiUrl}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest";
|
||||
string fetchedJson = await jsonClient.GetStringAsync(buildInfoUrl);
|
||||
var fetched = JsonHelper.Deserialize(fetchedJson, _serializerContext.GithubReleasesJsonResponse);
|
||||
_buildVer = fetched.Name;
|
||||
|
||||
foreach (var asset in fetched.Assets)
|
||||
{
|
||||
if (asset.Name.StartsWith("test-ava-ryujinx") && asset.Name.EndsWith(_platformExt))
|
||||
{
|
||||
_buildUrl = asset.BrowserDownloadUrl;
|
||||
|
||||
if (asset.State != "uploaded")
|
||||
{
|
||||
if (showVersionUpToDate)
|
||||
{
|
||||
await ContentDialogHelper.CreateUpdaterInfoDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
|
||||
"");
|
||||
}
|
||||
|
||||
_running = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If build not done, assume no new update are available.
|
||||
if (_buildUrl is null)
|
||||
{
|
||||
if (showVersionUpToDate)
|
||||
{
|
||||
await ContentDialogHelper.CreateUpdaterInfoDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
|
||||
"");
|
||||
}
|
||||
|
||||
_running = false;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, exception.Message);
|
||||
|
||||
await ContentDialogHelper.CreateErrorDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterFailedToGetVersionMessage]);
|
||||
|
||||
_running = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
newVersion = Version.Parse(_buildVer);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, "Failed to convert the received Ryujinx version from Github!");
|
||||
|
||||
await ContentDialogHelper.CreateWarningDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedGithubMessage],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
|
||||
|
||||
_running = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (newVersion <= currentVersion)
|
||||
{
|
||||
if (showVersionUpToDate)
|
||||
{
|
||||
await ContentDialogHelper.CreateUpdaterInfoDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
|
||||
"");
|
||||
}
|
||||
|
||||
_running = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch build size information to learn chunk sizes.
|
||||
using HttpClient buildSizeClient = ConstructHttpClient();
|
||||
try
|
||||
{
|
||||
buildSizeClient.DefaultRequestHeaders.Add("Range", "bytes=0-0");
|
||||
|
||||
HttpResponseMessage message = await buildSizeClient.GetAsync(new Uri(_buildUrl), HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
_buildSize = message.Content.Headers.ContentRange.Length.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, ex.Message);
|
||||
Logger.Warning?.Print(LogClass.Application, "Couldn't determine build size for update, using single-threaded updater");
|
||||
|
||||
_buildSize = -1;
|
||||
}
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
// Show a message asking the user if they want to update
|
||||
var shouldUpdate = await ContentDialogHelper.CreateChoiceDialog(
|
||||
LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
|
||||
LocaleManager.Instance[LocaleKeys.RyujinxUpdaterMessage],
|
||||
$"{Program.Version} -> {newVersion}");
|
||||
|
||||
if (shouldUpdate)
|
||||
{
|
||||
await UpdateRyujinx(mainWindow, _buildUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
_running = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static HttpClient ConstructHttpClient()
|
||||
{
|
||||
HttpClient result = new();
|
||||
|
||||
// Required by GitHub to interact with APIs.
|
||||
result.DefaultRequestHeaders.Add("User-Agent", "Ryujinx-Updater/1.0.0");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static async Task UpdateRyujinx(Window parent, string downloadUrl)
|
||||
{
|
||||
_updateSuccessful = false;
|
||||
|
||||
// Empty update dir, although it shouldn't ever have anything inside it
|
||||
if (Directory.Exists(_updateDir))
|
||||
{
|
||||
Directory.Delete(_updateDir, true);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(_updateDir);
|
||||
|
||||
string updateFile = Path.Combine(_updateDir, "update.bin");
|
||||
|
||||
TaskDialog taskDialog = new()
|
||||
{
|
||||
Header = LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
|
||||
SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterDownloading],
|
||||
IconSource = new SymbolIconSource { Symbol = Symbol.Download },
|
||||
ShowProgressBar = true,
|
||||
XamlRoot = parent,
|
||||
};
|
||||
|
||||
taskDialog.Opened += (s, e) =>
|
||||
{
|
||||
if (_buildSize >= 0)
|
||||
{
|
||||
DoUpdateWithMultipleThreads(taskDialog, downloadUrl, updateFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
DoUpdateWithSingleThread(taskDialog, downloadUrl, updateFile);
|
||||
}
|
||||
};
|
||||
|
||||
await taskDialog.ShowAsync(true);
|
||||
|
||||
if (_updateSuccessful)
|
||||
{
|
||||
bool shouldRestart = true;
|
||||
|
||||
if (!OperatingSystem.IsMacOS())
|
||||
{
|
||||
shouldRestart = await ContentDialogHelper.CreateChoiceDialog(LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterCompleteMessage],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterRestartMessage]);
|
||||
}
|
||||
|
||||
if (shouldRestart)
|
||||
{
|
||||
List<string> arguments = CommandLineState.Arguments.ToList();
|
||||
string executableDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||
|
||||
// On macOS we perform the update at relaunch.
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
string baseBundlePath = Path.GetFullPath(Path.Combine(executableDirectory, "..", ".."));
|
||||
string newBundlePath = Path.Combine(_updateDir, "Ryujinx.app");
|
||||
string updaterScriptPath = Path.Combine(newBundlePath, "Contents", "Resources", "updater.sh");
|
||||
string currentPid = Environment.ProcessId.ToString();
|
||||
|
||||
arguments.InsertRange(0, new List<string> { updaterScriptPath, baseBundlePath, newBundlePath, currentPid });
|
||||
Process.Start("/bin/bash", arguments);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Find the process name.
|
||||
string ryuName = Path.GetFileName(Environment.ProcessPath);
|
||||
|
||||
// Some operating systems can see the renamed executable, so strip off the .ryuold if found.
|
||||
if (ryuName.EndsWith(".ryuold"))
|
||||
{
|
||||
ryuName = ryuName[..^7];
|
||||
}
|
||||
|
||||
// Fallback if the executable could not be found.
|
||||
if (!Path.Exists(Path.Combine(executableDirectory, ryuName)))
|
||||
{
|
||||
ryuName = OperatingSystem.IsWindows() ? "Ryujinx.Ava.exe" : "Ryujinx.Ava";
|
||||
}
|
||||
|
||||
ProcessStartInfo processStart = new(ryuName)
|
||||
{
|
||||
UseShellExecute = true,
|
||||
WorkingDirectory = executableDirectory,
|
||||
};
|
||||
|
||||
foreach (string argument in CommandLineState.Arguments)
|
||||
{
|
||||
processStart.ArgumentList.Add(argument);
|
||||
}
|
||||
|
||||
Process.Start(processStart);
|
||||
}
|
||||
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DoUpdateWithMultipleThreads(TaskDialog taskDialog, string downloadUrl, string updateFile)
|
||||
{
|
||||
// Multi-Threaded Updater
|
||||
long chunkSize = _buildSize / ConnectionCount;
|
||||
long remainderChunk = _buildSize % ConnectionCount;
|
||||
|
||||
int completedRequests = 0;
|
||||
int totalProgressPercentage = 0;
|
||||
int[] progressPercentage = new int[ConnectionCount];
|
||||
|
||||
List<byte[]> list = new(ConnectionCount);
|
||||
List<WebClient> webClients = new(ConnectionCount);
|
||||
|
||||
for (int i = 0; i < ConnectionCount; i++)
|
||||
{
|
||||
list.Add(Array.Empty<byte>());
|
||||
}
|
||||
|
||||
for (int i = 0; i < ConnectionCount; i++)
|
||||
{
|
||||
#pragma warning disable SYSLIB0014
|
||||
// TODO: WebClient is obsolete and need to be replaced with a more complex logic using HttpClient.
|
||||
using WebClient client = new();
|
||||
#pragma warning restore SYSLIB0014
|
||||
|
||||
webClients.Add(client);
|
||||
|
||||
if (i == ConnectionCount - 1)
|
||||
{
|
||||
client.Headers.Add("Range", $"bytes={chunkSize * i}-{(chunkSize * (i + 1) - 1) + remainderChunk}");
|
||||
}
|
||||
else
|
||||
{
|
||||
client.Headers.Add("Range", $"bytes={chunkSize * i}-{chunkSize * (i + 1) - 1}");
|
||||
}
|
||||
|
||||
client.DownloadProgressChanged += (_, args) =>
|
||||
{
|
||||
int index = (int)args.UserState;
|
||||
|
||||
Interlocked.Add(ref totalProgressPercentage, -1 * progressPercentage[index]);
|
||||
Interlocked.Exchange(ref progressPercentage[index], args.ProgressPercentage);
|
||||
Interlocked.Add(ref totalProgressPercentage, args.ProgressPercentage);
|
||||
|
||||
taskDialog.SetProgressBarState(totalProgressPercentage / ConnectionCount, TaskDialogProgressState.Normal);
|
||||
};
|
||||
|
||||
client.DownloadDataCompleted += (_, args) =>
|
||||
{
|
||||
int index = (int)args.UserState;
|
||||
|
||||
if (args.Cancelled)
|
||||
{
|
||||
webClients[index].Dispose();
|
||||
|
||||
taskDialog.Hide();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
list[index] = args.Result;
|
||||
Interlocked.Increment(ref completedRequests);
|
||||
|
||||
if (Equals(completedRequests, ConnectionCount))
|
||||
{
|
||||
byte[] mergedFileBytes = new byte[_buildSize];
|
||||
for (int connectionIndex = 0, destinationOffset = 0; connectionIndex < ConnectionCount; connectionIndex++)
|
||||
{
|
||||
Array.Copy(list[connectionIndex], 0, mergedFileBytes, destinationOffset, list[connectionIndex].Length);
|
||||
destinationOffset += list[connectionIndex].Length;
|
||||
}
|
||||
|
||||
File.WriteAllBytes(updateFile, mergedFileBytes);
|
||||
|
||||
// On macOS, ensure that we remove the quarantine bit to prevent Gatekeeper from blocking execution.
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
using Process xattrProcess = Process.Start("xattr", new List<string> { "-d", "com.apple.quarantine", updateFile });
|
||||
|
||||
xattrProcess.WaitForExit();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
InstallUpdate(taskDialog, updateFile);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, e.Message);
|
||||
Logger.Warning?.Print(LogClass.Application, "Multi-Threaded update failed, falling back to single-threaded updater.");
|
||||
|
||||
DoUpdateWithSingleThread(taskDialog, downloadUrl, updateFile);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
client.DownloadDataAsync(new Uri(downloadUrl), i);
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, ex.Message);
|
||||
Logger.Warning?.Print(LogClass.Application, "Multi-Threaded update failed, falling back to single-threaded updater.");
|
||||
|
||||
foreach (WebClient webClient in webClients)
|
||||
{
|
||||
webClient.CancelAsync();
|
||||
}
|
||||
|
||||
DoUpdateWithSingleThread(taskDialog, downloadUrl, updateFile);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DoUpdateWithSingleThreadWorker(TaskDialog taskDialog, string downloadUrl, string updateFile)
|
||||
{
|
||||
using HttpClient client = new();
|
||||
// We do not want to timeout while downloading
|
||||
client.Timeout = TimeSpan.FromDays(1);
|
||||
|
||||
using HttpResponseMessage response = client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead).Result;
|
||||
using Stream remoteFileStream = response.Content.ReadAsStreamAsync().Result;
|
||||
using Stream updateFileStream = File.Open(updateFile, FileMode.Create);
|
||||
|
||||
long totalBytes = response.Content.Headers.ContentLength.Value;
|
||||
long byteWritten = 0;
|
||||
|
||||
byte[] buffer = new byte[32 * 1024];
|
||||
|
||||
while (true)
|
||||
{
|
||||
int readSize = remoteFileStream.Read(buffer);
|
||||
|
||||
if (readSize == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
byteWritten += readSize;
|
||||
|
||||
taskDialog.SetProgressBarState(GetPercentage(byteWritten, totalBytes), TaskDialogProgressState.Normal);
|
||||
|
||||
updateFileStream.Write(buffer, 0, readSize);
|
||||
}
|
||||
|
||||
InstallUpdate(taskDialog, updateFile);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double GetPercentage(double value, double max)
|
||||
{
|
||||
return max == 0 ? 0 : value / max * 100;
|
||||
}
|
||||
|
||||
private static void DoUpdateWithSingleThread(TaskDialog taskDialog, string downloadUrl, string updateFile)
|
||||
{
|
||||
Thread worker = new(() => DoUpdateWithSingleThreadWorker(taskDialog, downloadUrl, updateFile))
|
||||
{
|
||||
Name = "Updater.SingleThreadWorker",
|
||||
};
|
||||
|
||||
worker.Start();
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("linux")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
private static void ExtractTarGzipFile(TaskDialog taskDialog, string archivePath, string outputDirectoryPath)
|
||||
{
|
||||
using Stream inStream = File.OpenRead(archivePath);
|
||||
using GZipInputStream gzipStream = new(inStream);
|
||||
using TarInputStream tarStream = new(gzipStream, Encoding.ASCII);
|
||||
|
||||
TarEntry tarEntry;
|
||||
|
||||
while ((tarEntry = tarStream.GetNextEntry()) is not null)
|
||||
{
|
||||
if (tarEntry.IsDirectory)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string outPath = Path.Combine(outputDirectoryPath, tarEntry.Name);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outPath));
|
||||
|
||||
using FileStream outStream = File.OpenWrite(outPath);
|
||||
tarStream.CopyEntryContents(outStream);
|
||||
|
||||
File.SetUnixFileMode(outPath, (UnixFileMode)tarEntry.TarHeader.Mode);
|
||||
File.SetLastWriteTime(outPath, DateTime.SpecifyKind(tarEntry.ModTime, DateTimeKind.Utc));
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (tarEntry is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
taskDialog.SetProgressBarState(GetPercentage(tarEntry.Size, inStream.Length), TaskDialogProgressState.Normal);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExtractZipFile(TaskDialog taskDialog, string archivePath, string outputDirectoryPath)
|
||||
{
|
||||
using Stream inStream = File.OpenRead(archivePath);
|
||||
using ZipFile zipFile = new(inStream);
|
||||
|
||||
double count = 0;
|
||||
foreach (ZipEntry zipEntry in zipFile)
|
||||
{
|
||||
count++;
|
||||
if (zipEntry.IsDirectory)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string outPath = Path.Combine(outputDirectoryPath, zipEntry.Name);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outPath));
|
||||
|
||||
using Stream zipStream = zipFile.GetInputStream(zipEntry);
|
||||
using FileStream outStream = File.OpenWrite(outPath);
|
||||
|
||||
zipStream.CopyTo(outStream);
|
||||
|
||||
File.SetLastWriteTime(outPath, DateTime.SpecifyKind(zipEntry.DateTime, DateTimeKind.Utc));
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
taskDialog.SetProgressBarState(GetPercentage(count, zipFile.Count), TaskDialogProgressState.Normal);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void InstallUpdate(TaskDialog taskDialog, string updateFile)
|
||||
{
|
||||
// Extract Update
|
||||
taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterExtracting];
|
||||
taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal);
|
||||
|
||||
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||
{
|
||||
ExtractTarGzipFile(taskDialog, updateFile, _updateDir);
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
ExtractZipFile(taskDialog, updateFile, _updateDir);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
// Delete downloaded zip
|
||||
File.Delete(updateFile);
|
||||
|
||||
List<string> allFiles = EnumerateFilesToDelete().ToList();
|
||||
|
||||
taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterRenaming];
|
||||
taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal);
|
||||
|
||||
// NOTE: On macOS, replacement is delayed to the restart phase.
|
||||
if (!OperatingSystem.IsMacOS())
|
||||
{
|
||||
// Replace old files
|
||||
double count = 0;
|
||||
foreach (string file in allFiles)
|
||||
{
|
||||
count++;
|
||||
try
|
||||
{
|
||||
File.Move(file, file + ".ryuold");
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
taskDialog.SetProgressBarState(GetPercentage(count, allFiles.Count), TaskDialogProgressState.Normal);
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.UpdaterRenameFailed, file));
|
||||
}
|
||||
}
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterAddingFiles];
|
||||
taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal);
|
||||
});
|
||||
|
||||
MoveAllFilesOver(_updatePublishDir, _homeDir, taskDialog);
|
||||
|
||||
Directory.Delete(_updateDir, true);
|
||||
}
|
||||
|
||||
_updateSuccessful = true;
|
||||
|
||||
taskDialog.Hide();
|
||||
}
|
||||
|
||||
public static bool CanUpdate(bool showWarnings)
|
||||
{
|
||||
#if !DISABLE_UPDATER
|
||||
if (RuntimeInformation.OSArchitecture != Architecture.X64 && !OperatingSystem.IsMacOS())
|
||||
{
|
||||
if (showWarnings)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
ContentDialogHelper.CreateWarningDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterArchNotSupportedMessage],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterArchNotSupportedSubMessage])
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!NetworkInterface.GetIsNetworkAvailable())
|
||||
{
|
||||
if (showWarnings)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
ContentDialogHelper.CreateWarningDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterNoInternetMessage],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterNoInternetSubMessage])
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Program.Version.Contains("dirty") || !ReleaseInformation.IsValid())
|
||||
{
|
||||
if (showWarnings)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
ContentDialogHelper.CreateWarningDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildMessage],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage])
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
#else
|
||||
if (showWarnings)
|
||||
{
|
||||
if (ReleaseInformation.IsFlatHubBuild())
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
ContentDialogHelper.CreateWarningDialog(
|
||||
LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterFlatpakNotSupportedMessage])
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
ContentDialogHelper.CreateWarningDialog(
|
||||
LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// NOTE: This method should always reflect the latest build layout.
|
||||
private static IEnumerable<string> EnumerateFilesToDelete()
|
||||
{
|
||||
var files = Directory.EnumerateFiles(_homeDir); // All files directly in base dir.
|
||||
|
||||
// Determine and exclude user files only when the updater is running, not when cleaning old files
|
||||
if (_running && !OperatingSystem.IsMacOS())
|
||||
{
|
||||
// Compare the loose files in base directory against the loose files from the incoming update, and store foreign ones in a user list.
|
||||
var oldFiles = Directory.EnumerateFiles(_homeDir, "*", SearchOption.TopDirectoryOnly).Select(Path.GetFileName);
|
||||
var newFiles = Directory.EnumerateFiles(_updatePublishDir, "*", SearchOption.TopDirectoryOnly).Select(Path.GetFileName);
|
||||
var userFiles = oldFiles.Except(newFiles).Select(filename => Path.Combine(_homeDir, filename));
|
||||
|
||||
// Remove user files from the paths in files.
|
||||
files = files.Except(userFiles);
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
foreach (string dir in _windowsDependencyDirs)
|
||||
{
|
||||
string dirPath = Path.Combine(_homeDir, dir);
|
||||
if (Directory.Exists(dirPath))
|
||||
{
|
||||
files = files.Concat(Directory.EnumerateFiles(dirPath, "*", SearchOption.AllDirectories));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files.Where(f => !new FileInfo(f).Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System));
|
||||
}
|
||||
|
||||
private static void MoveAllFilesOver(string root, string dest, TaskDialog taskDialog)
|
||||
{
|
||||
int total = Directory.GetFiles(root, "*", SearchOption.AllDirectories).Length;
|
||||
foreach (string directory in Directory.GetDirectories(root))
|
||||
{
|
||||
string dirName = Path.GetFileName(directory);
|
||||
|
||||
if (!Directory.Exists(Path.Combine(dest, dirName)))
|
||||
{
|
||||
Directory.CreateDirectory(Path.Combine(dest, dirName));
|
||||
}
|
||||
|
||||
MoveAllFilesOver(directory, Path.Combine(dest, dirName), taskDialog);
|
||||
}
|
||||
|
||||
double count = 0;
|
||||
foreach (string file in Directory.GetFiles(root))
|
||||
{
|
||||
count++;
|
||||
|
||||
File.Move(file, Path.Combine(dest, Path.GetFileName(file)), true);
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
taskDialog.SetProgressBarState(GetPercentage(count, total), TaskDialogProgressState.Normal);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void CleanupUpdate()
|
||||
{
|
||||
foreach (string file in Directory.GetFiles(_homeDir, "*.ryuold", SearchOption.AllDirectories))
|
||||
{
|
||||
File.Delete(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,236 +0,0 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Threading;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.GraphicsDriver;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.SystemInterop;
|
||||
using Ryujinx.Modules;
|
||||
using Ryujinx.SDL2.Common;
|
||||
using Ryujinx.Ui.Common;
|
||||
using Ryujinx.Ui.Common.Configuration;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using Ryujinx.Ui.Common.SystemInfo;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava
|
||||
{
|
||||
internal partial class Program
|
||||
{
|
||||
public static double WindowScaleFactor { get; set; }
|
||||
public static double DesktopScaleFactor { get; set; } = 1.0;
|
||||
public static string Version { get; private set; }
|
||||
public static string ConfigurationPath { get; private set; }
|
||||
public static bool PreviewerDetached { get; private set; }
|
||||
|
||||
[LibraryImport("user32.dll", SetLastError = true)]
|
||||
public static partial int MessageBoxA(IntPtr hWnd, [MarshalAs(UnmanagedType.LPStr)] string text, [MarshalAs(UnmanagedType.LPStr)] string caption, uint type);
|
||||
|
||||
private const uint MbIconwarning = 0x30;
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Version = ReleaseInformation.GetVersion();
|
||||
|
||||
if (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134))
|
||||
{
|
||||
_ = MessageBoxA(IntPtr.Zero, "You are running an outdated version of Windows.\n\nStarting on June 1st 2022, Ryujinx will only support Windows 10 1803 and newer.\n", $"Ryujinx {Version}", MbIconwarning);
|
||||
}
|
||||
|
||||
PreviewerDetached = true;
|
||||
|
||||
Initialize(args);
|
||||
|
||||
LoggerAdapter.Register();
|
||||
|
||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||
}
|
||||
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
{
|
||||
return AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.With(new X11PlatformOptions
|
||||
{
|
||||
EnableMultiTouch = true,
|
||||
EnableIme = true,
|
||||
RenderingMode = new[] { X11RenderingMode.Glx, X11RenderingMode.Software },
|
||||
})
|
||||
.With(new Win32PlatformOptions
|
||||
{
|
||||
WinUICompositionBackdropCornerRadius = 8.0f,
|
||||
RenderingMode = new[] { Win32RenderingMode.AngleEgl, Win32RenderingMode.Software },
|
||||
})
|
||||
.UseSkia();
|
||||
}
|
||||
|
||||
private static void Initialize(string[] args)
|
||||
{
|
||||
// Parse arguments
|
||||
CommandLineState.ParseArguments(args);
|
||||
|
||||
// Delete backup files after updating.
|
||||
Task.Run(Updater.CleanupUpdate);
|
||||
|
||||
Console.Title = $"Ryujinx Console {Version}";
|
||||
|
||||
// Hook unhandled exception and process exit events.
|
||||
AppDomain.CurrentDomain.UnhandledException += (sender, e) => ProcessUnhandledException(e.ExceptionObject as Exception, e.IsTerminating);
|
||||
AppDomain.CurrentDomain.ProcessExit += (sender, e) => Exit();
|
||||
|
||||
// Setup base data directory.
|
||||
AppDataManager.Initialize(CommandLineState.BaseDirPathArg);
|
||||
|
||||
// Initialize the configuration.
|
||||
ConfigurationState.Initialize();
|
||||
|
||||
// Initialize the logger system.
|
||||
LoggerModule.Initialize();
|
||||
|
||||
// Initialize Discord integration.
|
||||
DiscordIntegrationModule.Initialize();
|
||||
|
||||
// Initialize SDL2 driver
|
||||
SDL2Driver.MainThreadDispatcher = action => Dispatcher.UIThread.InvokeAsync(action, DispatcherPriority.Input);
|
||||
|
||||
ReloadConfig();
|
||||
|
||||
WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor();
|
||||
|
||||
// Logging system information.
|
||||
PrintSystemInfo();
|
||||
|
||||
// Enable OGL multithreading on the driver, when available.
|
||||
DriverUtilities.ToggleOGLThreading(ConfigurationState.Instance.Graphics.BackendThreading == BackendThreading.Off);
|
||||
|
||||
// Check if keys exists.
|
||||
if (!File.Exists(Path.Combine(AppDataManager.KeysDirPath, "prod.keys")))
|
||||
{
|
||||
if (!(AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile && File.Exists(Path.Combine(AppDataManager.KeysDirPathUser, "prod.keys"))))
|
||||
{
|
||||
MainWindow.ShowKeyErrorOnLoad = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (CommandLineState.LaunchPathArg != null)
|
||||
{
|
||||
MainWindow.DeferLoadApplication(CommandLineState.LaunchPathArg, CommandLineState.StartFullscreenArg);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ReloadConfig()
|
||||
{
|
||||
string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json");
|
||||
string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, "Config.json");
|
||||
|
||||
// Now load the configuration as the other subsystems are now registered
|
||||
if (File.Exists(localConfigurationPath))
|
||||
{
|
||||
ConfigurationPath = localConfigurationPath;
|
||||
}
|
||||
else if (File.Exists(appDataConfigurationPath))
|
||||
{
|
||||
ConfigurationPath = appDataConfigurationPath;
|
||||
}
|
||||
|
||||
if (ConfigurationPath == null)
|
||||
{
|
||||
// No configuration, we load the default values and save it to disk
|
||||
ConfigurationPath = appDataConfigurationPath;
|
||||
|
||||
ConfigurationState.Instance.LoadDefault();
|
||||
ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ConfigurationFileFormat.TryLoad(ConfigurationPath, out ConfigurationFileFormat configurationFileFormat))
|
||||
{
|
||||
ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
ConfigurationState.Instance.LoadDefault();
|
||||
|
||||
Logger.Warning?.PrintMsg(LogClass.Application, $"Failed to load config! Loading the default config instead.\nFailed config location {ConfigurationPath}");
|
||||
}
|
||||
}
|
||||
|
||||
// Check if graphics backend was overridden
|
||||
if (CommandLineState.OverrideGraphicsBackend != null)
|
||||
{
|
||||
if (CommandLineState.OverrideGraphicsBackend.ToLower() == "opengl")
|
||||
{
|
||||
ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.OpenGl;
|
||||
}
|
||||
else if (CommandLineState.OverrideGraphicsBackend.ToLower() == "vulkan")
|
||||
{
|
||||
ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.Vulkan;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if docked mode was overriden.
|
||||
if (CommandLineState.OverrideDockedMode.HasValue)
|
||||
{
|
||||
ConfigurationState.Instance.System.EnableDockedMode.Value = CommandLineState.OverrideDockedMode.Value;
|
||||
}
|
||||
|
||||
// Check if HideCursor was overridden.
|
||||
if (CommandLineState.OverrideHideCursor is not null)
|
||||
{
|
||||
ConfigurationState.Instance.HideCursor.Value = CommandLineState.OverrideHideCursor!.ToLower() switch
|
||||
{
|
||||
"never" => HideCursorMode.Never,
|
||||
"onidle" => HideCursorMode.OnIdle,
|
||||
"always" => HideCursorMode.Always,
|
||||
_ => ConfigurationState.Instance.HideCursor.Value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static void PrintSystemInfo()
|
||||
{
|
||||
Logger.Notice.Print(LogClass.Application, $"Ryujinx Version: {Version}");
|
||||
SystemInfo.Gather().Print();
|
||||
|
||||
Logger.Notice.Print(LogClass.Application, $"Logs Enabled: {(Logger.GetEnabledLevels().Count == 0 ? "<None>" : string.Join(", ", Logger.GetEnabledLevels()))}");
|
||||
|
||||
if (AppDataManager.Mode == AppDataManager.LaunchMode.Custom)
|
||||
{
|
||||
Logger.Notice.Print(LogClass.Application, $"Launch Mode: Custom Path {AppDataManager.BaseDirPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Notice.Print(LogClass.Application, $"Launch Mode: {AppDataManager.Mode}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessUnhandledException(Exception ex, bool isTerminating)
|
||||
{
|
||||
string message = $"Unhandled exception caught: {ex}";
|
||||
|
||||
Logger.Error?.PrintMsg(LogClass.Application, message);
|
||||
|
||||
if (Logger.Error == null)
|
||||
{
|
||||
Logger.Notice.PrintMsg(LogClass.Application, message);
|
||||
}
|
||||
|
||||
if (isTerminating)
|
||||
{
|
||||
Exit();
|
||||
}
|
||||
}
|
||||
|
||||
public static void Exit()
|
||||
{
|
||||
DiscordIntegrationModule.Exit();
|
||||
|
||||
Logger.Shutdown();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,168 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
|
||||
<OutputType>Exe</OutputType>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Version>1.0.0-dirty</Version>
|
||||
<DefineConstants Condition=" '$(ExtraDefineConstants)' != '' ">$(DefineConstants);$(ExtraDefineConstants)</DefineConstants>
|
||||
<SigningCertificate Condition=" '$(SigningCertificate)' == '' ">-</SigningCertificate>
|
||||
<RootNamespace>Ryujinx.Ava</RootNamespace>
|
||||
<ApplicationIcon>Ryujinx.ico</ApplicationIcon>
|
||||
<TieredPGO>true</TieredPGO>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="$([MSBuild]::IsOSPlatform('OSX'))">
|
||||
<Exec Command="codesign --entitlements '$(ProjectDir)..\..\distribution\macos\entitlements.xml' -f --deep -s $(SigningCertificate) '$(TargetDir)$(TargetName)'" />
|
||||
</Target>
|
||||
|
||||
<PropertyGroup Condition="'$(RuntimeIdentifier)' != ''">
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<TrimmerSingleWarn>false</TrimmerSingleWarn>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<TrimMode>partial</TrimMode>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
FluentAvalonia, used in the Avalonia UI, requires a workaround for the json serializer used internally when using .NET 8+ System.Text.Json.
|
||||
See:
|
||||
https://github.com/amwx/FluentAvalonia/issues/481
|
||||
https://devblogs.microsoft.com/dotnet/system-text-json-in-dotnet-8/
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" />
|
||||
<PackageReference Include="Avalonia.Desktop" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Condition="'$(Configuration)'=='Debug'" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" />
|
||||
<PackageReference Include="Avalonia.Markup.Xaml.Loader" />
|
||||
<PackageReference Include="Avalonia.Svg" />
|
||||
<PackageReference Include="Avalonia.Svg.Skia" />
|
||||
<PackageReference Include="jp2masa.Avalonia.Flexbox" />
|
||||
<PackageReference Include="DynamicData" />
|
||||
<PackageReference Include="FluentAvaloniaUI" />
|
||||
|
||||
<PackageReference Include="OpenTK.Core" />
|
||||
<PackageReference Include="Ryujinx.Audio.OpenAL.Dependencies" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64' AND '$(RuntimeIdentifier)' != 'osx-arm64'" />
|
||||
<PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies" />
|
||||
<PackageReference Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'win-x64'" />
|
||||
<PackageReference Include="Silk.NET.Vulkan" />
|
||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
|
||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
|
||||
<PackageReference Include="SPB" />
|
||||
<PackageReference Include="SharpZipLib" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" />
|
||||
|
||||
<!--NOTE: DO NOT REMOVE, THIS IS REQUIRED AS A RESULT OF A TRIMMING ISSUE IN AVALONIA -->
|
||||
<PackageReference Include="System.Drawing.Common" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Ryujinx.Audio.Backends.SDL2\Ryujinx.Audio.Backends.SDL2.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Input.SDL2\Ryujinx.Input.SDL2.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Audio.Backends.SoundIo\Ryujinx.Audio.Backends.SoundIo.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
|
||||
<ProjectReference Include="..\ARMeilleure\ARMeilleure.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Graphics.Gpu\Ryujinx.Graphics.Gpu.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Ui.Common\Ryujinx.Ui.Common.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Ui.LocaleGenerator\Ryujinx.Ui.LocaleGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\distribution\windows\alsoft.ini" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<TargetPath>alsoft.ini</TargetPath>
|
||||
</Content>
|
||||
<Content Include="..\..\distribution\legal\THIRDPARTY.md">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<TargetPath>THIRDPARTY.md</TargetPath>
|
||||
</Content>
|
||||
<Content Include="..\..\LICENSE.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<TargetPath>LICENSE.txt</TargetPath>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(RuntimeIdentifier)' == 'linux-x64'">
|
||||
<Content Include="..\..\distribution\linux\Ryujinx.sh">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="..\..\distribution\linux\mime\Ryujinx.xml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<TargetPath>mime\Ryujinx.xml</TargetPath>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Ui\**\*.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
</AvaloniaResource>
|
||||
<AvaloniaResource Include="Assets\Fonts\SegoeFluentIcons.ttf" />
|
||||
<AvaloniaResource Include="Assets\Styles\Themes.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</AvaloniaResource>
|
||||
<AvaloniaResource Include="Assets\Styles\Styles.xaml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Assets\Locales\el_GR.json" />
|
||||
<None Remove="Assets\Locales\en_US.json" />
|
||||
<None Remove="Assets\Locales\es_ES.json" />
|
||||
<None Remove="Assets\Locales\fr_FR.json" />
|
||||
<None Remove="Assets\Locales\he_IL.json" />
|
||||
<None Remove="Assets\Locales\de_DE.json" />
|
||||
<None Remove="Assets\Locales\it_IT.json" />
|
||||
<None Remove="Assets\Locales\ja_JP.json" />
|
||||
<None Remove="Assets\Locales\ko_KR.json" />
|
||||
<None Remove="Assets\Locales\pl_PL.json" />
|
||||
<None Remove="Assets\Locales\pt_BR.json" />
|
||||
<None Remove="Assets\Locales\ru_RU.json" />
|
||||
<None Remove="Assets\Locales\tr_TR.json" />
|
||||
<None Remove="Assets\Locales\uk_UA.json" />
|
||||
<None Remove="Assets\Locales\zh_CN.json" />
|
||||
<None Remove="Assets\Locales\zh_TW.json" />
|
||||
<None Remove="Assets\Styles\Styles.xaml" />
|
||||
<None Remove="Assets\Styles\Themes.xaml" />
|
||||
<None Remove="Assets\Icons\Controller_JoyConLeft.svg" />
|
||||
<None Remove="Assets\Icons\Controller_JoyConPair.svg" />
|
||||
<None Remove="Assets\Icons\Controller_JoyConRight.svg" />
|
||||
<None Remove="Assets\Icons\Controller_ProCon.svg" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Assets\Locales\el_GR.json" />
|
||||
<EmbeddedResource Include="Assets\Locales\en_US.json" />
|
||||
<EmbeddedResource Include="Assets\Locales\es_ES.json" />
|
||||
<EmbeddedResource Include="Assets\Locales\fr_FR.json" />
|
||||
<EmbeddedResource Include="Assets\Locales\he_IL.json" />
|
||||
<EmbeddedResource Include="Assets\Locales\de_DE.json" />
|
||||
<EmbeddedResource Include="Assets\Locales\it_IT.json" />
|
||||
<EmbeddedResource Include="Assets\Locales\ja_JP.json" />
|
||||
<EmbeddedResource Include="Assets\Locales\ko_KR.json" />
|
||||
<EmbeddedResource Include="Assets\Locales\pl_PL.json" />
|
||||
<EmbeddedResource Include="Assets\Locales\pt_BR.json" />
|
||||
<EmbeddedResource Include="Assets\Locales\ru_RU.json" />
|
||||
<EmbeddedResource Include="Assets\Locales\tr_TR.json" />
|
||||
<EmbeddedResource Include="Assets\Locales\uk_UA.json" />
|
||||
<EmbeddedResource Include="Assets\Locales\zh_CN.json" />
|
||||
<EmbeddedResource Include="Assets\Locales\zh_TW.json" />
|
||||
<EmbeddedResource Include="Assets\Styles\Styles.xaml" />
|
||||
<EmbeddedResource Include="Assets\Icons\Controller_JoyConLeft.svg" />
|
||||
<EmbeddedResource Include="Assets\Icons\Controller_JoyConPair.svg" />
|
||||
<EmbeddedResource Include="Assets\Icons\Controller_JoyConRight.svg" />
|
||||
<EmbeddedResource Include="Assets\Icons\Controller_ProCon.svg" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="Assets\Locales\en_US.json" />
|
||||
</ItemGroup>
|
||||
</Project>
|
@ -1,46 +0,0 @@
|
||||
using Avalonia.Data.Converters;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
internal class KeyValueConverter : IValueConverter
|
||||
{
|
||||
public static KeyValueConverter Instance = new();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.ToString();
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
object key = null;
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
if (targetType == typeof(Key))
|
||||
{
|
||||
key = Enum.Parse<Key>(value.ToString());
|
||||
}
|
||||
else if (targetType == typeof(GamepadInputId))
|
||||
{
|
||||
key = Enum.Parse<GamepadInputId>(value.ToString());
|
||||
}
|
||||
else if (targetType == typeof(StickInputId))
|
||||
{
|
||||
key = Enum.Parse<StickInputId>(value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,456 +0,0 @@
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
|
||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models
|
||||
{
|
||||
internal class InputConfiguration<TKey, TStick> : BaseModel
|
||||
{
|
||||
private float _deadzoneRight;
|
||||
private float _triggerThreshold;
|
||||
private float _deadzoneLeft;
|
||||
private double _gyroDeadzone;
|
||||
private int _sensitivity;
|
||||
private bool _enableMotion;
|
||||
private float _weakRumble;
|
||||
private float _strongRumble;
|
||||
private float _rangeLeft;
|
||||
private float _rangeRight;
|
||||
|
||||
public InputBackendType Backend { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Controller id
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Controller's Type
|
||||
/// </summary>
|
||||
public ControllerType ControllerType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Player's Index for the controller
|
||||
/// </summary>
|
||||
public PlayerIndex PlayerIndex { get; set; }
|
||||
|
||||
public TStick LeftJoystick { get; set; }
|
||||
public bool LeftInvertStickX { get; set; }
|
||||
public bool LeftInvertStickY { get; set; }
|
||||
public bool RightRotate90 { get; set; }
|
||||
public TKey LeftControllerStickButton { get; set; }
|
||||
|
||||
public TStick RightJoystick { get; set; }
|
||||
public bool RightInvertStickX { get; set; }
|
||||
public bool RightInvertStickY { get; set; }
|
||||
public bool LeftRotate90 { get; set; }
|
||||
public TKey RightControllerStickButton { get; set; }
|
||||
|
||||
public float DeadzoneLeft
|
||||
{
|
||||
get => _deadzoneLeft;
|
||||
set
|
||||
{
|
||||
_deadzoneLeft = MathF.Round(value, 3);
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public float RangeLeft
|
||||
{
|
||||
get => _rangeLeft;
|
||||
set
|
||||
{
|
||||
_rangeLeft = MathF.Round(value, 3);
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public float DeadzoneRight
|
||||
{
|
||||
get => _deadzoneRight;
|
||||
set
|
||||
{
|
||||
_deadzoneRight = MathF.Round(value, 3);
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public float RangeRight
|
||||
{
|
||||
get => _rangeRight;
|
||||
set
|
||||
{
|
||||
_rangeRight = MathF.Round(value, 3);
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public float TriggerThreshold
|
||||
{
|
||||
get => _triggerThreshold;
|
||||
set
|
||||
{
|
||||
_triggerThreshold = MathF.Round(value, 3);
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public MotionInputBackendType MotionBackend { get; set; }
|
||||
|
||||
public TKey ButtonMinus { get; set; }
|
||||
public TKey ButtonL { get; set; }
|
||||
public TKey ButtonZl { get; set; }
|
||||
public TKey LeftButtonSl { get; set; }
|
||||
public TKey LeftButtonSr { get; set; }
|
||||
public TKey DpadUp { get; set; }
|
||||
public TKey DpadDown { get; set; }
|
||||
public TKey DpadLeft { get; set; }
|
||||
public TKey DpadRight { get; set; }
|
||||
|
||||
public TKey ButtonPlus { get; set; }
|
||||
public TKey ButtonR { get; set; }
|
||||
public TKey ButtonZr { get; set; }
|
||||
public TKey RightButtonSl { get; set; }
|
||||
public TKey RightButtonSr { get; set; }
|
||||
public TKey ButtonX { get; set; }
|
||||
public TKey ButtonB { get; set; }
|
||||
public TKey ButtonY { get; set; }
|
||||
public TKey ButtonA { get; set; }
|
||||
|
||||
public TKey LeftStickUp { get; set; }
|
||||
public TKey LeftStickDown { get; set; }
|
||||
public TKey LeftStickLeft { get; set; }
|
||||
public TKey LeftStickRight { get; set; }
|
||||
public TKey LeftKeyboardStickButton { get; set; }
|
||||
|
||||
public TKey RightStickUp { get; set; }
|
||||
public TKey RightStickDown { get; set; }
|
||||
public TKey RightStickLeft { get; set; }
|
||||
public TKey RightStickRight { get; set; }
|
||||
public TKey RightKeyboardStickButton { get; set; }
|
||||
|
||||
public int Sensitivity
|
||||
{
|
||||
get => _sensitivity;
|
||||
set
|
||||
{
|
||||
_sensitivity = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public double GyroDeadzone
|
||||
{
|
||||
get => _gyroDeadzone;
|
||||
set
|
||||
{
|
||||
_gyroDeadzone = Math.Round(value, 3);
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnableMotion
|
||||
{
|
||||
get => _enableMotion; set
|
||||
{
|
||||
_enableMotion = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnableCemuHookMotion { get; set; }
|
||||
public int Slot { get; set; }
|
||||
public int AltSlot { get; set; }
|
||||
public bool MirrorInput { get; set; }
|
||||
public string DsuServerHost { get; set; }
|
||||
public int DsuServerPort { get; set; }
|
||||
|
||||
public bool EnableRumble { get; set; }
|
||||
public float WeakRumble
|
||||
{
|
||||
get => _weakRumble; set
|
||||
{
|
||||
_weakRumble = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
public float StrongRumble
|
||||
{
|
||||
get => _strongRumble; set
|
||||
{
|
||||
_strongRumble = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public InputConfiguration(InputConfig config)
|
||||
{
|
||||
if (config != null)
|
||||
{
|
||||
Backend = config.Backend;
|
||||
Id = config.Id;
|
||||
ControllerType = config.ControllerType;
|
||||
PlayerIndex = config.PlayerIndex;
|
||||
|
||||
if (config is StandardKeyboardInputConfig keyboardConfig)
|
||||
{
|
||||
LeftStickUp = (TKey)(object)keyboardConfig.LeftJoyconStick.StickUp;
|
||||
LeftStickDown = (TKey)(object)keyboardConfig.LeftJoyconStick.StickDown;
|
||||
LeftStickLeft = (TKey)(object)keyboardConfig.LeftJoyconStick.StickLeft;
|
||||
LeftStickRight = (TKey)(object)keyboardConfig.LeftJoyconStick.StickRight;
|
||||
LeftKeyboardStickButton = (TKey)(object)keyboardConfig.LeftJoyconStick.StickButton;
|
||||
|
||||
RightStickUp = (TKey)(object)keyboardConfig.RightJoyconStick.StickUp;
|
||||
RightStickDown = (TKey)(object)keyboardConfig.RightJoyconStick.StickDown;
|
||||
RightStickLeft = (TKey)(object)keyboardConfig.RightJoyconStick.StickLeft;
|
||||
RightStickRight = (TKey)(object)keyboardConfig.RightJoyconStick.StickRight;
|
||||
RightKeyboardStickButton = (TKey)(object)keyboardConfig.RightJoyconStick.StickButton;
|
||||
|
||||
ButtonA = (TKey)(object)keyboardConfig.RightJoycon.ButtonA;
|
||||
ButtonB = (TKey)(object)keyboardConfig.RightJoycon.ButtonB;
|
||||
ButtonX = (TKey)(object)keyboardConfig.RightJoycon.ButtonX;
|
||||
ButtonY = (TKey)(object)keyboardConfig.RightJoycon.ButtonY;
|
||||
ButtonR = (TKey)(object)keyboardConfig.RightJoycon.ButtonR;
|
||||
RightButtonSl = (TKey)(object)keyboardConfig.RightJoycon.ButtonSl;
|
||||
RightButtonSr = (TKey)(object)keyboardConfig.RightJoycon.ButtonSr;
|
||||
ButtonZr = (TKey)(object)keyboardConfig.RightJoycon.ButtonZr;
|
||||
ButtonPlus = (TKey)(object)keyboardConfig.RightJoycon.ButtonPlus;
|
||||
|
||||
DpadUp = (TKey)(object)keyboardConfig.LeftJoycon.DpadUp;
|
||||
DpadDown = (TKey)(object)keyboardConfig.LeftJoycon.DpadDown;
|
||||
DpadLeft = (TKey)(object)keyboardConfig.LeftJoycon.DpadLeft;
|
||||
DpadRight = (TKey)(object)keyboardConfig.LeftJoycon.DpadRight;
|
||||
ButtonMinus = (TKey)(object)keyboardConfig.LeftJoycon.ButtonMinus;
|
||||
LeftButtonSl = (TKey)(object)keyboardConfig.LeftJoycon.ButtonSl;
|
||||
LeftButtonSr = (TKey)(object)keyboardConfig.LeftJoycon.ButtonSr;
|
||||
ButtonZl = (TKey)(object)keyboardConfig.LeftJoycon.ButtonZl;
|
||||
ButtonL = (TKey)(object)keyboardConfig.LeftJoycon.ButtonL;
|
||||
}
|
||||
else if (config is StandardControllerInputConfig controllerConfig)
|
||||
{
|
||||
LeftJoystick = (TStick)(object)controllerConfig.LeftJoyconStick.Joystick;
|
||||
LeftInvertStickX = controllerConfig.LeftJoyconStick.InvertStickX;
|
||||
LeftInvertStickY = controllerConfig.LeftJoyconStick.InvertStickY;
|
||||
LeftRotate90 = controllerConfig.LeftJoyconStick.Rotate90CW;
|
||||
LeftControllerStickButton = (TKey)(object)controllerConfig.LeftJoyconStick.StickButton;
|
||||
|
||||
RightJoystick = (TStick)(object)controllerConfig.RightJoyconStick.Joystick;
|
||||
RightInvertStickX = controllerConfig.RightJoyconStick.InvertStickX;
|
||||
RightInvertStickY = controllerConfig.RightJoyconStick.InvertStickY;
|
||||
RightRotate90 = controllerConfig.RightJoyconStick.Rotate90CW;
|
||||
RightControllerStickButton = (TKey)(object)controllerConfig.RightJoyconStick.StickButton;
|
||||
|
||||
ButtonA = (TKey)(object)controllerConfig.RightJoycon.ButtonA;
|
||||
ButtonB = (TKey)(object)controllerConfig.RightJoycon.ButtonB;
|
||||
ButtonX = (TKey)(object)controllerConfig.RightJoycon.ButtonX;
|
||||
ButtonY = (TKey)(object)controllerConfig.RightJoycon.ButtonY;
|
||||
ButtonR = (TKey)(object)controllerConfig.RightJoycon.ButtonR;
|
||||
RightButtonSl = (TKey)(object)controllerConfig.RightJoycon.ButtonSl;
|
||||
RightButtonSr = (TKey)(object)controllerConfig.RightJoycon.ButtonSr;
|
||||
ButtonZr = (TKey)(object)controllerConfig.RightJoycon.ButtonZr;
|
||||
ButtonPlus = (TKey)(object)controllerConfig.RightJoycon.ButtonPlus;
|
||||
|
||||
DpadUp = (TKey)(object)controllerConfig.LeftJoycon.DpadUp;
|
||||
DpadDown = (TKey)(object)controllerConfig.LeftJoycon.DpadDown;
|
||||
DpadLeft = (TKey)(object)controllerConfig.LeftJoycon.DpadLeft;
|
||||
DpadRight = (TKey)(object)controllerConfig.LeftJoycon.DpadRight;
|
||||
ButtonMinus = (TKey)(object)controllerConfig.LeftJoycon.ButtonMinus;
|
||||
LeftButtonSl = (TKey)(object)controllerConfig.LeftJoycon.ButtonSl;
|
||||
LeftButtonSr = (TKey)(object)controllerConfig.LeftJoycon.ButtonSr;
|
||||
ButtonZl = (TKey)(object)controllerConfig.LeftJoycon.ButtonZl;
|
||||
ButtonL = (TKey)(object)controllerConfig.LeftJoycon.ButtonL;
|
||||
|
||||
DeadzoneLeft = controllerConfig.DeadzoneLeft;
|
||||
DeadzoneRight = controllerConfig.DeadzoneRight;
|
||||
RangeLeft = controllerConfig.RangeLeft;
|
||||
RangeRight = controllerConfig.RangeRight;
|
||||
TriggerThreshold = controllerConfig.TriggerThreshold;
|
||||
|
||||
if (controllerConfig.Motion != null)
|
||||
{
|
||||
EnableMotion = controllerConfig.Motion.EnableMotion;
|
||||
MotionBackend = controllerConfig.Motion.MotionBackend;
|
||||
GyroDeadzone = controllerConfig.Motion.GyroDeadzone;
|
||||
Sensitivity = controllerConfig.Motion.Sensitivity;
|
||||
|
||||
if (controllerConfig.Motion is CemuHookMotionConfigController cemuHook)
|
||||
{
|
||||
EnableCemuHookMotion = true;
|
||||
DsuServerHost = cemuHook.DsuServerHost;
|
||||
DsuServerPort = cemuHook.DsuServerPort;
|
||||
Slot = cemuHook.Slot;
|
||||
AltSlot = cemuHook.AltSlot;
|
||||
MirrorInput = cemuHook.MirrorInput;
|
||||
}
|
||||
|
||||
if (controllerConfig.Rumble != null)
|
||||
{
|
||||
EnableRumble = controllerConfig.Rumble.EnableRumble;
|
||||
WeakRumble = controllerConfig.Rumble.WeakRumble;
|
||||
StrongRumble = controllerConfig.Rumble.StrongRumble;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public InputConfiguration()
|
||||
{
|
||||
}
|
||||
|
||||
public InputConfig GetConfig()
|
||||
{
|
||||
if (Backend == InputBackendType.WindowKeyboard)
|
||||
{
|
||||
return new StandardKeyboardInputConfig
|
||||
{
|
||||
Id = Id,
|
||||
Backend = Backend,
|
||||
PlayerIndex = PlayerIndex,
|
||||
ControllerType = ControllerType,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<Key>
|
||||
{
|
||||
DpadUp = (Key)(object)DpadUp,
|
||||
DpadDown = (Key)(object)DpadDown,
|
||||
DpadLeft = (Key)(object)DpadLeft,
|
||||
DpadRight = (Key)(object)DpadRight,
|
||||
ButtonL = (Key)(object)ButtonL,
|
||||
ButtonZl = (Key)(object)ButtonZl,
|
||||
ButtonSl = (Key)(object)LeftButtonSl,
|
||||
ButtonSr = (Key)(object)LeftButtonSr,
|
||||
ButtonMinus = (Key)(object)ButtonMinus,
|
||||
},
|
||||
RightJoycon = new RightJoyconCommonConfig<Key>
|
||||
{
|
||||
ButtonA = (Key)(object)ButtonA,
|
||||
ButtonB = (Key)(object)ButtonB,
|
||||
ButtonX = (Key)(object)ButtonX,
|
||||
ButtonY = (Key)(object)ButtonY,
|
||||
ButtonPlus = (Key)(object)ButtonPlus,
|
||||
ButtonSl = (Key)(object)RightButtonSl,
|
||||
ButtonSr = (Key)(object)RightButtonSr,
|
||||
ButtonR = (Key)(object)ButtonR,
|
||||
ButtonZr = (Key)(object)ButtonZr,
|
||||
},
|
||||
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
|
||||
{
|
||||
StickUp = (Key)(object)LeftStickUp,
|
||||
StickDown = (Key)(object)LeftStickDown,
|
||||
StickRight = (Key)(object)LeftStickRight,
|
||||
StickLeft = (Key)(object)LeftStickLeft,
|
||||
StickButton = (Key)(object)LeftKeyboardStickButton,
|
||||
},
|
||||
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
|
||||
{
|
||||
StickUp = (Key)(object)RightStickUp,
|
||||
StickDown = (Key)(object)RightStickDown,
|
||||
StickLeft = (Key)(object)RightStickLeft,
|
||||
StickRight = (Key)(object)RightStickRight,
|
||||
StickButton = (Key)(object)RightKeyboardStickButton,
|
||||
},
|
||||
Version = InputConfig.CurrentVersion,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
if (Backend == InputBackendType.GamepadSDL2)
|
||||
{
|
||||
var config = new StandardControllerInputConfig
|
||||
{
|
||||
Id = Id,
|
||||
Backend = Backend,
|
||||
PlayerIndex = PlayerIndex,
|
||||
ControllerType = ControllerType,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<GamepadInputId>
|
||||
{
|
||||
DpadUp = (GamepadInputId)(object)DpadUp,
|
||||
DpadDown = (GamepadInputId)(object)DpadDown,
|
||||
DpadLeft = (GamepadInputId)(object)DpadLeft,
|
||||
DpadRight = (GamepadInputId)(object)DpadRight,
|
||||
ButtonL = (GamepadInputId)(object)ButtonL,
|
||||
ButtonZl = (GamepadInputId)(object)ButtonZl,
|
||||
ButtonSl = (GamepadInputId)(object)LeftButtonSl,
|
||||
ButtonSr = (GamepadInputId)(object)LeftButtonSr,
|
||||
ButtonMinus = (GamepadInputId)(object)ButtonMinus,
|
||||
},
|
||||
RightJoycon = new RightJoyconCommonConfig<GamepadInputId>
|
||||
{
|
||||
ButtonA = (GamepadInputId)(object)ButtonA,
|
||||
ButtonB = (GamepadInputId)(object)ButtonB,
|
||||
ButtonX = (GamepadInputId)(object)ButtonX,
|
||||
ButtonY = (GamepadInputId)(object)ButtonY,
|
||||
ButtonPlus = (GamepadInputId)(object)ButtonPlus,
|
||||
ButtonSl = (GamepadInputId)(object)RightButtonSl,
|
||||
ButtonSr = (GamepadInputId)(object)RightButtonSr,
|
||||
ButtonR = (GamepadInputId)(object)ButtonR,
|
||||
ButtonZr = (GamepadInputId)(object)ButtonZr,
|
||||
},
|
||||
LeftJoyconStick = new JoyconConfigControllerStick<GamepadInputId, StickInputId>
|
||||
{
|
||||
Joystick = (StickInputId)(object)LeftJoystick,
|
||||
InvertStickX = LeftInvertStickX,
|
||||
InvertStickY = LeftInvertStickY,
|
||||
Rotate90CW = LeftRotate90,
|
||||
StickButton = (GamepadInputId)(object)LeftControllerStickButton,
|
||||
},
|
||||
RightJoyconStick = new JoyconConfigControllerStick<GamepadInputId, StickInputId>
|
||||
{
|
||||
Joystick = (StickInputId)(object)RightJoystick,
|
||||
InvertStickX = RightInvertStickX,
|
||||
InvertStickY = RightInvertStickY,
|
||||
Rotate90CW = RightRotate90,
|
||||
StickButton = (GamepadInputId)(object)RightControllerStickButton,
|
||||
},
|
||||
Rumble = new RumbleConfigController
|
||||
{
|
||||
EnableRumble = EnableRumble,
|
||||
WeakRumble = WeakRumble,
|
||||
StrongRumble = StrongRumble,
|
||||
},
|
||||
Version = InputConfig.CurrentVersion,
|
||||
DeadzoneLeft = DeadzoneLeft,
|
||||
DeadzoneRight = DeadzoneRight,
|
||||
RangeLeft = RangeLeft,
|
||||
RangeRight = RangeRight,
|
||||
TriggerThreshold = TriggerThreshold,
|
||||
Motion = EnableCemuHookMotion
|
||||
? new CemuHookMotionConfigController
|
||||
{
|
||||
DsuServerHost = DsuServerHost,
|
||||
DsuServerPort = DsuServerPort,
|
||||
Slot = Slot,
|
||||
AltSlot = AltSlot,
|
||||
MirrorInput = MirrorInput,
|
||||
MotionBackend = MotionInputBackendType.CemuHook,
|
||||
}
|
||||
: new StandardMotionConfigController
|
||||
{
|
||||
MotionBackend = MotionInputBackendType.GamepadDriver,
|
||||
},
|
||||
};
|
||||
|
||||
config.Motion.Sensitivity = Sensitivity;
|
||||
config.Motion.EnableMotion = EnableMotion;
|
||||
config.Motion.GyroDeadzone = GyroDeadzone;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,181 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.LogicalTree;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Input;
|
||||
using Ryujinx.Input.Assigner;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Views.Input
|
||||
{
|
||||
public partial class ControllerInputView : UserControl
|
||||
{
|
||||
private bool _dialogOpen;
|
||||
|
||||
private ButtonKeyAssigner _currentAssigner;
|
||||
internal ControllerInputViewModel ViewModel { get; set; }
|
||||
|
||||
public ControllerInputView()
|
||||
{
|
||||
DataContext = ViewModel = new ControllerInputViewModel(this);
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
foreach (ILogical visual in SettingButtons.GetLogicalDescendants())
|
||||
{
|
||||
if (visual is ToggleButton button && visual is not CheckBox)
|
||||
{
|
||||
button.IsCheckedChanged += Button_IsCheckedChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
||||
{
|
||||
base.OnPointerReleased(e);
|
||||
|
||||
if (_currentAssigner != null && _currentAssigner.ToggledButton != null && !_currentAssigner.ToggledButton.IsPointerOver)
|
||||
{
|
||||
_currentAssigner.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private void Button_IsCheckedChanged(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is ToggleButton button)
|
||||
{
|
||||
if ((bool)button.IsChecked)
|
||||
{
|
||||
if (_currentAssigner != null && button == _currentAssigner.ToggledButton)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool isStick = button.Tag != null && button.Tag.ToString() == "stick";
|
||||
|
||||
if (_currentAssigner == null)
|
||||
{
|
||||
_currentAssigner = new ButtonKeyAssigner(button);
|
||||
|
||||
this.Focus(NavigationMethod.Pointer);
|
||||
|
||||
PointerPressed += MouseClick;
|
||||
|
||||
IKeyboard keyboard = (IKeyboard)ViewModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations.
|
||||
IButtonAssigner assigner = CreateButtonAssigner(isStick);
|
||||
|
||||
_currentAssigner.ButtonAssigned += (sender, e) =>
|
||||
{
|
||||
if (e.IsAssigned)
|
||||
{
|
||||
ViewModel.IsModified = true;
|
||||
}
|
||||
};
|
||||
|
||||
_currentAssigner.GetInputAndAssign(assigner, keyboard);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_currentAssigner != null)
|
||||
{
|
||||
ToggleButton oldButton = _currentAssigner.ToggledButton;
|
||||
|
||||
_currentAssigner.Cancel();
|
||||
_currentAssigner = null;
|
||||
button.IsChecked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentAssigner?.Cancel();
|
||||
_currentAssigner = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveCurrentProfile()
|
||||
{
|
||||
ViewModel.Save();
|
||||
}
|
||||
|
||||
private IButtonAssigner CreateButtonAssigner(bool forStick)
|
||||
{
|
||||
IButtonAssigner assigner;
|
||||
|
||||
var device = ViewModel.Devices[ViewModel.Device];
|
||||
|
||||
if (device.Type == DeviceType.Keyboard)
|
||||
{
|
||||
assigner = new KeyboardKeyAssigner((IKeyboard)ViewModel.SelectedGamepad);
|
||||
}
|
||||
else if (device.Type == DeviceType.Controller)
|
||||
{
|
||||
assigner = new GamepadButtonAssigner(ViewModel.SelectedGamepad, (ViewModel.Config as StandardControllerInputConfig).TriggerThreshold, forStick);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Controller not supported");
|
||||
}
|
||||
|
||||
return assigner;
|
||||
}
|
||||
|
||||
private void MouseClick(object sender, PointerPressedEventArgs e)
|
||||
{
|
||||
bool shouldUnbind = false;
|
||||
|
||||
if (e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed)
|
||||
{
|
||||
shouldUnbind = true;
|
||||
}
|
||||
|
||||
_currentAssigner?.Cancel(shouldUnbind);
|
||||
|
||||
PointerPressed -= MouseClick;
|
||||
}
|
||||
|
||||
private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (ViewModel.IsModified && !_dialogOpen)
|
||||
{
|
||||
_dialogOpen = true;
|
||||
|
||||
var result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmMessage],
|
||||
LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmSubMessage],
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
||||
|
||||
if (result == UserResult.Yes)
|
||||
{
|
||||
ViewModel.Save();
|
||||
}
|
||||
|
||||
_dialogOpen = false;
|
||||
|
||||
ViewModel.IsModified = false;
|
||||
|
||||
if (e.AddedItems.Count > 0)
|
||||
{
|
||||
var player = (PlayerModel)e.AddedItems[0];
|
||||
ViewModel.PlayerId = player.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_currentAssigner?.Cancel();
|
||||
_currentAssigner = null;
|
||||
ViewModel.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsHotkeysView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
mc:Ignorable="d"
|
||||
x:DataType="viewModels:SettingsViewModel"
|
||||
Focusable="True">
|
||||
<Design.DataContext>
|
||||
<viewModels:SettingsViewModel />
|
||||
</Design.DataContext>
|
||||
<UserControl.Resources>
|
||||
<helpers:KeyValueConverter x:Key="Key" />
|
||||
</UserControl.Resources>
|
||||
<ScrollViewer
|
||||
Name="HotkeysPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel Margin="10" Orientation="Vertical" Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabHotkeysHotkeys}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysToggleVsyncHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.ToggleVsync, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysScreenshotHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.Screenshot, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysShowUiHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.ShowUi, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysPauseHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.Pause, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysToggleMuteHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.ToggleMute, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysResScaleUpHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.ResScaleUp, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysResScaleDownHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.ResScaleDown, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysVolumeUpHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.VolumeUp, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysVolumeDownHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.VolumeDown, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
@ -1,81 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Ryujinx.Ava.Input;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Input;
|
||||
using Ryujinx.Input.Assigner;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Views.Settings
|
||||
{
|
||||
public partial class SettingsHotkeysView : UserControl
|
||||
{
|
||||
private ButtonKeyAssigner _currentAssigner;
|
||||
private readonly IGamepadDriver _avaloniaKeyboardDriver;
|
||||
|
||||
public SettingsHotkeysView()
|
||||
{
|
||||
InitializeComponent();
|
||||
_avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this);
|
||||
}
|
||||
|
||||
private void MouseClick(object sender, PointerPressedEventArgs e)
|
||||
{
|
||||
bool shouldUnbind = e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed;
|
||||
|
||||
_currentAssigner?.Cancel(shouldUnbind);
|
||||
|
||||
PointerPressed -= MouseClick;
|
||||
}
|
||||
|
||||
private void Button_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is ToggleButton button)
|
||||
{
|
||||
if (_currentAssigner != null && button == _currentAssigner.ToggledButton)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_currentAssigner == null && button.IsChecked != null && (bool)button.IsChecked)
|
||||
{
|
||||
_currentAssigner = new ButtonKeyAssigner(button);
|
||||
|
||||
this.Focus(NavigationMethod.Pointer);
|
||||
|
||||
PointerPressed += MouseClick;
|
||||
|
||||
var keyboard = (IKeyboard)_avaloniaKeyboardDriver.GetGamepad(_avaloniaKeyboardDriver.GamepadsIds[0]);
|
||||
IButtonAssigner assigner = new KeyboardKeyAssigner(keyboard);
|
||||
|
||||
_currentAssigner.GetInputAndAssign(assigner);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_currentAssigner != null)
|
||||
{
|
||||
ToggleButton oldButton = _currentAssigner.ToggledButton;
|
||||
|
||||
_currentAssigner.Cancel();
|
||||
_currentAssigner = null;
|
||||
|
||||
button.IsChecked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Button_Unchecked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_currentAssigner?.Cancel();
|
||||
_currentAssigner = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_currentAssigner?.Cancel();
|
||||
_currentAssigner = null;
|
||||
}
|
||||
}
|
||||
}
|
@ -5,10 +5,10 @@ namespace Ryujinx.Common.Collections
|
||||
/// </summary>
|
||||
public class IntrusiveRedBlackTreeNode<T> where T : IntrusiveRedBlackTreeNode<T>
|
||||
{
|
||||
internal bool Color = true;
|
||||
internal T Left;
|
||||
internal T Right;
|
||||
internal T Parent;
|
||||
public bool Color = true;
|
||||
public T Left;
|
||||
public T Right;
|
||||
public T Parent;
|
||||
|
||||
public T Predecessor => IntrusiveRedBlackTreeImpl<T>.PredecessorOf((T)this);
|
||||
public T Successor => IntrusiveRedBlackTreeImpl<T>.SuccessorOf((T)this);
|
||||
|
@ -1,13 +1,15 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Ryujinx.Common.Configuration
|
||||
{
|
||||
public static class AppDataManager
|
||||
{
|
||||
public const string DefaultBaseDir = "Ryujinx";
|
||||
public const string DefaultPortableDir = "portable";
|
||||
private const string DefaultBaseDir = "Ryujinx";
|
||||
private const string DefaultPortableDir = "portable";
|
||||
|
||||
// The following 3 are always part of Base Directory
|
||||
private const string GamesDir = "games";
|
||||
@ -29,6 +31,8 @@ namespace Ryujinx.Common.Configuration
|
||||
public static string KeysDirPath { get; private set; }
|
||||
public static string KeysDirPathUser { get; }
|
||||
|
||||
public static string LogsDirPath { get; private set; }
|
||||
|
||||
public const string DefaultNandDir = "bis";
|
||||
public const string DefaultSdcardDir = "sdcard";
|
||||
private const string DefaultModsDir = "mods";
|
||||
@ -45,15 +49,7 @@ namespace Ryujinx.Common.Configuration
|
||||
|
||||
public static void Initialize(string baseDirPath)
|
||||
{
|
||||
string appDataPath;
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support");
|
||||
}
|
||||
else
|
||||
{
|
||||
appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
}
|
||||
string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
|
||||
if (appDataPath.Length == 0)
|
||||
{
|
||||
@ -63,6 +59,17 @@ namespace Ryujinx.Common.Configuration
|
||||
string userProfilePath = Path.Combine(appDataPath, DefaultBaseDir);
|
||||
string portablePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DefaultPortableDir);
|
||||
|
||||
// On macOS, check for a portable directory next to the app bundle as well.
|
||||
if (OperatingSystem.IsMacOS() && !Directory.Exists(portablePath))
|
||||
{
|
||||
string bundlePath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", ".."));
|
||||
// Make sure we're actually running within an app bundle.
|
||||
if (bundlePath.EndsWith(".app"))
|
||||
{
|
||||
portablePath = Path.GetFullPath(Path.Combine(bundlePath, "..", DefaultPortableDir));
|
||||
}
|
||||
}
|
||||
|
||||
if (Directory.Exists(portablePath))
|
||||
{
|
||||
BaseDirPath = portablePath;
|
||||
@ -89,65 +96,227 @@ namespace Ryujinx.Common.Configuration
|
||||
|
||||
BaseDirPath = Path.GetFullPath(BaseDirPath); // convert relative paths
|
||||
|
||||
// NOTE: Moves the Ryujinx folder in `~/.config` to `~/Library/Application Support` if one is found
|
||||
// and a Ryujinx folder does not already exist in Application Support.
|
||||
// Also creates a symlink from `~/.config/Ryujinx` to `~/Library/Application Support/Ryujinx` to preserve backwards compatibility.
|
||||
// This should be removed in the future.
|
||||
if (OperatingSystem.IsMacOS() && Mode == LaunchMode.UserProfile)
|
||||
if (IsPathSymlink(BaseDirPath))
|
||||
{
|
||||
string oldConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), DefaultBaseDir);
|
||||
if (Path.Exists(oldConfigPath) && !IsPathSymlink(oldConfigPath) && !Path.Exists(BaseDirPath))
|
||||
{
|
||||
CopyDirectory(oldConfigPath, BaseDirPath);
|
||||
Directory.Delete(oldConfigPath, true);
|
||||
Directory.CreateSymbolicLink(oldConfigPath, BaseDirPath);
|
||||
}
|
||||
Logger.Warning?.Print(LogClass.Application, $"Application data directory is a symlink. This may be unintended.");
|
||||
}
|
||||
|
||||
SetupBasePaths();
|
||||
}
|
||||
|
||||
public static string GetOrCreateLogsDir()
|
||||
{
|
||||
if (Directory.Exists(LogsDirPath))
|
||||
{
|
||||
return LogsDirPath;
|
||||
}
|
||||
|
||||
Logger.Notice.Print(LogClass.Application, "Logging directory not found; attempting to create new logging directory.");
|
||||
LogsDirPath = SetUpLogsDir();
|
||||
|
||||
return LogsDirPath;
|
||||
}
|
||||
|
||||
private static string SetUpLogsDir()
|
||||
{
|
||||
string logDir = "";
|
||||
|
||||
if (Mode == LaunchMode.Portable)
|
||||
{
|
||||
logDir = Path.Combine(BaseDirPath, "Logs");
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(logDir);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, $"Logging directory could not be created '{logDir}'");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
// NOTE: Should evaluate to "~/Library/Logs/Ryujinx/".
|
||||
logDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Logs", DefaultBaseDir);
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(logDir);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, $"Logging directory could not be created '{logDir}'");
|
||||
logDir = "";
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(logDir))
|
||||
{
|
||||
// NOTE: Should evaluate to "~/Library/Application Support/Ryujinx/Logs".
|
||||
logDir = Path.Combine(BaseDirPath, "Logs");
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(logDir);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, $"Logging directory could not be created '{logDir}'");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
// NOTE: Should evaluate to a "Logs" directory in whatever directory Ryujinx was launched from.
|
||||
logDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(logDir);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, $"Logging directory could not be created '{logDir}'");
|
||||
logDir = "";
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(logDir))
|
||||
{
|
||||
// NOTE: Should evaluate to "C:\Users\user\AppData\Roaming\Ryujinx\Logs".
|
||||
logDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), DefaultBaseDir, "Logs");
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(logDir);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, $"Logging directory could not be created '{logDir}'");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (OperatingSystem.IsLinux())
|
||||
{
|
||||
// NOTE: Should evaluate to "~/.config/Ryujinx/Logs".
|
||||
logDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), DefaultBaseDir, "Logs");
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(logDir);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, $"Logging directory could not be created '{logDir}'");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return logDir;
|
||||
}
|
||||
|
||||
private static void SetupBasePaths()
|
||||
{
|
||||
Directory.CreateDirectory(BaseDirPath);
|
||||
LogsDirPath = SetUpLogsDir();
|
||||
Directory.CreateDirectory(GamesDirPath = Path.Combine(BaseDirPath, GamesDir));
|
||||
Directory.CreateDirectory(ProfilesDirPath = Path.Combine(BaseDirPath, ProfilesDir));
|
||||
Directory.CreateDirectory(KeysDirPath = Path.Combine(BaseDirPath, KeysDir));
|
||||
}
|
||||
|
||||
// Check if existing old baseDirPath is a symlink, to prevent possible errors.
|
||||
// Should be removed, when the existance of the old directory isn't checked anymore.
|
||||
// Should be removed, when the existence of the old directory isn't checked anymore.
|
||||
private static bool IsPathSymlink(string path)
|
||||
{
|
||||
FileAttributes attributes = File.GetAttributes(path);
|
||||
return (attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
|
||||
try
|
||||
{
|
||||
FileAttributes attributes = File.GetAttributes(path);
|
||||
return (attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CopyDirectory(string sourceDir, string destinationDir)
|
||||
[SupportedOSPlatform("macos")]
|
||||
public static void FixMacOSConfigurationFolders()
|
||||
{
|
||||
var dir = new DirectoryInfo(sourceDir);
|
||||
|
||||
if (!dir.Exists)
|
||||
string oldConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
".config", DefaultBaseDir);
|
||||
if (Path.Exists(oldConfigPath) && !IsPathSymlink(oldConfigPath) && !Path.Exists(BaseDirPath))
|
||||
{
|
||||
throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}");
|
||||
FileSystemUtils.MoveDirectory(oldConfigPath, BaseDirPath);
|
||||
Directory.CreateSymbolicLink(oldConfigPath, BaseDirPath);
|
||||
}
|
||||
|
||||
DirectoryInfo[] subDirs = dir.GetDirectories();
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
|
||||
foreach (FileInfo file in dir.GetFiles())
|
||||
string correctApplicationDataDirectoryPath =
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), DefaultBaseDir);
|
||||
if (IsPathSymlink(correctApplicationDataDirectoryPath))
|
||||
{
|
||||
if (file.Name == ".DS_Store")
|
||||
//copy the files somewhere temporarily
|
||||
string tempPath = Path.Combine(Path.GetTempPath(), DefaultBaseDir);
|
||||
try
|
||||
{
|
||||
continue;
|
||||
FileSystemUtils.CopyDirectory(correctApplicationDataDirectoryPath, tempPath, true);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application,
|
||||
$"Critical error copying Ryujinx application data into the temp folder. {exception}");
|
||||
try
|
||||
{
|
||||
FileSystemInfo resolvedDirectoryInfo =
|
||||
Directory.ResolveLinkTarget(correctApplicationDataDirectoryPath, true);
|
||||
string resolvedPath = resolvedDirectoryInfo.FullName;
|
||||
Logger.Error?.Print(LogClass.Application, $"Please manually move your Ryujinx data from {resolvedPath} to {correctApplicationDataDirectoryPath}, and remove the symlink.");
|
||||
}
|
||||
catch (Exception symlinkException)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Unable to resolve the symlink for Ryujinx application data: {symlinkException}. Follow the symlink at {correctApplicationDataDirectoryPath} and move your data back to the Application Support folder.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
file.CopyTo(Path.Combine(destinationDir, file.Name));
|
||||
}
|
||||
//delete the symlink
|
||||
try
|
||||
{
|
||||
//This will fail if this is an actual directory, so there is no way we can actually delete user data here.
|
||||
File.Delete(correctApplicationDataDirectoryPath);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application,
|
||||
$"Critical error deleting the Ryujinx application data folder symlink at {correctApplicationDataDirectoryPath}. {exception}");
|
||||
try
|
||||
{
|
||||
FileSystemInfo resolvedDirectoryInfo =
|
||||
Directory.ResolveLinkTarget(correctApplicationDataDirectoryPath, true);
|
||||
string resolvedPath = resolvedDirectoryInfo.FullName;
|
||||
Logger.Error?.Print(LogClass.Application, $"Please manually move your Ryujinx data from {resolvedPath} to {correctApplicationDataDirectoryPath}, and remove the symlink.");
|
||||
}
|
||||
catch (Exception symlinkException)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Unable to resolve the symlink for Ryujinx application data: {symlinkException}. Follow the symlink at {correctApplicationDataDirectoryPath} and move your data back to the Application Support folder.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (DirectoryInfo subDir in subDirs)
|
||||
{
|
||||
CopyDirectory(subDir.FullName, Path.Combine(destinationDir, subDir.Name));
|
||||
//put the files back
|
||||
try
|
||||
{
|
||||
FileSystemUtils.CopyDirectory(tempPath, correctApplicationDataDirectoryPath, true);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application,
|
||||
$"Critical error copying Ryujinx application data into the correct location. {exception}. Please manually move your application data from {tempPath} to {correctApplicationDataDirectoryPath}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,10 @@
|
||||
namespace Ryujinx.Common.Configuration.Hid
|
||||
{
|
||||
// NOTE: Please don't change this to struct.
|
||||
// This breaks Avalonia's TwoWay binding, which makes us unable to save new KeyboardHotkeys.
|
||||
public class KeyboardHotkeys
|
||||
{
|
||||
public Key ToggleVsync { get; set; }
|
||||
public Key Screenshot { get; set; }
|
||||
public Key ShowUi { get; set; }
|
||||
public Key ShowUI { get; set; }
|
||||
public Key Pause { get; set; }
|
||||
public Key ToggleMute { get; set; }
|
||||
public Key ResScaleUp { get; set; }
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user