diff --git a/.editorconfig b/.editorconfig
index 1eaf77ae6b..76edc491ce 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -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]
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 491676acb2..20bdc19d18 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -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:
diff --git a/.github/labeler.yml b/.github/labeler.yml
index b967cc776e..cd7650a9d1 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -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:
diff --git a/.github/reviewers.yml b/.github/reviewers.yml
index 052594f237..46c0d5c116 100644
--- a/.github/reviewers.yml
+++ b/.github/reviewers.yml
@@ -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:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 598f23c5ea..221c7732e9 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -20,7 +20,7 @@ jobs:
- { 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-latest, zip_os_name: osx_x64 }
+ - { name: osx-x64, os: macos-13, zip_os_name: osx_x64 }
fail-fast: false
steps:
@@ -41,12 +41,12 @@ jobs:
- 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-latest'
+ 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-latest'
+ 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
@@ -61,21 +61,21 @@ jobs:
- name: Publish Ryujinx
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-latest'
+ if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
- name: Publish Ryujinx.Headless.SDL2
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-latest'
+ if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
- - name: Publish Ryujinx.Ava
- run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -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.platform.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
+ 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
@@ -83,21 +83,21 @@ jobs:
with:
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.platform.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.platform.zip_os_name }}
path: publish_sdl2_headless
- if: github.event_name == 'pull_request' && matrix.platform.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.platform.zip_os_name }}
- path: publish_ava
- if: github.event_name == 'pull_request' && matrix.platform.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 }})
@@ -140,19 +140,19 @@ jobs:
shell: bash
if: github.event_name == 'pull_request'
- - 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 "${{ 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
diff --git a/.github/workflows/nightly_pr_comment.yml b/.github/workflows/nightly_pr_comment.yml
index f59a6be1fb..38850df06c 100644
--- a/.github/workflows/nightly_pr_comment.yml
+++ b/.github/workflows/nightly_pr_comment.yml
@@ -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 Experimental GUI (Avalonia)
\n`;
+ let hidden_gtk_artifacts = `\n\n Old GUI (GTK3)
\n`;
let hidden_headless_artifacts = `\n\n GUI-less (SDL2)
\n`;
let hidden_debug_artifacts = `\n\n Only for Developers
\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 `;
+ hidden_gtk_artifacts += `\n `;
hidden_headless_artifacts += `\n `;
hidden_debug_artifacts += `\n `;
- body += hidden_avalonia_artifacts;
+ body += hidden_gtk_artifacts;
body += hidden_headless_artifacts;
body += hidden_debug_artifacts;
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index ac598684f1..f2bebc77fc 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -44,6 +44,17 @@ 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 for ${{ matrix.platform.name }}
runs-on: ${{ matrix.platform.os }}
@@ -60,7 +71,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"
@@ -86,43 +97,37 @@ jobs:
- name: Publish
run: |
- dotnet publish -c Release -r "${{ matrix.platform.name }}" -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.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
- 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.Ava --self-contained true
- name: Packing Windows builds
if: matrix.platform.os == 'windows-latest'
run: |
- pushd publish_gtk
+ 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 }}-${{ matrix.platform.zip_os_name }}.zip publish
popd
-
- pushd publish_ava
- 7z a ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip publish
- popd
shell: bash
- name: Packing Linux builds
if: matrix.platform.os == 'ubuntu-latest'
run: |
- pushd publish_gtk
- chmod +x publish/Ryujinx.sh publish/Ryujinx
+ 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 }}-${{ matrix.platform.zip_os_name }}.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 }}-${{ matrix.platform.zip_os_name }}.tar.gz publish
- popd
shell: bash
- name: Pushing new release
@@ -183,10 +188,10 @@ jobs:
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
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 412b33a6e3..ef274125a4 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,24 +3,24 @@
true
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
-
-
+
+
@@ -42,10 +42,9 @@
-
-
+
+
-
diff --git a/README.md b/README.md
index f2f3cb001e..7f2294d311 100644
--- a/README.md
+++ b/README.md
@@ -36,8 +36,8 @@
## Compatibility
-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.
+As of May 2024, Ryujinx has been tested on approximately 4,300 titles;
+over 4,100 boot past menus and into gameplay, with roughly 3,550 of those being considered playable.
You can check out the compatibility list [here](https://github.com/Ryujinx/Ryujinx-Games-List/issues).
diff --git a/Ryujinx.sln b/Ryujinx.sln
index 47a5c714c1..b8304164d5 100644
--- a/Ryujinx.sln
+++ b/Ryujinx.sln
@@ -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,7 +69,7 @@ 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}"
EndProject
diff --git a/Ryujinx.sln.DotSettings b/Ryujinx.sln.DotSettings
index 049bdaf69d..ed7f3e9118 100644
--- a/Ryujinx.sln.DotSettings
+++ b/Ryujinx.sln.DotSettings
@@ -4,6 +4,8 @@
UseExplicitType
UseExplicitType
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="I" Suffix="" Style="AaBb" /></Policy>
+ <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>
+ True
True
True
True
diff --git a/distribution/linux/Ryujinx.sh b/distribution/linux/Ryujinx.sh
index 6cce4d213c..30eb143991 100755
--- a/distribution/linux/Ryujinx.sh
+++ b/distribution/linux/Ryujinx.sh
@@ -6,10 +6,6 @@ if [ -f "$SCRIPT_DIR/Ryujinx.Headless.SDL2" ]; then
RYUJINX_BIN="Ryujinx.Headless.SDL2"
fi
-if [ -f "$SCRIPT_DIR/Ryujinx.Ava" ]; then
- RYUJINX_BIN="Ryujinx.Ava"
-fi
-
if [ -f "$SCRIPT_DIR/Ryujinx" ]; then
RYUJINX_BIN="Ryujinx"
fi
diff --git a/distribution/macos/create_app_bundle.sh b/distribution/macos/create_app_bundle.sh
index 858c06f59a..0fa54eaddf 100755
--- a/distribution/macos/create_app_bundle.sh
+++ b/distribution/macos/create_app_bundle.sh
@@ -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
diff --git a/distribution/macos/create_macos_build_ava.sh b/distribution/macos/create_macos_build_ava.sh
index 80594a40a5..23eafc1293 100755
--- a/distribution/macos/create_macos_build_ava.sh
+++ b/distribution/macos/create_macos_build_ava.sh
@@ -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"
\ No newline at end of file
diff --git a/docs/README.md b/docs/README.md
index 2213086f67..a22da3c7cf 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -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
diff --git a/src/ARMeilleure/Instructions/InstEmitAlu32.cs b/src/ARMeilleure/Instructions/InstEmitAlu32.cs
index 3a5e71bccf..028ffbeb13 100644
--- a/src/ARMeilleure/Instructions/InstEmitAlu32.cs
+++ b/src/ARMeilleure/Instructions/InstEmitAlu32.cs
@@ -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))
diff --git a/src/ARMeilleure/Instructions/InstEmitMemoryHelper.cs b/src/ARMeilleure/Instructions/InstEmitMemoryHelper.cs
index a807eed51c..ace6fe1ce9 100644
--- a/src/ARMeilleure/Instructions/InstEmitMemoryHelper.cs
+++ b/src/ARMeilleure/Instructions/InstEmitMemoryHelper.cs
@@ -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;
diff --git a/src/ARMeilleure/Instructions/InstEmitSimdArithmetic.cs b/src/ARMeilleure/Instructions/InstEmitSimdArithmetic.cs
index 543aab0236..13d9fac683 100644
--- a/src/ARMeilleure/Instructions/InstEmitSimdArithmetic.cs
+++ b/src/ARMeilleure/Instructions/InstEmitSimdArithmetic.cs
@@ -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)
{
diff --git a/src/ARMeilleure/Instructions/NativeInterface.cs b/src/ARMeilleure/Instructions/NativeInterface.cs
index d1b2e353c5..0cd3754f7f 100644
--- a/src/ARMeilleure/Instructions/NativeInterface.cs
+++ b/src/ARMeilleure/Instructions/NativeInterface.cs
@@ -91,54 +91,54 @@ namespace ARMeilleure.Instructions
#region "Read"
public static byte ReadByte(ulong address)
{
- return GetMemoryManager().ReadTracked(address);
+ return GetMemoryManager().ReadGuest(address);
}
public static ushort ReadUInt16(ulong address)
{
- return GetMemoryManager().ReadTracked(address);
+ return GetMemoryManager().ReadGuest(address);
}
public static uint ReadUInt32(ulong address)
{
- return GetMemoryManager().ReadTracked(address);
+ return GetMemoryManager().ReadGuest(address);
}
public static ulong ReadUInt64(ulong address)
{
- return GetMemoryManager().ReadTracked(address);
+ return GetMemoryManager().ReadGuest(address);
}
public static V128 ReadVector128(ulong address)
{
- return GetMemoryManager().ReadTracked(address);
+ return GetMemoryManager().ReadGuest(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
diff --git a/src/ARMeilleure/Memory/IMemoryManager.cs b/src/ARMeilleure/Memory/IMemoryManager.cs
index 952cd2b4f0..46d4426559 100644
--- a/src/ARMeilleure/Memory/IMemoryManager.cs
+++ b/src/ARMeilleure/Memory/IMemoryManager.cs
@@ -28,6 +28,17 @@ namespace ARMeilleure.Memory
/// The data
T ReadTracked(ulong va) where T : unmanaged;
+ ///
+ /// Reads data from CPU mapped memory, from guest code. (with read tracking)
+ ///
+ /// Type of the data being read
+ /// Virtual address of the data in memory
+ /// The data
+ T ReadGuest(ulong va) where T : unmanaged
+ {
+ return ReadTracked(va);
+ }
+
///
/// Writes data to CPU mapped memory.
///
@@ -36,6 +47,17 @@ namespace ARMeilleure.Memory
/// Data to be written
void Write(ulong va, T value) where T : unmanaged;
+ ///
+ /// Writes data to CPU mapped memory, from guest code.
+ ///
+ /// Type of the data being written
+ /// Virtual address to write the data into
+ /// Data to be written
+ void WriteGuest(ulong va, T value) where T : unmanaged
+ {
+ Write(va, value);
+ }
+
///
/// Gets a read-only span of data from CPU mapped memory.
///
diff --git a/src/ARMeilleure/Memory/MemoryManagerType.cs b/src/ARMeilleure/Memory/MemoryManagerType.cs
index b1cdbb069a..bc8ae26359 100644
--- a/src/ARMeilleure/Memory/MemoryManagerType.cs
+++ b/src/ARMeilleure/Memory/MemoryManagerType.cs
@@ -29,6 +29,18 @@ namespace ARMeilleure.Memory
/// Allows invalid access from JIT code to the rest of the program, but is faster.
///
HostMappedUnsafe,
+
+ ///
+ /// High level implementation using a software flat page table for address translation
+ /// with no support for handling invalid or non-contiguous memory access.
+ ///
+ HostTracked,
+
+ ///
+ /// 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.
+ ///
+ HostTrackedUnsafe,
}
public static class MemoryManagerTypeExtensions
@@ -37,5 +49,15 @@ namespace ARMeilleure.Memory
{
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();
+ }
}
}
diff --git a/src/ARMeilleure/Signal/NativeSignalHandlerGenerator.cs b/src/ARMeilleure/Signal/NativeSignalHandlerGenerator.cs
index c5e708e169..2ec5bc1b38 100644
--- a/src/ARMeilleure/Signal/NativeSignalHandlerGenerator.cs
+++ b/src/ARMeilleure/Signal/NativeSignalHandlerGenerator.cs
@@ -21,10 +21,8 @@ namespace ARMeilleure.Signal
private const uint EXCEPTION_ACCESS_VIOLATION = 0xc0000005;
- private static Operand EmitGenericRegionCheck(EmitterContext context, IntPtr signalStructPtr, Operand faultAddress, Operand isWrite, int rangeStructSize, ulong pageSize)
+ private static Operand EmitGenericRegionCheck(EmitterContext context, IntPtr signalStructPtr, Operand faultAddress, Operand isWrite, int rangeStructSize)
{
- ulong pageMask = pageSize - 1;
-
Operand inRegionLocal = context.AllocateLocal(OperandType.I32);
context.Copy(inRegionLocal, Const(0));
@@ -51,7 +49,7 @@ namespace ARMeilleure.Signal
// 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));
@@ -62,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);
@@ -155,7 +155,7 @@ namespace ARMeilleure.Signal
throw new PlatformNotSupportedException();
}
- public static byte[] GenerateUnixSignalHandler(IntPtr signalStructPtr, int rangeStructSize, ulong pageSize)
+ public static byte[] GenerateUnixSignalHandler(IntPtr signalStructPtr, int rangeStructSize)
{
EmitterContext context = new();
@@ -168,7 +168,7 @@ namespace ARMeilleure.Signal
Operand isWrite = context.ICompareNotEqual(writeFlag, Const(0L)); // Normalize to 0/1.
- Operand isInRegion = EmitGenericRegionCheck(context, signalStructPtr, faultAddress, isWrite, rangeStructSize, pageSize);
+ Operand isInRegion = EmitGenericRegionCheck(context, signalStructPtr, faultAddress, isWrite, rangeStructSize);
Operand endLabel = Label();
@@ -203,7 +203,7 @@ namespace ARMeilleure.Signal
return Compiler.Compile(cfg, argTypes, OperandType.None, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Code;
}
- public static byte[] GenerateWindowsSignalHandler(IntPtr signalStructPtr, int rangeStructSize, ulong pageSize)
+ public static byte[] GenerateWindowsSignalHandler(IntPtr signalStructPtr, int rangeStructSize)
{
EmitterContext context = new();
@@ -232,7 +232,7 @@ namespace ARMeilleure.Signal
Operand isWrite = context.ICompareNotEqual(writeFlag, Const(0L)); // Normalize to 0/1.
- Operand isInRegion = EmitGenericRegionCheck(context, signalStructPtr, faultAddress, isWrite, rangeStructSize, pageSize);
+ Operand isInRegion = EmitGenericRegionCheck(context, signalStructPtr, faultAddress, isWrite, rangeStructSize);
Operand endLabel = Label();
@@ -256,5 +256,86 @@ namespace ARMeilleure.Signal
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();
+ }
}
}
diff --git a/src/ARMeilleure/Translation/PTC/Ptc.cs b/src/ARMeilleure/Translation/PTC/Ptc.cs
index 6f6dfcadf3..f56bdce1cd 100644
--- a/src/ARMeilleure/Translation/PTC/Ptc.cs
+++ b/src/ARMeilleure/Translation/PTC/Ptc.cs
@@ -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();
diff --git a/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs
index 00188ba58e..62fe5025d6 100644
--- a/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs
+++ b/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs
@@ -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;
@@ -87,7 +89,9 @@ namespace Ryujinx.Audio.Backends.SDL2
return;
}
- byte[] samples = new byte[frameCount * _bytesPerFrame];
+ using IMemoryOwner samplesOwner = ByteMemoryPool.Rent(frameCount * _bytesPerFrame);
+
+ Span samples = samplesOwner.Memory.Span;
_ringBuffer.Read(samples, 0, samples.Length);
diff --git a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs
index f60982e303..4011a12142 100644
--- a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs
+++ b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs
@@ -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;
@@ -37,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();
}
@@ -120,7 +122,9 @@ namespace Ryujinx.Audio.Backends.SoundIo
int channelCount = areas.Length;
- byte[] samples = new byte[frameCount * bytesPerFrame];
+ using IMemoryOwner samplesOwner = ByteMemoryPool.Rent(frameCount * bytesPerFrame);
+
+ Span samples = samplesOwner.Memory.Span;
_ringBuffer.Read(samples, 0, samples.Length);
diff --git a/src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs b/src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs
index 05dd2162a5..b95e5bed14 100644
--- a/src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs
+++ b/src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs
@@ -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 _bufferOwner;
+ private Memory _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 newBufferOwner = ByteMemoryPool.RentCleared(capacity);
+ Memory 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[] buffer, int index, int count)
+ public void Write(ReadOnlySpan 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[] buffer, int index, int count)
+ public int Read(Span 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)..]);
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Common/BehaviourParameter.cs b/src/Ryujinx.Audio/Renderer/Common/BehaviourParameter.cs
index b0963c9350..3b8d15dc53 100644
--- a/src/Ryujinx.Audio/Renderer/Common/BehaviourParameter.cs
+++ b/src/Ryujinx.Audio/Renderer/Common/BehaviourParameter.cs
@@ -25,7 +25,7 @@ namespace Ryujinx.Audio.Renderer.Common
public ulong Flags;
///
- /// Represents an error during .
+ /// Represents an error during .
///
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ErrorInfo
diff --git a/src/Ryujinx.Audio/Renderer/Common/UpdateDataHeader.cs b/src/Ryujinx.Audio/Renderer/Common/UpdateDataHeader.cs
index 7efe3b02b4..98b224ebfb 100644
--- a/src/Ryujinx.Audio/Renderer/Common/UpdateDataHeader.cs
+++ b/src/Ryujinx.Audio/Renderer/Common/UpdateDataHeader.cs
@@ -4,7 +4,7 @@ using System.Runtime.CompilerServices;
namespace Ryujinx.Audio.Renderer.Common
{
///
- /// Update data header used for input and output of .
+ /// Update data header used for input and output of .
///
public struct UpdateDataHeader
{
diff --git a/src/Ryujinx.Audio/Renderer/Parameter/BehaviourErrorInfoOutStatus.cs b/src/Ryujinx.Audio/Renderer/Parameter/BehaviourErrorInfoOutStatus.cs
index 5a0565dc61..72438be0e4 100644
--- a/src/Ryujinx.Audio/Renderer/Parameter/BehaviourErrorInfoOutStatus.cs
+++ b/src/Ryujinx.Audio/Renderer/Parameter/BehaviourErrorInfoOutStatus.cs
@@ -8,7 +8,7 @@ namespace Ryujinx.Audio.Renderer.Parameter
///
/// Output information for behaviour.
///
- /// This is used to report errors to the user during processing.
+ /// This is used to report errors to the user during processing.
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct BehaviourErrorInfoOutStatus
{
diff --git a/src/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs b/src/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs
index 7bb8ae5ba7..9b56f5cbdf 100644
--- a/src/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs
@@ -386,7 +386,7 @@ namespace Ryujinx.Audio.Renderer.Server
}
}
- public ResultCode Update(Memory output, Memory performanceOutput, ReadOnlyMemory input)
+ public ResultCode Update(Memory output, Memory performanceOutput, ReadOnlySequence 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)
{
diff --git a/src/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs b/src/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs
index 3297b5d9fa..fe1dfc4beb 100644
--- a/src/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs
@@ -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
///
/// Check if the audio renderer should fix the GC-ADPCM context not being provided to the DSP.
///
- /// True if if the audio renderer should fix it.
+ /// True if the audio renderer should fix it.
public bool IsAdpcmLoopContextBugFixed()
{
return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision2);
@@ -273,7 +274,7 @@ namespace Ryujinx.Audio.Renderer.Server
}
///
- /// Check if the audio renderer should trust the user destination count in .
+ /// Check if the audio renderer should trust the user destination count in .
///
/// True if the audio renderer should trust the user destination count.
public bool IsSplitterBugFixed()
diff --git a/src/Ryujinx.Audio/Renderer/Server/Effect/AuxiliaryBufferEffect.cs b/src/Ryujinx.Audio/Renderer/Server/Effect/AuxiliaryBufferEffect.cs
index 57ca266f4d..74a9baff2a 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Effect/AuxiliaryBufferEffect.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Effect/AuxiliaryBufferEffect.cs
@@ -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(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+ public void Update(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(parameter.SpecificData)[0];
IsEnabled = parameter.IsEnabled;
diff --git a/src/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs b/src/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs
index a9716db2a5..77d9b5c295 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs
@@ -81,7 +81,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
///
/// The user parameter.
/// Returns true if the sent by the user matches the internal .
- public bool IsTypeValid(ref T parameter) where T : unmanaged, IEffectInParameter
+ public bool IsTypeValid(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.
///
/// The user parameter.
- protected void UpdateParameterBase(ref T parameter) where T : unmanaged, IEffectInParameter
+ protected void UpdateParameterBase(in T parameter) where T : unmanaged, IEffectInParameter
{
MixId = parameter.MixId;
ProcessingOrder = parameter.ProcessingOrder;
@@ -139,7 +139,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
///
/// Initialize the given result state.
///
- /// The state to initalize
+ /// The state to initialize
public virtual void InitializeResultState(ref EffectResultState state) { }
///
@@ -155,9 +155,9 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
/// The possible that was generated.
/// The user parameter.
/// The mapper to use.
- 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
/// The possible that was generated.
/// The user parameter.
/// The mapper to use.
- 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();
}
diff --git a/src/Ryujinx.Audio/Renderer/Server/Effect/BiquadFilterEffect.cs b/src/Ryujinx.Audio/Renderer/Server/Effect/BiquadFilterEffect.cs
index b987f7c85e..3b3e1021c4 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Effect/BiquadFilterEffect.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Effect/BiquadFilterEffect.cs
@@ -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(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+ public void Update(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(parameter.SpecificData)[0];
IsEnabled = parameter.IsEnabled;
diff --git a/src/Ryujinx.Audio/Renderer/Server/Effect/BufferMixEffect.cs b/src/Ryujinx.Audio/Renderer/Server/Effect/BufferMixEffect.cs
index d6cb9cfa39..5d82b5ae87 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Effect/BufferMixEffect.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Effect/BufferMixEffect.cs
@@ -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(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+ public void Update(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(parameter.SpecificData)[0];
IsEnabled = parameter.IsEnabled;
diff --git a/src/Ryujinx.Audio/Renderer/Server/Effect/CaptureBufferEffect.cs b/src/Ryujinx.Audio/Renderer/Server/Effect/CaptureBufferEffect.cs
index 5be4b4ed51..6917222f0a 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Effect/CaptureBufferEffect.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Effect/CaptureBufferEffect.cs
@@ -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(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+ public void Update(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(parameter.SpecificData)[0];
IsEnabled = parameter.IsEnabled;
diff --git a/src/Ryujinx.Audio/Renderer/Server/Effect/CompressorEffect.cs b/src/Ryujinx.Audio/Renderer/Server/Effect/CompressorEffect.cs
index 826c32cb07..eff60e7da8 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Effect/CompressorEffect.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Effect/CompressorEffect.cs
@@ -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(parameter.SpecificData)[0];
IsEnabled = parameter.IsEnabled;
diff --git a/src/Ryujinx.Audio/Renderer/Server/Effect/DelayEffect.cs b/src/Ryujinx.Audio/Renderer/Server/Effect/DelayEffect.cs
index 43cabb7db9..9db1ce465d 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Effect/DelayEffect.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Effect/DelayEffect.cs
@@ -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(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+ public void Update(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(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;
diff --git a/src/Ryujinx.Audio/Renderer/Server/Effect/LimiterEffect.cs b/src/Ryujinx.Audio/Renderer/Server/Effect/LimiterEffect.cs
index 3e2f7326d0..d9b3d5666e 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Effect/LimiterEffect.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Effect/LimiterEffect.cs
@@ -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(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+ public void Update(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(parameter.SpecificData)[0];
updateErrorInfo = new BehaviourParameter.ErrorInfo();
- UpdateParameterBase(ref parameter);
+ UpdateParameterBase(in parameter);
Parameter = limiterParameter;
diff --git a/src/Ryujinx.Audio/Renderer/Server/Effect/Reverb3dEffect.cs b/src/Ryujinx.Audio/Renderer/Server/Effect/Reverb3dEffect.cs
index f9d7f4943c..4b13cfec62 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Effect/Reverb3dEffect.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Effect/Reverb3dEffect.cs
@@ -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(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+ public void Update(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(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;
diff --git a/src/Ryujinx.Audio/Renderer/Server/Effect/ReverbEffect.cs b/src/Ryujinx.Audio/Renderer/Server/Effect/ReverbEffect.cs
index 6fdf8fc23d..aa6e674481 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Effect/ReverbEffect.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Effect/ReverbEffect.cs
@@ -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(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+ public void Update(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(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;
diff --git a/src/Ryujinx.Audio/Renderer/Server/MemoryPool/PoolMapper.cs b/src/Ryujinx.Audio/Renderer/Server/MemoryPool/PoolMapper.cs
index 391b80f8db..f67d0c1249 100644
--- a/src/Ryujinx.Audio/Renderer/Server/MemoryPool/PoolMapper.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/MemoryPool/PoolMapper.cs
@@ -249,7 +249,7 @@ namespace Ryujinx.Audio.Renderer.Server.MemoryPool
/// Input user parameter.
/// Output user parameter.
/// Returns the of the operations performed.
- 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;
diff --git a/src/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs b/src/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs
index 88ae448314..b90574da92 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs
@@ -195,7 +195,7 @@ namespace Ryujinx.Audio.Renderer.Server.Mix
/// The input parameter of the mix.
/// The splitter context.
/// Return true, new connections were done on the adjacency matrix.
- 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
/// The splitter context.
/// The behaviour context.
/// Return true if the mix was changed.
- 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
{
diff --git a/src/Ryujinx.Audio/Renderer/Server/Sink/BaseSink.cs b/src/Ryujinx.Audio/Renderer/Server/Sink/BaseSink.cs
index d36c5e260e..8c65e09bc3 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Sink/BaseSink.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Sink/BaseSink.cs
@@ -59,7 +59,7 @@ namespace Ryujinx.Audio.Renderer.Server.Sink
///
/// The user parameter.
/// Return true, if the sent by the user match the internal .
- 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.
///
/// The user parameter.
- 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
/// The user parameter.
/// The user output status.
/// The mapper to use.
- 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();
}
diff --git a/src/Ryujinx.Audio/Renderer/Server/Sink/CircularBufferSink.cs b/src/Ryujinx.Audio/Renderer/Server/Sink/CircularBufferSink.cs
index 097757988d..f2751cf29b 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Sink/CircularBufferSink.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Sink/CircularBufferSink.cs
@@ -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(parameter.SpecificData)[0];
if (parameter.IsUsed != IsUsed || ShouldSkip)
{
- UpdateStandardParameter(ref parameter);
+ UpdateStandardParameter(in parameter);
if (parameter.IsUsed)
{
diff --git a/src/Ryujinx.Audio/Renderer/Server/Sink/DeviceSink.cs b/src/Ryujinx.Audio/Renderer/Server/Sink/DeviceSink.cs
index e03fe11d49..afe2d4b1b7 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Sink/DeviceSink.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Sink/DeviceSink.cs
@@ -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(parameter.SpecificData)[0];
if (parameter.IsUsed != IsUsed)
{
- UpdateStandardParameter(ref parameter);
+ UpdateStandardParameter(in parameter);
Parameter = inputDeviceParameter;
}
else
diff --git a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs
index e408692ab9..3efa783c37 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs
@@ -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 _splitterDestinations;
///
- /// If set to true, trust the user destination count in .
+ /// If set to true, trust the user destination count in .
///
public bool IsBugFixed { get; private set; }
@@ -110,7 +111,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
///
/// The storage.
/// The storage.
- /// If set to true, trust the user destination count in .
+ /// If set to true, trust the user destination count in .
private void Setup(Memory splitters, Memory splitterDestinations, bool isBugFixed)
{
_splitters = splitters;
@@ -148,11 +149,11 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
///
/// The splitter header.
/// The raw data after the splitter header.
- private void UpdateState(scoped ref SplitterInParameterHeader inputHeader, ref ReadOnlySpan input)
+ private void UpdateState(in SplitterInParameterHeader inputHeader, ref SequenceReader input)
{
for (int i = 0; i < inputHeader.SplitterCount; i++)
{
- SplitterInParameter parameter = MemoryMarshal.Read(input);
+ ref readonly SplitterInParameter parameter = ref input.GetRefOrRefToCopy(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()..]);
+ 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());
+ break;
}
}
}
@@ -175,11 +182,11 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
///
/// The splitter header.
/// The raw data after the splitter header.
- private void UpdateData(scoped ref SplitterInParameterHeader inputHeader, ref ReadOnlySpan input)
+ private void UpdateData(in SplitterInParameterHeader inputHeader, ref SequenceReader input)
{
for (int i = 0; i < inputHeader.SplitterDestinationCount; i++)
{
- SplitterDestinationInParameter parameter = MemoryMarshal.Read(input);
+ ref readonly SplitterDestinationInParameter parameter = ref input.GetRefOrRefToCopy(out _);
Debug.Assert(parameter.IsMagicValid());
@@ -191,8 +198,11 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
destination.Update(parameter);
}
-
- input = input[Unsafe.SizeOf()..];
+ }
+ else
+ {
+ input.Rewind(Unsafe.SizeOf());
+ break;
}
}
}
@@ -201,36 +211,33 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// Update splitter from user parameters.
///
/// The input raw user data.
- /// The total consumed size.
/// Return true if the update was successful.
- public bool Update(ReadOnlySpan input, out int consumedSize)
+ public bool Update(ref SequenceReader input)
{
if (_splitterDestinations.IsEmpty || _splitters.IsEmpty)
{
- consumedSize = 0;
-
return true;
}
- int originalSize = input.Length;
-
- SplitterInParameterHeader header = SpanIOHelper.Read(ref input);
+ ref readonly SplitterInParameterHeader header = ref input.GetRefOrRefToCopy(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());
- consumedSize = 0;
-
- return false;
+ return false;
+ }
}
///
diff --git a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs
index e08ee9ea77..944f092d2c 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs
@@ -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
/// The splitter context.
/// The user parameter.
/// The raw input data after the .
- public void Update(SplitterContext context, ref SplitterInParameter parameter, ReadOnlySpan input)
+ public void Update(SplitterContext context, in SplitterInParameter parameter, ref SequenceReader input)
{
ClearLinks();
@@ -139,9 +140,9 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
if (destinationCount > 0)
{
- ReadOnlySpan destinationIds = MemoryMarshal.Cast(input);
+ input.ReadLittleEndian(out int destinationId);
- Memory destination = context.GetDestinationMemory(destinationIds[0]);
+ Memory 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 nextDestination = context.GetDestinationMemory(destinationIds[i]);
+ input.ReadLittleEndian(out destinationId);
+
+ Memory 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)
diff --git a/src/Ryujinx.Audio/Renderer/Server/StateUpdater.cs b/src/Ryujinx.Audio/Renderer/Server/StateUpdater.cs
index 22eebc7ccc..f8d87f2d14 100644
--- a/src/Ryujinx.Audio/Renderer/Server/StateUpdater.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/StateUpdater.cs
@@ -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 _inputOrigin;
+ private SequenceReader _inputReader;
+
private readonly ReadOnlyMemory _outputOrigin;
- private ReadOnlyMemory _input;
private Memory _output;
private readonly uint _processHandle;
private BehaviourContext _behaviourContext;
- private UpdateDataHeader _inputHeader;
+ private readonly ref readonly UpdateDataHeader _inputHeader;
private readonly Memory _outputHeader;
- private ref UpdateDataHeader OutputHeader => ref _outputHeader.Span[0];
+ private readonly ref UpdateDataHeader OutputHeader => ref _outputHeader.Span[0];
- public StateUpdater(ReadOnlyMemory input, Memory output, uint processHandle, BehaviourContext behaviourContext)
+ public StateUpdater(ReadOnlySequence input, Memory output, uint processHandle, BehaviourContext behaviourContext)
{
- _input = input;
- _inputOrigin = _input;
+ _inputReader = new SequenceReader(input);
_output = output;
_outputOrigin = _output;
_processHandle = processHandle;
_behaviourContext = behaviourContext;
- _inputHeader = SpanIOHelper.Read(ref _input);
+ _inputHeader = ref _inputReader.GetRefOrRefToCopy(out _);
_outputHeader = SpanMemoryManager.Cast(_output[..Unsafe.SizeOf()]);
OutputHeader.Initialize(_behaviourContext.UserRevision);
@@ -52,7 +51,7 @@ namespace Ryujinx.Audio.Renderer.Server
public ResultCode UpdateBehaviourContext()
{
- BehaviourParameter parameter = SpanIOHelper.Read(ref _input);
+ ref readonly BehaviourParameter parameter = ref _inputReader.GetRefOrRefToCopy(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(ref _input);
+ ref readonly MemoryPoolInParameter parameter = ref _inputReader.GetRefOrRefToCopy(out _);
ref MemoryPoolOutStatus outStatus = ref SpanIOHelper.GetWriteRef(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(ref _input);
+ ref readonly VoiceChannelResourceInParameter parameter = ref _inputReader.GetRefOrRefToCopy(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 memoryPools)
+ public ResultCode UpdateVoices(VoiceContext context, PoolMapper mapper)
{
if (context.GetCount() * Unsafe.SizeOf() != _inputHeader.VoicesSize)
{
@@ -136,11 +135,7 @@ namespace Ryujinx.Audio.Renderer.Server
int initialOutputSize = _output.Length;
- ReadOnlySpan parameters = MemoryMarshal.Cast(_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(out _);
voiceUpdateStates.Fill(Memory.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(ref BaseEffect effect, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+ private static void ResetEffect(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 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 memoryPools)
+ public ResultCode UpdateEffectsVersion2(EffectContext context, bool isAudioRendererActive, PoolMapper mapper)
{
if (context.GetCount() * Unsafe.SizeOf() != _inputHeader.EffectsSize)
{
@@ -253,26 +250,22 @@ namespace Ryujinx.Audio.Renderer.Server
int initialOutputSize = _output.Length;
- ReadOnlySpan parameters = MemoryMarshal.Cast(_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(out _);
ref EffectOutStatusVersion2 outStatus = ref SpanIOHelper.GetWriteRef(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 memoryPools)
+ public ResultCode UpdateEffectsVersion1(EffectContext context, bool isAudioRendererActive, PoolMapper mapper)
{
if (context.GetCount() * Unsafe.SizeOf() != _inputHeader.EffectsSize)
{
@@ -309,26 +304,22 @@ namespace Ryujinx.Audio.Renderer.Server
int initialOutputSize = _output.Length;
- ReadOnlySpan parameters = MemoryMarshal.Cast(_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(out _);
ref EffectOutStatusVersion1 outStatus = ref SpanIOHelper.GetWriteRef(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 parameters)
+ private static bool CheckMixParametersValidity(MixContext mixContext, uint mixBufferCount, uint inputMixCount, SequenceReader 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(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(_input.Span)[0];
+ ref readonly MixInParameterDirtyOnlyUpdate parameter = ref _inputReader.GetRefOrRefToCopy(out _);
mixCount = parameter.MixCount;
@@ -411,25 +404,20 @@ namespace Ryujinx.Audio.Renderer.Server
return ResultCode.InvalidUpdateInfo;
}
- if (_behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported())
- {
- _input = _input[Unsafe.SizeOf()..];
- }
+ long initialInputConsumed = _inputReader.Consumed;
- ReadOnlySpan parameters = MemoryMarshal.Cast(_input.Span[..(int)inputMixSize]);
+ int parameterCount = (int)inputMixSize / Unsafe.SizeOf();
- _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(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 memoryPools)
+ public ResultCode UpdateSinks(SinkContext context, PoolMapper mapper)
{
- PoolMapper mapper = new(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled());
-
if (context.GetCount() * Unsafe.SizeOf() != _inputHeader.SinksSize)
{
return ResultCode.InvalidUpdateInfo;
@@ -500,22 +488,20 @@ namespace Ryujinx.Audio.Renderer.Server
int initialOutputSize = _output.Length;
- ReadOnlySpan parameters = MemoryMarshal.Cast(_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(out _);
ref SinkOutStatus outStatus = ref SpanIOHelper.GetWriteRef(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(ref _input);
+ ref readonly PerformanceInParameter parameter = ref _inputReader.GetRefOrRefToCopy(out _);
ref PerformanceOutStatus outStatus = ref SpanIOHelper.GetWriteRef(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)
diff --git a/src/Ryujinx.Audio/Renderer/Server/Voice/VoiceState.cs b/src/Ryujinx.Audio/Renderer/Server/Voice/VoiceState.cs
index 225f7d31b0..040c70e6ce 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Voice/VoiceState.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Voice/VoiceState.cs
@@ -254,7 +254,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
///
/// The user parameter.
/// Return true, if the server voice information needs to be updated.
- 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
/// The user parameter.
/// The mapper to use.
/// The behaviour context.
- 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
/// The given user output.
/// The user parameter.
/// The voice states associated to the .
- public void WriteOutStatus(ref VoiceOutStatus outStatus, ref VoiceInParameter parameter, ReadOnlySpan> voiceUpdateStates)
+ public void WriteOutStatus(ref VoiceOutStatus outStatus, in VoiceInParameter parameter, ReadOnlySpan> voiceUpdateStates)
{
#if DEBUG
// Sanity check in debug mode of the internal state
@@ -426,7 +426,12 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
/// The voice states associated to the .
/// The mapper to use.
/// The behaviour context.
- public void UpdateWaveBuffers(out ErrorInfo[] errorInfos, ref VoiceInParameter parameter, ReadOnlySpan> voiceUpdateStates, ref PoolMapper mapper, ref BehaviourContext behaviourContext)
+ public void UpdateWaveBuffers(
+ out ErrorInfo[] errorInfos,
+ in VoiceInParameter parameter,
+ ReadOnlySpan> 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
/// If set to true, the server side wavebuffer is considered valid.
/// The mapper to use.
/// The behaviour context.
- private void UpdateWaveBuffer(Span errorInfos, ref WaveBuffer waveBuffer, ref WaveBufferInternal inputWaveBuffer, SampleFormat sampleFormat, bool isValid, ref PoolMapper mapper, ref BehaviourContext behaviourContext)
+ private void UpdateWaveBuffer(
+ Span errorInfos,
+ ref WaveBuffer waveBuffer,
+ ref WaveBufferInternal inputWaveBuffer,
+ SampleFormat sampleFormat,
+ bool isValid,
+ PoolMapper mapper,
+ ref BehaviourContext behaviourContext)
{
if (!isValid && waveBuffer.IsSendToAudioProcessor && waveBuffer.BufferAddressInfo.CpuAddress != 0)
{
diff --git a/src/Ryujinx.Ava/Assets/Locales/zh_CN.json b/src/Ryujinx.Ava/Assets/Locales/zh_CN.json
deleted file mode 100644
index d09a80ec6e..0000000000
--- a/src/Ryujinx.Ava/Assets/Locales/zh_CN.json
+++ /dev/null
@@ -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": "点击这里在您的默认浏览器中打开此版本的更新日志。"
-}
\ No newline at end of file
diff --git a/src/Ryujinx.Ava/Assets/Locales/zh_TW.json b/src/Ryujinx.Ava/Assets/Locales/zh_TW.json
deleted file mode 100644
index a2f59f60dd..0000000000
--- a/src/Ryujinx.Ava/Assets/Locales/zh_TW.json
+++ /dev/null
@@ -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版本的更新日誌。"
-}
\ No newline at end of file
diff --git a/src/Ryujinx.Ava/Program.cs b/src/Ryujinx.Ava/Program.cs
deleted file mode 100644
index 702240ba3f..0000000000
--- a/src/Ryujinx.Ava/Program.cs
+++ /dev/null
@@ -1,237 +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.Version;
-
- 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()
- .UsePlatformDetect()
- .With(new X11PlatformOptions
- {
- EnableMultiTouch = true,
- EnableIme = true,
- EnableInputFocusProxy = 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, ReleaseInformation.ConfigName);
- string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName);
-
- // 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 ? "" : 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();
- }
- }
-}
diff --git a/src/Ryujinx.Ava/Ryujinx.Ava.csproj b/src/Ryujinx.Ava/Ryujinx.Ava.csproj
deleted file mode 100644
index 91c2744f0e..0000000000
--- a/src/Ryujinx.Ava/Ryujinx.Ava.csproj
+++ /dev/null
@@ -1,167 +0,0 @@
-
-
- net8.0
- win-x64;osx-x64;linux-x64
- Exe
- true
- 1.0.0-dirty
- $(DefineConstants);$(ExtraDefineConstants)
- -
- Ryujinx.Ava
- Ryujinx.ico
- true
- true
- app.manifest
-
-
-
-
-
-
-
- true
- false
- true
- partial
-
-
-
-
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Always
- alsoft.ini
-
-
- Always
- THIRDPARTY.md
-
-
- Always
- LICENSE.txt
-
-
-
-
-
- Always
-
-
- Always
- mime\Ryujinx.xml
-
-
-
-
-
- Designer
-
-
-
- MSBuild:Compile
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs b/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs
deleted file mode 100644
index 028ed6bf46..0000000000
--- a/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs
+++ /dev/null
@@ -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(value.ToString());
- }
- else if (targetType == typeof(GamepadInputId))
- {
- key = Enum.Parse(value.ToString());
- }
- else if (targetType == typeof(StickInputId))
- {
- key = Enum.Parse(value.ToString());
- }
- }
-
- return key;
- }
- }
-}
diff --git a/src/Ryujinx.Ava/UI/Models/InputConfiguration.cs b/src/Ryujinx.Ava/UI/Models/InputConfiguration.cs
deleted file mode 100644
index f1352c6d8b..0000000000
--- a/src/Ryujinx.Ava/UI/Models/InputConfiguration.cs
+++ /dev/null
@@ -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 : 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; }
-
- ///
- /// Controller id
- ///
- public string Id { get; set; }
-
- ///
- /// Controller's Type
- ///
- public ControllerType ControllerType { get; set; }
-
- ///
- /// Player's Index for the controller
- ///
- 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
- {
- 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
- {
- 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
- {
- StickUp = (Key)(object)LeftStickUp,
- StickDown = (Key)(object)LeftStickDown,
- StickRight = (Key)(object)LeftStickRight,
- StickLeft = (Key)(object)LeftStickLeft,
- StickButton = (Key)(object)LeftKeyboardStickButton,
- },
- RightJoyconStick = new JoyconConfigKeyboardStick
- {
- 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
- {
- 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
- {
- 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
- {
- Joystick = (StickInputId)(object)LeftJoystick,
- InvertStickX = LeftInvertStickX,
- InvertStickY = LeftInvertStickY,
- Rotate90CW = LeftRotate90,
- StickButton = (GamepadInputId)(object)LeftControllerStickButton,
- },
- RightJoyconStick = new JoyconConfigControllerStick
- {
- 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;
- }
- }
-}
diff --git a/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml.cs
deleted file mode 100644
index 3512970602..0000000000
--- a/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml.cs
+++ /dev/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();
- }
- }
-}
diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsHotkeysView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsHotkeysView.axaml
deleted file mode 100644
index b4eae01ef9..0000000000
--- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsHotkeysView.axaml
+++ /dev/null
@@ -1,103 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsHotkeysView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Settings/SettingsHotkeysView.axaml.cs
deleted file mode 100644
index b006d703f4..0000000000
--- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsHotkeysView.axaml.cs
+++ /dev/null
@@ -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;
- }
- }
-}
diff --git a/src/Ryujinx.Common/Collections/IntrusiveRedBlackTreeNode.cs b/src/Ryujinx.Common/Collections/IntrusiveRedBlackTreeNode.cs
index 8480d51ad6..29d2d0c9a8 100644
--- a/src/Ryujinx.Common/Collections/IntrusiveRedBlackTreeNode.cs
+++ b/src/Ryujinx.Common/Collections/IntrusiveRedBlackTreeNode.cs
@@ -5,10 +5,10 @@ namespace Ryujinx.Common.Collections
///
public class IntrusiveRedBlackTreeNode where T : IntrusiveRedBlackTreeNode
{
- 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.PredecessorOf((T)this);
public T Successor => IntrusiveRedBlackTreeImpl.SuccessorOf((T)this);
diff --git a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs
index e9c163cf20..0cb49ca8ce 100644
--- a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs
+++ b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs
@@ -1,7 +1,5 @@
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; }
diff --git a/src/Ryujinx.Common/Extensions/SequenceReaderExtensions.cs b/src/Ryujinx.Common/Extensions/SequenceReaderExtensions.cs
new file mode 100644
index 0000000000..79b5d743b9
--- /dev/null
+++ b/src/Ryujinx.Common/Extensions/SequenceReaderExtensions.cs
@@ -0,0 +1,181 @@
+using System;
+using System.Buffers;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common.Extensions
+{
+ public static class SequenceReaderExtensions
+ {
+ ///
+ /// Dumps the entire to a file, restoring its previous location afterward.
+ /// Useful for debugging purposes.
+ ///
+ /// The to write to a file
+ /// The path and name of the file to create and dump to
+ public static void DumpToFile(this ref SequenceReader reader, string fileFullName)
+ {
+ var initialConsumed = reader.Consumed;
+
+ reader.Rewind(initialConsumed);
+
+ using (var fileStream = System.IO.File.Create(fileFullName, 4096, System.IO.FileOptions.None))
+ {
+ while (reader.End == false)
+ {
+ var span = reader.CurrentSpan;
+ fileStream.Write(span);
+ reader.Advance(span.Length);
+ }
+ }
+
+ reader.SetConsumed(initialConsumed);
+ }
+
+ ///
+ /// Returns a reference to the desired value. This ref should always be used. The argument passed in should never be used, as this is only used for storage if the value
+ /// must be copied from multiple segments held by the .
+ ///
+ /// Type to get
+ /// The to read from
+ /// A location used as storage if (and only if) the value to be read spans multiple segments
+ /// A reference to the desired value, either directly to memory in the , or to if it has been used for copying the value in to
+ ///
+ /// DO NOT use after calling this method, as it will only
+ /// contain a value if the value couldn't be referenced directly because it spans multiple segments.
+ /// To discourage use, it is recommended to call this method like the following:
+ ///
+ /// ref readonly MyStruct value = ref sequenceReader.GetRefOrRefToCopy{MyStruct}(out _);
+ ///
+ ///
+ /// The does not contain enough data to read a value of type
+ public static ref readonly T GetRefOrRefToCopy(this scoped ref SequenceReader reader, out T copyDestinationIfRequiredDoNotUse) where T : unmanaged
+ {
+ int lengthRequired = Unsafe.SizeOf();
+
+ ReadOnlySpan span = reader.UnreadSpan;
+ if (lengthRequired <= span.Length)
+ {
+ reader.Advance(lengthRequired);
+
+ copyDestinationIfRequiredDoNotUse = default;
+
+ ReadOnlySpan spanOfT = MemoryMarshal.Cast(span);
+
+ return ref spanOfT[0];
+ }
+ else
+ {
+ copyDestinationIfRequiredDoNotUse = default;
+
+ Span valueSpan = MemoryMarshal.CreateSpan(ref copyDestinationIfRequiredDoNotUse, 1);
+
+ Span valueBytesSpan = MemoryMarshal.AsBytes(valueSpan);
+
+ if (!reader.TryCopyTo(valueBytesSpan))
+ {
+ throw new ArgumentOutOfRangeException(nameof(reader), "The sequence is not long enough to read the desired value.");
+ }
+
+ reader.Advance(lengthRequired);
+
+ return ref valueSpan[0];
+ }
+ }
+
+ ///
+ /// Reads an as little endian.
+ ///
+ /// The to read from
+ /// A location to receive the read value
+ /// Thrown if there wasn't enough data for an
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ReadLittleEndian(this ref SequenceReader reader, out int value)
+ {
+ if (!reader.TryReadLittleEndian(out value))
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), "The sequence is not long enough to read the desired value.");
+ }
+ }
+
+ ///
+ /// Reads the desired unmanaged value by copying it to the specified .
+ ///
+ /// Type to read
+ /// The to read from
+ /// The target that will receive the read value
+ /// The does not contain enough data to read a value of type
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ReadUnmanaged(this ref SequenceReader reader, out T value) where T : unmanaged
+ {
+ if (!reader.TryReadUnmanaged(out value))
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), "The sequence is not long enough to read the desired value.");
+ }
+ }
+
+ ///
+ /// Sets the reader's position as bytes consumed.
+ ///
+ /// The to set the position
+ /// The number of bytes consumed
+ public static void SetConsumed(ref this SequenceReader reader, long consumed)
+ {
+ reader.Rewind(reader.Consumed);
+ reader.Advance(consumed);
+ }
+
+ ///
+ /// Try to read the given type out of the buffer if possible. Warning: this is dangerous to use with arbitrary
+ /// structs - see remarks for full details.
+ ///
+ /// Type to read
+ ///
+ /// IMPORTANT: The read is a straight copy of bits. If a struct depends on specific state of it's members to
+ /// behave correctly this can lead to exceptions, etc. If reading endian specific integers, use the explicit
+ /// overloads such as
+ ///
+ ///
+ /// True if successful. will be default if failed (due to lack of space).
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe bool TryReadUnmanaged(ref this SequenceReader reader, out T value) where T : unmanaged
+ {
+ ReadOnlySpan span = reader.UnreadSpan;
+
+ if (span.Length < sizeof(T))
+ {
+ return TryReadUnmanagedMultiSegment(ref reader, out value);
+ }
+
+ value = Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(span));
+
+ reader.Advance(sizeof(T));
+
+ return true;
+ }
+
+ private static unsafe bool TryReadUnmanagedMultiSegment(ref SequenceReader reader, out T value) where T : unmanaged
+ {
+ Debug.Assert(reader.UnreadSpan.Length < sizeof(T));
+
+ // Not enough data in the current segment, try to peek for the data we need.
+ T buffer = default;
+
+ Span tempSpan = new Span(&buffer, sizeof(T));
+
+ if (!reader.TryCopyTo(tempSpan))
+ {
+ value = default;
+ return false;
+ }
+
+ value = Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(tempSpan));
+
+ reader.Advance(sizeof(T));
+
+ return true;
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Memory/ByteMemoryPool.ByteMemoryPoolBuffer.cs b/src/Ryujinx.Common/Memory/ByteMemoryPool.ByteMemoryPoolBuffer.cs
index df3f8dc93e..05fb29ac71 100644
--- a/src/Ryujinx.Common/Memory/ByteMemoryPool.ByteMemoryPoolBuffer.cs
+++ b/src/Ryujinx.Common/Memory/ByteMemoryPool.ByteMemoryPoolBuffer.cs
@@ -4,7 +4,7 @@ using System.Threading;
namespace Ryujinx.Common.Memory
{
- public sealed partial class ByteMemoryPool
+ public partial class ByteMemoryPool
{
///
/// Represents a that wraps an array rented from
diff --git a/src/Ryujinx.Common/Memory/ByteMemoryPool.cs b/src/Ryujinx.Common/Memory/ByteMemoryPool.cs
index 071f56b136..6fd6a98aa7 100644
--- a/src/Ryujinx.Common/Memory/ByteMemoryPool.cs
+++ b/src/Ryujinx.Common/Memory/ByteMemoryPool.cs
@@ -6,24 +6,8 @@ namespace Ryujinx.Common.Memory
///
/// Provides a pool of re-usable byte array instances.
///
- public sealed partial class ByteMemoryPool
+ public static partial class ByteMemoryPool
{
- private static readonly ByteMemoryPool _shared = new();
-
- ///
- /// Constructs a instance. Private to force access through
- /// the instance.
- ///
- private ByteMemoryPool()
- {
- // No implementation
- }
-
- ///
- /// Retrieves a shared instance.
- ///
- public static ByteMemoryPool Shared => _shared;
-
///
/// Returns the maximum buffer size supported by this pool.
///
@@ -95,6 +79,20 @@ namespace Ryujinx.Common.Memory
return buffer;
}
+ ///
+ /// Copies into a newly rented byte memory buffer.
+ ///
+ /// The byte buffer to copy
+ /// A wrapping the rented memory with copied to it
+ public static IMemoryOwner RentCopy(ReadOnlySpan buffer)
+ {
+ var copy = RentImpl(buffer.Length);
+
+ buffer.CopyTo(copy.Memory.Span);
+
+ return copy;
+ }
+
private static ByteMemoryPoolBuffer RentImpl(int length)
{
if ((uint)length > Array.MaxLength)
diff --git a/src/Ryujinx.Common/Memory/SpanOrArray.cs b/src/Ryujinx.Common/Memory/SpanOrArray.cs
deleted file mode 100644
index 269ac02fd6..0000000000
--- a/src/Ryujinx.Common/Memory/SpanOrArray.cs
+++ /dev/null
@@ -1,89 +0,0 @@
-using System;
-
-namespace Ryujinx.Common.Memory
-{
- ///
- /// A struct that can represent both a Span and Array.
- /// This is useful to keep the Array representation when possible to avoid copies.
- ///
- /// Element Type
- public readonly ref struct SpanOrArray where T : unmanaged
- {
- public readonly T[] Array;
- public readonly ReadOnlySpan Span;
-
- ///
- /// Create a new SpanOrArray from an array.
- ///
- /// Array to store
- public SpanOrArray(T[] array)
- {
- Array = array;
- Span = ReadOnlySpan.Empty;
- }
-
- ///
- /// Create a new SpanOrArray from a readonly span.
- ///
- /// Span to store
- public SpanOrArray(ReadOnlySpan span)
- {
- Array = null;
- Span = span;
- }
-
- ///
- /// Return the contained array, or convert the span if necessary.
- ///
- /// An array containing the data
- public T[] ToArray()
- {
- return Array ?? Span.ToArray();
- }
-
- ///
- /// Return a ReadOnlySpan from either the array or ReadOnlySpan.
- ///
- /// A ReadOnlySpan containing the data
- public ReadOnlySpan AsSpan()
- {
- return Array ?? Span;
- }
-
- ///
- /// Cast an array to a SpanOrArray.
- ///
- /// Source array
- public static implicit operator SpanOrArray(T[] array)
- {
- return new SpanOrArray(array);
- }
-
- ///
- /// Cast a ReadOnlySpan to a SpanOrArray.
- ///
- /// Source ReadOnlySpan
- public static implicit operator SpanOrArray(ReadOnlySpan span)
- {
- return new SpanOrArray(span);
- }
-
- ///
- /// Cast a Span to a SpanOrArray.
- ///
- /// Source Span
- public static implicit operator SpanOrArray(Span span)
- {
- return new SpanOrArray(span);
- }
-
- ///
- /// Cast a SpanOrArray to a ReadOnlySpan
- ///
- /// Source SpanOrArray
- public static implicit operator ReadOnlySpan(SpanOrArray spanOrArray)
- {
- return spanOrArray.AsSpan();
- }
- }
-}
diff --git a/src/Ryujinx.Common/Utilities/EmbeddedResources.cs b/src/Ryujinx.Common/Utilities/EmbeddedResources.cs
index a4facc2e37..e22571c966 100644
--- a/src/Ryujinx.Common/Utilities/EmbeddedResources.cs
+++ b/src/Ryujinx.Common/Utilities/EmbeddedResources.cs
@@ -1,5 +1,6 @@
using Ryujinx.Common.Utilities;
using System;
+using System.Buffers;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -41,6 +42,22 @@ namespace Ryujinx.Common
return StreamUtils.StreamToBytes(stream);
}
+ public static IMemoryOwner ReadFileToRentedMemory(string filename)
+ {
+ var (assembly, path) = ResolveManifestPath(filename);
+
+ return ReadFileToRentedMemory(assembly, path);
+ }
+
+ public static IMemoryOwner ReadFileToRentedMemory(Assembly assembly, string filename)
+ {
+ using var stream = GetStream(assembly, filename);
+
+ return stream is null
+ ? null
+ : StreamUtils.StreamToRentedMemory(stream);
+ }
+
public async static Task ReadAsync(Assembly assembly, string filename)
{
using var stream = GetStream(assembly, filename);
diff --git a/src/Ryujinx.Common/Utilities/FileSystemUtils.cs b/src/Ryujinx.Common/Utilities/FileSystemUtils.cs
index e76c2b60bf..a57fa8a788 100644
--- a/src/Ryujinx.Common/Utilities/FileSystemUtils.cs
+++ b/src/Ryujinx.Common/Utilities/FileSystemUtils.cs
@@ -1,4 +1,6 @@
+using System.Collections.Generic;
using System.IO;
+using System.Linq;
namespace Ryujinx.Common.Utilities
{
@@ -44,5 +46,11 @@ namespace Ryujinx.Common.Utilities
CopyDirectory(sourceDir, destinationDir, true);
Directory.Delete(sourceDir, true);
}
+
+ public static string SanitizeFileName(string fileName)
+ {
+ var reservedChars = new HashSet(Path.GetInvalidFileNameChars());
+ return string.Concat(fileName.Select(c => reservedChars.Contains(c) ? '_' : c));
+ }
}
}
diff --git a/src/Ryujinx.Common/Utilities/StreamUtils.cs b/src/Ryujinx.Common/Utilities/StreamUtils.cs
index 7a20c98e95..74b6af5ecf 100644
--- a/src/Ryujinx.Common/Utilities/StreamUtils.cs
+++ b/src/Ryujinx.Common/Utilities/StreamUtils.cs
@@ -1,4 +1,6 @@
+using Microsoft.IO;
using Ryujinx.Common.Memory;
+using System.Buffers;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@@ -9,12 +11,50 @@ namespace Ryujinx.Common.Utilities
{
public static byte[] StreamToBytes(Stream input)
{
- using MemoryStream stream = MemoryStreamManager.Shared.GetStream();
+ using RecyclableMemoryStream output = StreamToRecyclableMemoryStream(input);
+ return output.ToArray();
+ }
- input.CopyTo(stream);
+ public static IMemoryOwner StreamToRentedMemory(Stream input)
+ {
+ if (input is MemoryStream inputMemoryStream)
+ {
+ return MemoryStreamToRentedMemory(inputMemoryStream);
+ }
+ else if (input.CanSeek)
+ {
+ long bytesExpected = input.Length;
- return stream.ToArray();
+ IMemoryOwner ownedMemory = ByteMemoryPool.Rent(bytesExpected);
+
+ var destSpan = ownedMemory.Memory.Span;
+
+ int totalBytesRead = 0;
+
+ while (totalBytesRead < bytesExpected)
+ {
+ int bytesRead = input.Read(destSpan[totalBytesRead..]);
+
+ if (bytesRead == 0)
+ {
+ ownedMemory.Dispose();
+
+ throw new IOException($"Tried reading {bytesExpected} but the stream closed after reading {totalBytesRead}.");
+ }
+
+ totalBytesRead += bytesRead;
+ }
+
+ return ownedMemory;
+ }
+ else
+ {
+ // If input is (non-seekable) then copy twice: first into a RecyclableMemoryStream, then to a rented IMemoryOwner.
+ using RecyclableMemoryStream output = StreamToRecyclableMemoryStream(input);
+
+ return MemoryStreamToRentedMemory(output);
+ }
}
public static async Task StreamToBytesAsync(Stream input, CancellationToken cancellationToken = default)
@@ -25,5 +65,26 @@ namespace Ryujinx.Common.Utilities
return stream.ToArray();
}
+
+ private static IMemoryOwner MemoryStreamToRentedMemory(MemoryStream input)
+ {
+ input.Position = 0;
+
+ IMemoryOwner ownedMemory = ByteMemoryPool.Rent(input.Length);
+
+ // Discard the return value because we assume reading a MemoryStream always succeeds completely.
+ _ = input.Read(ownedMemory.Memory.Span);
+
+ return ownedMemory;
+ }
+
+ private static RecyclableMemoryStream StreamToRecyclableMemoryStream(Stream input)
+ {
+ RecyclableMemoryStream stream = MemoryStreamManager.Shared.GetStream();
+
+ input.CopyTo(stream);
+
+ return stream;
+ }
}
}
diff --git a/src/Ryujinx.Cpu/AddressSpace.cs b/src/Ryujinx.Cpu/AddressSpace.cs
index beea14beec..6664ed1345 100644
--- a/src/Ryujinx.Cpu/AddressSpace.cs
+++ b/src/Ryujinx.Cpu/AddressSpace.cs
@@ -1,5 +1,3 @@
-using Ryujinx.Common;
-using Ryujinx.Common.Collections;
using Ryujinx.Memory;
using System;
@@ -7,175 +5,23 @@ namespace Ryujinx.Cpu
{
public class AddressSpace : IDisposable
{
- private const int DefaultBlockAlignment = 1 << 20;
-
- private enum MappingType : byte
- {
- None,
- Private,
- Shared,
- }
-
- private class Mapping : IntrusiveRedBlackTreeNode, IComparable
- {
- public ulong Address { get; private set; }
- public ulong Size { get; private set; }
- public ulong EndAddress => Address + Size;
- public MappingType Type { get; private set; }
-
- public Mapping(ulong address, ulong size, MappingType type)
- {
- Address = address;
- Size = size;
- Type = type;
- }
-
- public Mapping Split(ulong splitAddress)
- {
- ulong leftSize = splitAddress - Address;
- ulong rightSize = EndAddress - splitAddress;
-
- Mapping left = new(Address, leftSize, Type);
-
- Address = splitAddress;
- Size = rightSize;
-
- return left;
- }
-
- public void UpdateState(MappingType newType)
- {
- Type = newType;
- }
-
- public void Extend(ulong sizeDelta)
- {
- Size += sizeDelta;
- }
-
- public int CompareTo(Mapping other)
- {
- if (Address < other.Address)
- {
- return -1;
- }
- else if (Address <= other.EndAddress - 1UL)
- {
- return 0;
- }
- else
- {
- return 1;
- }
- }
- }
-
- private class PrivateMapping : IntrusiveRedBlackTreeNode, IComparable
- {
- public ulong Address { get; private set; }
- public ulong Size { get; private set; }
- public ulong EndAddress => Address + Size;
- public PrivateMemoryAllocation PrivateAllocation { get; private set; }
-
- public PrivateMapping(ulong address, ulong size, PrivateMemoryAllocation privateAllocation)
- {
- Address = address;
- Size = size;
- PrivateAllocation = privateAllocation;
- }
-
- public PrivateMapping Split(ulong splitAddress)
- {
- ulong leftSize = splitAddress - Address;
- ulong rightSize = EndAddress - splitAddress;
-
- (var leftAllocation, PrivateAllocation) = PrivateAllocation.Split(leftSize);
-
- PrivateMapping left = new(Address, leftSize, leftAllocation);
-
- Address = splitAddress;
- Size = rightSize;
-
- return left;
- }
-
- public void Map(MemoryBlock baseBlock, MemoryBlock mirrorBlock, PrivateMemoryAllocation newAllocation)
- {
- baseBlock.MapView(newAllocation.Memory, newAllocation.Offset, Address, Size);
- mirrorBlock.MapView(newAllocation.Memory, newAllocation.Offset, Address, Size);
- PrivateAllocation = newAllocation;
- }
-
- public void Unmap(MemoryBlock baseBlock, MemoryBlock mirrorBlock)
- {
- if (PrivateAllocation.IsValid)
- {
- baseBlock.UnmapView(PrivateAllocation.Memory, Address, Size);
- mirrorBlock.UnmapView(PrivateAllocation.Memory, Address, Size);
- PrivateAllocation.Dispose();
- }
-
- PrivateAllocation = default;
- }
-
- public void Extend(ulong sizeDelta)
- {
- Size += sizeDelta;
- }
-
- public int CompareTo(PrivateMapping other)
- {
- if (Address < other.Address)
- {
- return -1;
- }
- else if (Address <= other.EndAddress - 1UL)
- {
- return 0;
- }
- else
- {
- return 1;
- }
- }
- }
-
private readonly MemoryBlock _backingMemory;
- private readonly PrivateMemoryAllocator _privateMemoryAllocator;
- private readonly IntrusiveRedBlackTree _mappingTree;
- private readonly IntrusiveRedBlackTree _privateTree;
-
- private readonly object _treeLock;
-
- private readonly bool _supports4KBPages;
public MemoryBlock Base { get; }
public MemoryBlock Mirror { get; }
public ulong AddressSpaceSize { get; }
- public AddressSpace(MemoryBlock backingMemory, MemoryBlock baseMemory, MemoryBlock mirrorMemory, ulong addressSpaceSize, bool supports4KBPages)
+ public AddressSpace(MemoryBlock backingMemory, MemoryBlock baseMemory, MemoryBlock mirrorMemory, ulong addressSpaceSize)
{
- if (!supports4KBPages)
- {
- _privateMemoryAllocator = new PrivateMemoryAllocator(DefaultBlockAlignment, MemoryAllocationFlags.Mirrorable | MemoryAllocationFlags.NoMap);
- _mappingTree = new IntrusiveRedBlackTree();
- _privateTree = new IntrusiveRedBlackTree();
- _treeLock = new object();
-
- _mappingTree.Add(new Mapping(0UL, addressSpaceSize, MappingType.None));
- _privateTree.Add(new PrivateMapping(0UL, addressSpaceSize, default));
- }
-
_backingMemory = backingMemory;
- _supports4KBPages = supports4KBPages;
Base = baseMemory;
Mirror = mirrorMemory;
AddressSpaceSize = addressSpaceSize;
}
- public static bool TryCreate(MemoryBlock backingMemory, ulong asSize, bool supports4KBPages, out AddressSpace addressSpace)
+ public static bool TryCreate(MemoryBlock backingMemory, ulong asSize, out AddressSpace addressSpace)
{
addressSpace = null;
@@ -193,7 +39,7 @@ namespace Ryujinx.Cpu
{
baseMemory = new MemoryBlock(addressSpaceSize, AsFlags);
mirrorMemory = new MemoryBlock(addressSpaceSize, AsFlags);
- addressSpace = new AddressSpace(backingMemory, baseMemory, mirrorMemory, addressSpaceSize, supports4KBPages);
+ addressSpace = new AddressSpace(backingMemory, baseMemory, mirrorMemory, addressSpaceSize);
break;
}
@@ -209,289 +55,20 @@ namespace Ryujinx.Cpu
public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags)
{
- if (_supports4KBPages)
- {
- Base.MapView(_backingMemory, pa, va, size);
- Mirror.MapView(_backingMemory, pa, va, size);
-
- return;
- }
-
- lock (_treeLock)
- {
- ulong alignment = MemoryBlock.GetPageSize();
- bool isAligned = ((va | pa | size) & (alignment - 1)) == 0;
-
- if (flags.HasFlag(MemoryMapFlags.Private) && !isAligned)
- {
- Update(va, pa, size, MappingType.Private);
- }
- else
- {
- // The update method assumes that shared mappings are already aligned.
-
- if (!flags.HasFlag(MemoryMapFlags.Private))
- {
- if ((va & (alignment - 1)) != (pa & (alignment - 1)))
- {
- throw new InvalidMemoryRegionException($"Virtual address 0x{va:X} and physical address 0x{pa:X} are misaligned and can't be aligned.");
- }
-
- ulong endAddress = va + size;
- va = BitUtils.AlignDown(va, alignment);
- pa = BitUtils.AlignDown(pa, alignment);
- size = BitUtils.AlignUp(endAddress, alignment) - va;
- }
-
- Update(va, pa, size, MappingType.Shared);
- }
- }
+ Base.MapView(_backingMemory, pa, va, size);
+ Mirror.MapView(_backingMemory, pa, va, size);
}
public void Unmap(ulong va, ulong size)
{
- if (_supports4KBPages)
- {
- Base.UnmapView(_backingMemory, va, size);
- Mirror.UnmapView(_backingMemory, va, size);
-
- return;
- }
-
- lock (_treeLock)
- {
- Update(va, 0UL, size, MappingType.None);
- }
- }
-
- private void Update(ulong va, ulong pa, ulong size, MappingType type)
- {
- Mapping map = _mappingTree.GetNode(new Mapping(va, 1UL, MappingType.None));
-
- Update(map, va, pa, size, type);
- }
-
- private Mapping Update(Mapping map, ulong va, ulong pa, ulong size, MappingType type)
- {
- ulong endAddress = va + size;
-
- for (; map != null; map = map.Successor)
- {
- if (map.Address < va)
- {
- _mappingTree.Add(map.Split(va));
- }
-
- if (map.EndAddress > endAddress)
- {
- Mapping newMap = map.Split(endAddress);
- _mappingTree.Add(newMap);
- map = newMap;
- }
-
- switch (type)
- {
- case MappingType.None:
- if (map.Type == MappingType.Shared)
- {
- ulong startOffset = map.Address - va;
- ulong mapVa = va + startOffset;
- ulong mapSize = Math.Min(size - startOffset, map.Size);
- ulong mapEndAddress = mapVa + mapSize;
- ulong alignment = MemoryBlock.GetPageSize();
-
- mapVa = BitUtils.AlignDown(mapVa, alignment);
- mapEndAddress = BitUtils.AlignUp(mapEndAddress, alignment);
-
- mapSize = mapEndAddress - mapVa;
-
- Base.UnmapView(_backingMemory, mapVa, mapSize);
- Mirror.UnmapView(_backingMemory, mapVa, mapSize);
- }
- else
- {
- UnmapPrivate(va, size);
- }
- break;
- case MappingType.Private:
- if (map.Type == MappingType.Shared)
- {
- throw new InvalidMemoryRegionException($"Private mapping request at 0x{va:X} with size 0x{size:X} overlaps shared mapping at 0x{map.Address:X} with size 0x{map.Size:X}.");
- }
- else
- {
- MapPrivate(va, size);
- }
- break;
- case MappingType.Shared:
- if (map.Type != MappingType.None)
- {
- throw new InvalidMemoryRegionException($"Shared mapping request at 0x{va:X} with size 0x{size:X} overlaps mapping at 0x{map.Address:X} with size 0x{map.Size:X}.");
- }
- else
- {
- ulong startOffset = map.Address - va;
- ulong mapPa = pa + startOffset;
- ulong mapVa = va + startOffset;
- ulong mapSize = Math.Min(size - startOffset, map.Size);
-
- Base.MapView(_backingMemory, mapPa, mapVa, mapSize);
- Mirror.MapView(_backingMemory, mapPa, mapVa, mapSize);
- }
- break;
- }
-
- map.UpdateState(type);
- map = TryCoalesce(map);
-
- if (map.EndAddress >= endAddress)
- {
- break;
- }
- }
-
- return map;
- }
-
- private Mapping TryCoalesce(Mapping map)
- {
- Mapping previousMap = map.Predecessor;
- Mapping nextMap = map.Successor;
-
- if (previousMap != null && CanCoalesce(previousMap, map))
- {
- previousMap.Extend(map.Size);
- _mappingTree.Remove(map);
- map = previousMap;
- }
-
- if (nextMap != null && CanCoalesce(map, nextMap))
- {
- map.Extend(nextMap.Size);
- _mappingTree.Remove(nextMap);
- }
-
- return map;
- }
-
- private static bool CanCoalesce(Mapping left, Mapping right)
- {
- return left.Type == right.Type;
- }
-
- private void MapPrivate(ulong va, ulong size)
- {
- ulong endAddress = va + size;
-
- ulong alignment = MemoryBlock.GetPageSize();
-
- // Expand the range outwards based on page size to ensure that at least the requested region is mapped.
- ulong vaAligned = BitUtils.AlignDown(va, alignment);
- ulong endAddressAligned = BitUtils.AlignUp(endAddress, alignment);
-
- PrivateMapping map = _privateTree.GetNode(new PrivateMapping(va, 1UL, default));
-
- for (; map != null; map = map.Successor)
- {
- if (!map.PrivateAllocation.IsValid)
- {
- if (map.Address < vaAligned)
- {
- _privateTree.Add(map.Split(vaAligned));
- }
-
- if (map.EndAddress > endAddressAligned)
- {
- PrivateMapping newMap = map.Split(endAddressAligned);
- _privateTree.Add(newMap);
- map = newMap;
- }
-
- map.Map(Base, Mirror, _privateMemoryAllocator.Allocate(map.Size, MemoryBlock.GetPageSize()));
- }
-
- if (map.EndAddress >= endAddressAligned)
- {
- break;
- }
- }
- }
-
- private void UnmapPrivate(ulong va, ulong size)
- {
- ulong endAddress = va + size;
-
- ulong alignment = MemoryBlock.GetPageSize();
-
- // Shrink the range inwards based on page size to ensure we won't unmap memory that might be still in use.
- ulong vaAligned = BitUtils.AlignUp(va, alignment);
- ulong endAddressAligned = BitUtils.AlignDown(endAddress, alignment);
-
- if (endAddressAligned <= vaAligned)
- {
- return;
- }
-
- PrivateMapping map = _privateTree.GetNode(new PrivateMapping(va, 1UL, default));
-
- for (; map != null; map = map.Successor)
- {
- if (map.PrivateAllocation.IsValid)
- {
- if (map.Address < vaAligned)
- {
- _privateTree.Add(map.Split(vaAligned));
- }
-
- if (map.EndAddress > endAddressAligned)
- {
- PrivateMapping newMap = map.Split(endAddressAligned);
- _privateTree.Add(newMap);
- map = newMap;
- }
-
- map.Unmap(Base, Mirror);
- map = TryCoalesce(map);
- }
-
- if (map.EndAddress >= endAddressAligned)
- {
- break;
- }
- }
- }
-
- private PrivateMapping TryCoalesce(PrivateMapping map)
- {
- PrivateMapping previousMap = map.Predecessor;
- PrivateMapping nextMap = map.Successor;
-
- if (previousMap != null && CanCoalesce(previousMap, map))
- {
- previousMap.Extend(map.Size);
- _privateTree.Remove(map);
- map = previousMap;
- }
-
- if (nextMap != null && CanCoalesce(map, nextMap))
- {
- map.Extend(nextMap.Size);
- _privateTree.Remove(nextMap);
- }
-
- return map;
- }
-
- private static bool CanCoalesce(PrivateMapping left, PrivateMapping right)
- {
- return !left.PrivateAllocation.IsValid && !right.PrivateAllocation.IsValid;
+ Base.UnmapView(_backingMemory, va, size);
+ Mirror.UnmapView(_backingMemory, va, size);
}
public void Dispose()
{
GC.SuppressFinalize(this);
- _privateMemoryAllocator?.Dispose();
Base.Dispose();
Mirror.Dispose();
}
diff --git a/src/Ryujinx.Cpu/AppleHv/HvMemoryBlockAllocator.cs b/src/Ryujinx.Cpu/AppleHv/HvMemoryBlockAllocator.cs
index 4e3723d554..86936c5929 100644
--- a/src/Ryujinx.Cpu/AppleHv/HvMemoryBlockAllocator.cs
+++ b/src/Ryujinx.Cpu/AppleHv/HvMemoryBlockAllocator.cs
@@ -38,7 +38,7 @@ namespace Ryujinx.Cpu.AppleHv
private readonly HvIpaAllocator _ipaAllocator;
- public HvMemoryBlockAllocator(HvIpaAllocator ipaAllocator, int blockAlignment) : base(blockAlignment, MemoryAllocationFlags.None)
+ public HvMemoryBlockAllocator(HvIpaAllocator ipaAllocator, ulong blockAlignment) : base(blockAlignment, MemoryAllocationFlags.None)
{
_ipaAllocator = ipaAllocator;
}
diff --git a/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs b/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs
index 2f9743ab45..abdddb31c3 100644
--- a/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs
+++ b/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs
@@ -3,12 +3,11 @@ using Ryujinx.Memory;
using Ryujinx.Memory.Range;
using Ryujinx.Memory.Tracking;
using System;
+using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
using System.Runtime.Versioning;
-using System.Threading;
namespace Ryujinx.Cpu.AppleHv
{
@@ -16,31 +15,10 @@ namespace Ryujinx.Cpu.AppleHv
/// Represents a CPU memory manager which maps guest virtual memory directly onto the Hypervisor page table.
///
[SupportedOSPlatform("macos")]
- public class HvMemoryManager : MemoryManagerBase, IMemoryManager, IVirtualMemoryManagerTracked, IWritableBlock
+ public sealed class HvMemoryManager : VirtualMemoryManagerRefCountedBase, IMemoryManager, IVirtualMemoryManagerTracked
{
- public const int PageBits = 12;
- public const int PageSize = 1 << PageBits;
- public const int PageMask = PageSize - 1;
-
- public const int PageToPteShift = 5; // 32 pages (2 bits each) in one ulong page table entry.
- public const ulong BlockMappedMask = 0x5555555555555555; // First bit of each table entry set.
-
- private enum HostMappedPtBits : ulong
- {
- Unmapped = 0,
- Mapped,
- WriteTracked,
- ReadWriteTracked,
-
- MappedReplicated = 0x5555555555555555,
- WriteTrackedReplicated = 0xaaaaaaaaaaaaaaaa,
- ReadWriteTrackedReplicated = ulong.MaxValue,
- }
-
private readonly InvalidAccessHandler _invalidAccessHandler;
- private readonly ulong _addressSpaceSize;
-
private readonly HvAddressSpace _addressSpace;
internal HvAddressSpace AddressSpace => _addressSpace;
@@ -48,9 +26,9 @@ namespace Ryujinx.Cpu.AppleHv
private readonly MemoryBlock _backingMemory;
private readonly PageTable _pageTable;
- private readonly ulong[] _pageBitmap;
+ private readonly ManagedPageFlags _pages;
- public bool Supports4KBPages => true;
+ public bool UsesPrivateAllocations => false;
public int AddressSpaceBits { get; }
@@ -62,6 +40,8 @@ namespace Ryujinx.Cpu.AppleHv
public event Action UnmapEvent;
+ protected override ulong AddressSpaceSize { get; }
+
///
/// Creates a new instance of the Hypervisor memory manager.
///
@@ -73,7 +53,7 @@ namespace Ryujinx.Cpu.AppleHv
_backingMemory = backingMemory;
_pageTable = new PageTable();
_invalidAccessHandler = invalidAccessHandler;
- _addressSpaceSize = addressSpaceSize;
+ AddressSpaceSize = addressSpaceSize;
ulong asSize = PageSize;
int asBits = PageBits;
@@ -88,46 +68,10 @@ namespace Ryujinx.Cpu.AppleHv
AddressSpaceBits = asBits;
- _pageBitmap = new ulong[1 << (AddressSpaceBits - (PageBits + PageToPteShift))];
+ _pages = new ManagedPageFlags(AddressSpaceBits);
Tracking = new MemoryTracking(this, PageSize, invalidAccessHandler);
}
- ///
- /// Checks if the virtual address is part of the addressable space.
- ///
- /// Virtual address
- /// True if the virtual address is part of the addressable space
- private bool ValidateAddress(ulong va)
- {
- return va < _addressSpaceSize;
- }
-
- ///
- /// Checks if the combination of virtual address and size is part of the addressable space.
- ///
- /// Virtual address of the range
- /// Size of the range in bytes
- /// True if the combination of virtual address and size is part of the addressable space
- private bool ValidateAddressAndSize(ulong va, ulong size)
- {
- ulong endVa = va + size;
- return endVa >= va && endVa >= size && endVa <= _addressSpaceSize;
- }
-
- ///
- /// Ensures the combination of virtual address and size is part of the addressable space.
- ///
- /// Virtual address of the range
- /// Size of the range in bytes
- /// Throw when the memory region specified outside the addressable space
- private void AssertValidAddressAndSize(ulong va, ulong size)
- {
- if (!ValidateAddressAndSize(va, size))
- {
- throw new InvalidMemoryRegionException($"va=0x{va:X16}, size=0x{size:X16}");
- }
- }
-
///
public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags)
{
@@ -135,7 +79,7 @@ namespace Ryujinx.Cpu.AppleHv
PtMap(va, pa, size);
_addressSpace.MapUser(va, pa, size, MemoryPermission.ReadWriteExecute);
- AddMapping(va, size);
+ _pages.AddMapping(va, size);
Tracking.Map(va, size);
}
@@ -152,12 +96,6 @@ namespace Ryujinx.Cpu.AppleHv
}
}
- ///
- public void MapForeign(ulong va, nuint hostPointer, ulong size)
- {
- throw new NotSupportedException();
- }
-
///
public void Unmap(ulong va, ulong size)
{
@@ -166,7 +104,7 @@ namespace Ryujinx.Cpu.AppleHv
UnmapEvent?.Invoke(va, size);
Tracking.Unmap(va, size);
- RemoveMapping(va, size);
+ _pages.RemoveMapping(va, size);
_addressSpace.UnmapUser(va, size);
PtUnmap(va, size);
}
@@ -182,20 +120,11 @@ namespace Ryujinx.Cpu.AppleHv
}
}
- ///
- public T Read(ulong va) where T : unmanaged
- {
- return MemoryMarshal.Cast(GetSpan(va, Unsafe.SizeOf()))[0];
- }
-
- ///
- public T ReadTracked(ulong va) where T : unmanaged
+ public override T ReadTracked(ulong va)
{
try
{
- SignalMemoryTracking(va, (ulong)Unsafe.SizeOf(), false);
-
- return Read(va);
+ return base.ReadTracked(va);
}
catch (InvalidMemoryRegionException)
{
@@ -208,107 +137,11 @@ namespace Ryujinx.Cpu.AppleHv
}
}
- ///
- public void Read(ulong va, Span data)
- {
- ReadImpl(va, data);
- }
-
- ///
- public void Write(ulong va, T value) where T : unmanaged
- {
- Write(va, MemoryMarshal.Cast(MemoryMarshal.CreateSpan(ref value, 1)));
- }
-
- ///
- public void Write(ulong va, ReadOnlySpan data)
- {
- if (data.Length == 0)
- {
- return;
- }
-
- SignalMemoryTracking(va, (ulong)data.Length, true);
-
- WriteImpl(va, data);
- }
-
- ///
- public void WriteUntracked(ulong va, ReadOnlySpan data)
- {
- if (data.Length == 0)
- {
- return;
- }
-
- WriteImpl(va, data);
- }
-
- ///
- public bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan data)
- {
- if (data.Length == 0)
- {
- return false;
- }
-
- SignalMemoryTracking(va, (ulong)data.Length, false);
-
- if (IsContiguousAndMapped(va, data.Length))
- {
- var target = _backingMemory.GetSpan(GetPhysicalAddressInternal(va), data.Length);
-
- bool changed = !data.SequenceEqual(target);
-
- if (changed)
- {
- data.CopyTo(target);
- }
-
- return changed;
- }
- else
- {
- WriteImpl(va, data);
-
- return true;
- }
- }
-
- private void WriteImpl(ulong va, ReadOnlySpan data)
+ public override void Read(ulong va, Span data)
{
try
{
- AssertValidAddressAndSize(va, (ulong)data.Length);
-
- if (IsContiguousAndMapped(va, data.Length))
- {
- data.CopyTo(_backingMemory.GetSpan(GetPhysicalAddressInternal(va), data.Length));
- }
- else
- {
- int offset = 0, size;
-
- if ((va & PageMask) != 0)
- {
- ulong pa = GetPhysicalAddressChecked(va);
-
- size = Math.Min(data.Length, PageSize - (int)(va & PageMask));
-
- data[..size].CopyTo(_backingMemory.GetSpan(pa, size));
-
- offset += size;
- }
-
- for (; offset < data.Length; offset += size)
- {
- ulong pa = GetPhysicalAddressChecked(va + (ulong)offset);
-
- size = Math.Min(data.Length - offset, PageSize);
-
- data.Slice(offset, size).CopyTo(_backingMemory.GetSpan(pa, size));
- }
- }
+ base.Read(va, data);
}
catch (InvalidMemoryRegionException)
{
@@ -319,61 +152,53 @@ namespace Ryujinx.Cpu.AppleHv
}
}
- ///
- public ReadOnlySpan GetSpan(ulong va, int size, bool tracked = false)
+ public override void Write(ulong va, ReadOnlySpan data)
{
- if (size == 0)
+ try
{
- return ReadOnlySpan.Empty;
+ base.Write(va, data);
}
-
- if (tracked)
+ catch (InvalidMemoryRegionException)
{
- SignalMemoryTracking(va, (ulong)size, false);
- }
-
- if (IsContiguousAndMapped(va, size))
- {
- return _backingMemory.GetSpan(GetPhysicalAddressInternal(va), size);
- }
- else
- {
- Span data = new byte[size];
-
- ReadImpl(va, data);
-
- return data;
+ if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
+ {
+ throw;
+ }
}
}
- ///
- public WritableRegion GetWritableRegion(ulong va, int size, bool tracked = false)
+ public override void WriteUntracked(ulong va, ReadOnlySpan data)
{
- if (size == 0)
+ try
{
- return new WritableRegion(null, va, Memory.Empty);
+ base.WriteUntracked(va, data);
}
-
- if (tracked)
+ catch (InvalidMemoryRegionException)
{
- SignalMemoryTracking(va, (ulong)size, true);
- }
-
- if (IsContiguousAndMapped(va, size))
- {
- return new WritableRegion(null, va, _backingMemory.GetMemory(GetPhysicalAddressInternal(va), size));
- }
- else
- {
- Memory memory = new byte[size];
-
- ReadImpl(va, memory.Span);
-
- return new WritableRegion(this, va, memory);
+ if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
+ {
+ throw;
+ }
+ }
+ }
+
+ public override ReadOnlySequence GetReadOnlySequence(ulong va, int size, bool tracked = false)
+ {
+ try
+ {
+ return base.GetReadOnlySequence(va, size, tracked);
+ }
+ catch (InvalidMemoryRegionException)
+ {
+ if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
+ {
+ throw;
+ }
+
+ return ReadOnlySequence.Empty;
}
}
- ///
public ref T GetRef(ulong va) where T : unmanaged
{
if (!IsContiguous(va, Unsafe.SizeOf()))
@@ -386,26 +211,10 @@ namespace Ryujinx.Cpu.AppleHv
return ref _backingMemory.GetRef(GetPhysicalAddressChecked(va));
}
- ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public bool IsMapped(ulong va)
+ public override bool IsMapped(ulong va)
{
- return ValidateAddress(va) && IsMappedImpl(va);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private bool IsMappedImpl(ulong va)
- {
- ulong page = va >> PageBits;
-
- int bit = (int)((page & 31) << 1);
-
- int pageIndex = (int)(page >> PageToPteShift);
- ref ulong pageRef = ref _pageBitmap[pageIndex];
-
- ulong pte = Volatile.Read(ref pageRef);
-
- return ((pte >> bit) & 3) != 0;
+ return ValidateAddress(va) && _pages.IsMapped(va);
}
///
@@ -413,91 +222,7 @@ namespace Ryujinx.Cpu.AppleHv
{
AssertValidAddressAndSize(va, size);
- return IsRangeMappedImpl(va, size);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void GetPageBlockRange(ulong pageStart, ulong pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex)
- {
- startMask = ulong.MaxValue << ((int)(pageStart & 31) << 1);
- endMask = ulong.MaxValue >> (64 - ((int)(pageEnd & 31) << 1));
-
- pageIndex = (int)(pageStart >> PageToPteShift);
- pageEndIndex = (int)((pageEnd - 1) >> PageToPteShift);
- }
-
- private bool IsRangeMappedImpl(ulong va, ulong size)
- {
- int pages = GetPagesCount(va, size, out _);
-
- if (pages == 1)
- {
- return IsMappedImpl(va);
- }
-
- ulong pageStart = va >> PageBits;
- ulong pageEnd = pageStart + (ulong)pages;
-
- GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
-
- // Check if either bit in each 2 bit page entry is set.
- // OR the block with itself shifted down by 1, and check the first bit of each entry.
-
- ulong mask = BlockMappedMask & startMask;
-
- while (pageIndex <= pageEndIndex)
- {
- if (pageIndex == pageEndIndex)
- {
- mask &= endMask;
- }
-
- ref ulong pageRef = ref _pageBitmap[pageIndex++];
- ulong pte = Volatile.Read(ref pageRef);
-
- pte |= pte >> 1;
- if ((pte & mask) != mask)
- {
- return false;
- }
-
- mask = BlockMappedMask;
- }
-
- return true;
- }
-
- private static void ThrowMemoryNotContiguous() => throw new MemoryNotContiguousException();
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private bool IsContiguousAndMapped(ulong va, int size) => IsContiguous(va, size) && IsMapped(va);
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private bool IsContiguous(ulong va, int size)
- {
- if (!ValidateAddress(va) || !ValidateAddressAndSize(va, (ulong)size))
- {
- return false;
- }
-
- int pages = GetPagesCount(va, (uint)size, out va);
-
- for (int page = 0; page < pages - 1; page++)
- {
- if (!ValidateAddress(va + PageSize))
- {
- return false;
- }
-
- if (GetPhysicalAddressInternal(va) + PageSize != GetPhysicalAddressInternal(va + PageSize))
- {
- return false;
- }
-
- va += PageSize;
- }
-
- return true;
+ return _pages.IsRangeMapped(va, size);
}
///
@@ -576,53 +301,10 @@ namespace Ryujinx.Cpu.AppleHv
return regions;
}
- private void ReadImpl(ulong va, Span data)
- {
- if (data.Length == 0)
- {
- return;
- }
-
- try
- {
- AssertValidAddressAndSize(va, (ulong)data.Length);
-
- int offset = 0, size;
-
- if ((va & PageMask) != 0)
- {
- ulong pa = GetPhysicalAddressChecked(va);
-
- size = Math.Min(data.Length, PageSize - (int)(va & PageMask));
-
- _backingMemory.GetSpan(pa, size).CopyTo(data[..size]);
-
- offset += size;
- }
-
- for (; offset < data.Length; offset += size)
- {
- ulong pa = GetPhysicalAddressChecked(va + (ulong)offset);
-
- size = Math.Min(data.Length - offset, PageSize);
-
- _backingMemory.GetSpan(pa, size).CopyTo(data.Slice(offset, size));
- }
- }
- catch (InvalidMemoryRegionException)
- {
- if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
- {
- throw;
- }
- }
- }
-
- ///
///
/// This function also validates that the given range is both valid and mapped, and will throw if it is not.
///
- public void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null)
+ public override void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null)
{
AssertValidAddressAndSize(va, size);
@@ -632,199 +314,37 @@ namespace Ryujinx.Cpu.AppleHv
return;
}
- // Software table, used for managed memory tracking.
-
- int pages = GetPagesCount(va, size, out _);
- ulong pageStart = va >> PageBits;
-
- if (pages == 1)
- {
- ulong tag = (ulong)(write ? HostMappedPtBits.WriteTracked : HostMappedPtBits.ReadWriteTracked);
-
- int bit = (int)((pageStart & 31) << 1);
-
- int pageIndex = (int)(pageStart >> PageToPteShift);
- ref ulong pageRef = ref _pageBitmap[pageIndex];
-
- ulong pte = Volatile.Read(ref pageRef);
- ulong state = ((pte >> bit) & 3);
-
- if (state >= tag)
- {
- Tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId);
- return;
- }
- else if (state == 0)
- {
- ThrowInvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}");
- }
- }
- else
- {
- ulong pageEnd = pageStart + (ulong)pages;
-
- GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
-
- ulong mask = startMask;
-
- ulong anyTrackingTag = (ulong)HostMappedPtBits.WriteTrackedReplicated;
-
- while (pageIndex <= pageEndIndex)
- {
- if (pageIndex == pageEndIndex)
- {
- mask &= endMask;
- }
-
- ref ulong pageRef = ref _pageBitmap[pageIndex++];
-
- ulong pte = Volatile.Read(ref pageRef);
- ulong mappedMask = mask & BlockMappedMask;
-
- ulong mappedPte = pte | (pte >> 1);
- if ((mappedPte & mappedMask) != mappedMask)
- {
- ThrowInvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}");
- }
-
- pte &= mask;
- if ((pte & anyTrackingTag) != 0) // Search for any tracking.
- {
- // Writes trigger any tracking.
- // Only trigger tracking from reads if both bits are set on any page.
- if (write || (pte & (pte >> 1) & BlockMappedMask) != 0)
- {
- Tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId);
- break;
- }
- }
-
- mask = ulong.MaxValue;
- }
- }
+ _pages.SignalMemoryTracking(Tracking, va, size, write, exemptId);
}
- ///
- /// Computes the number of pages in a virtual address range.
- ///
- /// Virtual address of the range
- /// Size of the range
- /// The virtual address of the beginning of the first page
- /// This function does not differentiate between allocated and unallocated pages.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static int GetPagesCount(ulong va, ulong size, out ulong startVa)
- {
- // WARNING: Always check if ulong does not overflow during the operations.
- startVa = va & ~(ulong)PageMask;
- ulong vaSpan = (va - startVa + size + PageMask) & ~(ulong)PageMask;
-
- return (int)(vaSpan / PageSize);
- }
-
- ///
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
{
// TODO
}
///
- public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
+ public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection, bool guest)
{
- // Protection is inverted on software pages, since the default value is 0.
- protection = (~protection) & MemoryPermission.ReadAndWrite;
-
- int pages = GetPagesCount(va, size, out va);
- ulong pageStart = va >> PageBits;
-
- if (pages == 1)
+ if (guest)
{
- ulong protTag = protection switch
- {
- MemoryPermission.None => (ulong)HostMappedPtBits.Mapped,
- MemoryPermission.Write => (ulong)HostMappedPtBits.WriteTracked,
- _ => (ulong)HostMappedPtBits.ReadWriteTracked,
- };
-
- int bit = (int)((pageStart & 31) << 1);
-
- ulong tagMask = 3UL << bit;
- ulong invTagMask = ~tagMask;
-
- ulong tag = protTag << bit;
-
- int pageIndex = (int)(pageStart >> PageToPteShift);
- ref ulong pageRef = ref _pageBitmap[pageIndex];
-
- ulong pte;
-
- do
- {
- pte = Volatile.Read(ref pageRef);
- }
- while ((pte & tagMask) != 0 && Interlocked.CompareExchange(ref pageRef, (pte & invTagMask) | tag, pte) != pte);
+ _addressSpace.ReprotectUser(va, size, protection);
}
else
{
- ulong pageEnd = pageStart + (ulong)pages;
-
- GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
-
- ulong mask = startMask;
-
- ulong protTag = protection switch
- {
- MemoryPermission.None => (ulong)HostMappedPtBits.MappedReplicated,
- MemoryPermission.Write => (ulong)HostMappedPtBits.WriteTrackedReplicated,
- _ => (ulong)HostMappedPtBits.ReadWriteTrackedReplicated,
- };
-
- while (pageIndex <= pageEndIndex)
- {
- if (pageIndex == pageEndIndex)
- {
- mask &= endMask;
- }
-
- ref ulong pageRef = ref _pageBitmap[pageIndex++];
-
- ulong pte;
- ulong mappedMask;
-
- // Change the protection of all 2 bit entries that are mapped.
- do
- {
- pte = Volatile.Read(ref pageRef);
-
- mappedMask = pte | (pte >> 1);
- mappedMask |= (mappedMask & BlockMappedMask) << 1;
- mappedMask &= mask; // Only update mapped pages within the given range.
- }
- while (Interlocked.CompareExchange(ref pageRef, (pte & (~mappedMask)) | (protTag & mappedMask), pte) != pte);
-
- mask = ulong.MaxValue;
- }
+ _pages.TrackingReprotect(va, size, protection);
}
-
- protection = protection switch
- {
- MemoryPermission.None => MemoryPermission.ReadAndWrite,
- MemoryPermission.Write => MemoryPermission.Read,
- _ => MemoryPermission.None,
- };
-
- _addressSpace.ReprotectUser(va, size, protection);
}
///
- public RegionHandle BeginTracking(ulong address, ulong size, int id)
+ public RegionHandle BeginTracking(ulong address, ulong size, int id, RegionFlags flags = RegionFlags.None)
{
- return Tracking.BeginTracking(address, size, id);
+ return Tracking.BeginTracking(address, size, id, flags);
}
///
- public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable handles, ulong granularity, int id)
+ public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable handles, ulong granularity, int id, RegionFlags flags = RegionFlags.None)
{
- return Tracking.BeginGranularTracking(address, size, handles, granularity, id);
+ return Tracking.BeginGranularTracking(address, size, handles, granularity, id, flags);
}
///
@@ -833,87 +353,7 @@ namespace Ryujinx.Cpu.AppleHv
return Tracking.BeginSmartGranularTracking(address, size, granularity, id);
}
- ///
- /// Adds the given address mapping to the page table.
- ///
- /// Virtual memory address
- /// Size to be mapped
- private void AddMapping(ulong va, ulong size)
- {
- int pages = GetPagesCount(va, size, out _);
- ulong pageStart = va >> PageBits;
- ulong pageEnd = pageStart + (ulong)pages;
-
- GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
-
- ulong mask = startMask;
-
- while (pageIndex <= pageEndIndex)
- {
- if (pageIndex == pageEndIndex)
- {
- mask &= endMask;
- }
-
- ref ulong pageRef = ref _pageBitmap[pageIndex++];
-
- ulong pte;
- ulong mappedMask;
-
- // Map all 2-bit entries that are unmapped.
- do
- {
- pte = Volatile.Read(ref pageRef);
-
- mappedMask = pte | (pte >> 1);
- mappedMask |= (mappedMask & BlockMappedMask) << 1;
- mappedMask |= ~mask; // Treat everything outside the range as mapped, thus unchanged.
- }
- while (Interlocked.CompareExchange(ref pageRef, (pte & mappedMask) | (BlockMappedMask & (~mappedMask)), pte) != pte);
-
- mask = ulong.MaxValue;
- }
- }
-
- ///
- /// Removes the given address mapping from the page table.
- ///
- /// Virtual memory address
- /// Size to be unmapped
- private void RemoveMapping(ulong va, ulong size)
- {
- int pages = GetPagesCount(va, size, out _);
- ulong pageStart = va >> PageBits;
- ulong pageEnd = pageStart + (ulong)pages;
-
- GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
-
- startMask = ~startMask;
- endMask = ~endMask;
-
- ulong mask = startMask;
-
- while (pageIndex <= pageEndIndex)
- {
- if (pageIndex == pageEndIndex)
- {
- mask |= endMask;
- }
-
- ref ulong pageRef = ref _pageBitmap[pageIndex++];
- ulong pte;
-
- do
- {
- pte = Volatile.Read(ref pageRef);
- }
- while (Interlocked.CompareExchange(ref pageRef, pte & mask, pte) != pte);
-
- mask = 0;
- }
- }
-
- private ulong GetPhysicalAddressChecked(ulong va)
+ private nuint GetPhysicalAddressChecked(ulong va)
{
if (!IsMapped(va))
{
@@ -923,9 +363,9 @@ namespace Ryujinx.Cpu.AppleHv
return GetPhysicalAddressInternal(va);
}
- private ulong GetPhysicalAddressInternal(ulong va)
+ private nuint GetPhysicalAddressInternal(ulong va)
{
- return _pageTable.Read(va) + (va & PageMask);
+ return (nuint)(_pageTable.Read(va) + (va & PageMask));
}
///
@@ -936,6 +376,17 @@ namespace Ryujinx.Cpu.AppleHv
_addressSpace.Dispose();
}
- private static void ThrowInvalidMemoryRegionException(string message) => throw new InvalidMemoryRegionException(message);
+ protected override Memory GetPhysicalAddressMemory(nuint pa, int size)
+ => _backingMemory.GetMemory(pa, size);
+
+ protected override Span GetPhysicalAddressSpan(nuint pa, int size)
+ => _backingMemory.GetSpan(pa, size);
+
+ protected override nuint TranslateVirtualAddressChecked(ulong va)
+ => GetPhysicalAddressChecked(va);
+
+ protected override nuint TranslateVirtualAddressUnchecked(ulong va)
+ => GetPhysicalAddressInternal(va);
+
}
}
diff --git a/src/Ryujinx.Cpu/IVirtualMemoryManagerTracked.cs b/src/Ryujinx.Cpu/IVirtualMemoryManagerTracked.cs
index 199bff240e..e8d11ede5e 100644
--- a/src/Ryujinx.Cpu/IVirtualMemoryManagerTracked.cs
+++ b/src/Ryujinx.Cpu/IVirtualMemoryManagerTracked.cs
@@ -28,8 +28,9 @@ namespace Ryujinx.Cpu
/// CPU virtual address of the region
/// Size of the region
/// Handle ID
+ /// Region flags
/// The memory tracking handle
- RegionHandle BeginTracking(ulong address, ulong size, int id);
+ RegionHandle BeginTracking(ulong address, ulong size, int id, RegionFlags flags = RegionFlags.None);
///
/// Obtains a memory tracking handle for the given virtual region, with a specified granularity. This should be disposed when finished with.
@@ -39,8 +40,9 @@ namespace Ryujinx.Cpu
/// Handles to inherit state from or reuse. When none are present, provide null
/// Desired granularity of write tracking
/// Handle ID
+ /// Region flags
/// The memory tracking handle
- MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable handles, ulong granularity, int id);
+ MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable handles, ulong granularity, int id, RegionFlags flags = RegionFlags.None);
///
/// Obtains a smart memory tracking handle for the given virtual region, with a specified granularity. This should be disposed when finished with.
diff --git a/src/Ryujinx.Cpu/Jit/HostTracked/AddressIntrusiveRedBlackTree.cs b/src/Ryujinx.Cpu/Jit/HostTracked/AddressIntrusiveRedBlackTree.cs
new file mode 100644
index 0000000000..0e24433035
--- /dev/null
+++ b/src/Ryujinx.Cpu/Jit/HostTracked/AddressIntrusiveRedBlackTree.cs
@@ -0,0 +1,35 @@
+using Ryujinx.Common.Collections;
+using System;
+
+namespace Ryujinx.Cpu.Jit.HostTracked
+{
+ internal class AddressIntrusiveRedBlackTree : IntrusiveRedBlackTree where T : IntrusiveRedBlackTreeNode, IComparable, IComparable
+ {
+ ///
+ /// Retrieve the node that is considered equal to the specified address by the comparator.
+ ///
+ /// Address to compare with
+ /// Node that is equal to
+ public T GetNode(ulong address)
+ {
+ T node = Root;
+ while (node != null)
+ {
+ int cmp = node.CompareTo(address);
+ if (cmp < 0)
+ {
+ node = node.Left;
+ }
+ else if (cmp > 0)
+ {
+ node = node.Right;
+ }
+ else
+ {
+ return node;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartition.cs b/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartition.cs
new file mode 100644
index 0000000000..224c5edc30
--- /dev/null
+++ b/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartition.cs
@@ -0,0 +1,708 @@
+using Ryujinx.Common;
+using Ryujinx.Common.Collections;
+using Ryujinx.Memory;
+using System;
+using System.Diagnostics;
+using System.Threading;
+
+namespace Ryujinx.Cpu.Jit.HostTracked
+{
+ readonly struct PrivateRange
+ {
+ public readonly MemoryBlock Memory;
+ public readonly ulong Offset;
+ public readonly ulong Size;
+
+ public static PrivateRange Empty => new(null, 0, 0);
+
+ public PrivateRange(MemoryBlock memory, ulong offset, ulong size)
+ {
+ Memory = memory;
+ Offset = offset;
+ Size = size;
+ }
+ }
+
+ class AddressSpacePartition : IDisposable
+ {
+ public const ulong GuestPageSize = 0x1000;
+
+ private const int DefaultBlockAlignment = 1 << 20;
+
+ private enum MappingType : byte
+ {
+ None,
+ Private,
+ }
+
+ private class Mapping : IntrusiveRedBlackTreeNode, IComparable, IComparable
+ {
+ public ulong Address { get; private set; }
+ public ulong Size { get; private set; }
+ public ulong EndAddress => Address + Size;
+ public MappingType Type { get; private set; }
+
+ public Mapping(ulong address, ulong size, MappingType type)
+ {
+ Address = address;
+ Size = size;
+ Type = type;
+ }
+
+ public Mapping Split(ulong splitAddress)
+ {
+ ulong leftSize = splitAddress - Address;
+ ulong rightSize = EndAddress - splitAddress;
+
+ Mapping left = new(Address, leftSize, Type);
+
+ Address = splitAddress;
+ Size = rightSize;
+
+ return left;
+ }
+
+ public void UpdateState(MappingType newType)
+ {
+ Type = newType;
+ }
+
+ public void Extend(ulong sizeDelta)
+ {
+ Size += sizeDelta;
+ }
+
+ public int CompareTo(Mapping other)
+ {
+ if (Address < other.Address)
+ {
+ return -1;
+ }
+ else if (Address <= other.EndAddress - 1UL)
+ {
+ return 0;
+ }
+ else
+ {
+ return 1;
+ }
+ }
+
+ public int CompareTo(ulong address)
+ {
+ if (address < Address)
+ {
+ return -1;
+ }
+ else if (address <= EndAddress - 1UL)
+ {
+ return 0;
+ }
+ else
+ {
+ return 1;
+ }
+ }
+ }
+
+ private class PrivateMapping : IntrusiveRedBlackTreeNode, IComparable, IComparable
+ {
+ public ulong Address { get; private set; }
+ public ulong Size { get; private set; }
+ public ulong EndAddress => Address + Size;
+ public PrivateMemoryAllocation PrivateAllocation { get; private set; }
+
+ public PrivateMapping(ulong address, ulong size, PrivateMemoryAllocation privateAllocation)
+ {
+ Address = address;
+ Size = size;
+ PrivateAllocation = privateAllocation;
+ }
+
+ public PrivateMapping Split(ulong splitAddress)
+ {
+ ulong leftSize = splitAddress - Address;
+ ulong rightSize = EndAddress - splitAddress;
+
+ Debug.Assert(leftSize > 0);
+ Debug.Assert(rightSize > 0);
+
+ (var leftAllocation, PrivateAllocation) = PrivateAllocation.Split(leftSize);
+
+ PrivateMapping left = new(Address, leftSize, leftAllocation);
+
+ Address = splitAddress;
+ Size = rightSize;
+
+ return left;
+ }
+
+ public void Map(AddressSpacePartitionMultiAllocation baseBlock, ulong baseAddress, PrivateMemoryAllocation newAllocation)
+ {
+ baseBlock.MapView(newAllocation.Memory, newAllocation.Offset, Address - baseAddress, Size);
+ PrivateAllocation = newAllocation;
+ }
+
+ public void Unmap(AddressSpacePartitionMultiAllocation baseBlock, ulong baseAddress)
+ {
+ if (PrivateAllocation.IsValid)
+ {
+ baseBlock.UnmapView(PrivateAllocation.Memory, Address - baseAddress, Size);
+ PrivateAllocation.Dispose();
+ }
+
+ PrivateAllocation = default;
+ }
+
+ public void Extend(ulong sizeDelta)
+ {
+ Size += sizeDelta;
+ }
+
+ public int CompareTo(PrivateMapping other)
+ {
+ if (Address < other.Address)
+ {
+ return -1;
+ }
+ else if (Address <= other.EndAddress - 1UL)
+ {
+ return 0;
+ }
+ else
+ {
+ return 1;
+ }
+ }
+
+ public int CompareTo(ulong address)
+ {
+ if (address < Address)
+ {
+ return -1;
+ }
+ else if (address <= EndAddress - 1UL)
+ {
+ return 0;
+ }
+ else
+ {
+ return 1;
+ }
+ }
+ }
+
+ private readonly MemoryBlock _backingMemory;
+ private readonly AddressSpacePartitionMultiAllocation _baseMemory;
+ private readonly PrivateMemoryAllocator _privateMemoryAllocator;
+
+ private readonly AddressIntrusiveRedBlackTree _mappingTree;
+ private readonly AddressIntrusiveRedBlackTree _privateTree;
+
+ private readonly ReaderWriterLockSlim _treeLock;
+
+ private readonly ulong _hostPageSize;
+
+ private ulong? _firstPagePa;
+ private ulong? _lastPagePa;
+ private ulong _cachedFirstPagePa;
+ private ulong _cachedLastPagePa;
+ private MemoryBlock _firstPageMemoryForUnmap;
+ private ulong _firstPageOffsetForLateMap;
+ private MemoryPermission _firstPageMemoryProtection;
+
+ public ulong Address { get; }
+ public ulong Size { get; }
+ public ulong EndAddress => Address + Size;
+
+ public AddressSpacePartition(AddressSpacePartitionAllocation baseMemory, MemoryBlock backingMemory, ulong address, ulong size)
+ {
+ _privateMemoryAllocator = new PrivateMemoryAllocator(DefaultBlockAlignment, MemoryAllocationFlags.Mirrorable);
+ _mappingTree = new AddressIntrusiveRedBlackTree();
+ _privateTree = new AddressIntrusiveRedBlackTree();
+ _treeLock = new ReaderWriterLockSlim();
+
+ _mappingTree.Add(new Mapping(address, size, MappingType.None));
+ _privateTree.Add(new PrivateMapping(address, size, default));
+
+ _hostPageSize = MemoryBlock.GetPageSize();
+
+ _backingMemory = backingMemory;
+ _baseMemory = new(baseMemory);
+
+ _cachedFirstPagePa = ulong.MaxValue;
+ _cachedLastPagePa = ulong.MaxValue;
+
+ Address = address;
+ Size = size;
+ }
+
+ public bool IsEmpty()
+ {
+ _treeLock.EnterReadLock();
+
+ try
+ {
+ Mapping map = _mappingTree.GetNode(Address);
+
+ return map != null && map.Address == Address && map.Size == Size && map.Type == MappingType.None;
+ }
+ finally
+ {
+ _treeLock.ExitReadLock();
+ }
+ }
+
+ public void Map(ulong va, ulong pa, ulong size)
+ {
+ Debug.Assert(va >= Address);
+ Debug.Assert(va + size <= EndAddress);
+
+ if (va == Address)
+ {
+ _firstPagePa = pa;
+ }
+
+ if (va <= EndAddress - GuestPageSize && va + size > EndAddress - GuestPageSize)
+ {
+ _lastPagePa = pa + ((EndAddress - GuestPageSize) - va);
+ }
+
+ Update(va, pa, size, MappingType.Private);
+ }
+
+ public void Unmap(ulong va, ulong size)
+ {
+ Debug.Assert(va >= Address);
+ Debug.Assert(va + size <= EndAddress);
+
+ if (va == Address)
+ {
+ _firstPagePa = null;
+ }
+
+ if (va <= EndAddress - GuestPageSize && va + size > EndAddress - GuestPageSize)
+ {
+ _lastPagePa = null;
+ }
+
+ Update(va, 0UL, size, MappingType.None);
+ }
+
+ public void ReprotectAligned(ulong va, ulong size, MemoryPermission protection)
+ {
+ Debug.Assert(va >= Address);
+ Debug.Assert(va + size <= EndAddress);
+
+ _baseMemory.Reprotect(va - Address, size, protection, false);
+
+ if (va == Address)
+ {
+ _firstPageMemoryProtection = protection;
+ }
+ }
+
+ public void Reprotect(
+ ulong va,
+ ulong size,
+ MemoryPermission protection,
+ AddressSpacePartitioned addressSpace,
+ Action updatePtCallback)
+ {
+ if (_baseMemory.LazyInitMirrorForProtection(addressSpace, Address, Size, protection))
+ {
+ LateMap();
+ }
+
+ updatePtCallback(va, _baseMemory.GetPointerForProtection(va - Address, size, protection), size);
+ }
+
+ public IntPtr GetPointer(ulong va, ulong size)
+ {
+ Debug.Assert(va >= Address);
+ Debug.Assert(va + size <= EndAddress);
+
+ return _baseMemory.GetPointer(va - Address, size);
+ }
+
+ public void InsertBridgeAtEnd(AddressSpacePartition partitionAfter, bool useProtectionMirrors)
+ {
+ ulong firstPagePa = partitionAfter?._firstPagePa ?? ulong.MaxValue;
+ ulong lastPagePa = _lastPagePa ?? ulong.MaxValue;
+
+ if (firstPagePa != _cachedFirstPagePa || lastPagePa != _cachedLastPagePa)
+ {
+ if (partitionAfter != null && partitionAfter._firstPagePa.HasValue)
+ {
+ (MemoryBlock firstPageMemory, ulong firstPageOffset) = partitionAfter.GetFirstPageMemoryAndOffset();
+
+ _baseMemory.MapView(firstPageMemory, firstPageOffset, Size, _hostPageSize);
+
+ if (!useProtectionMirrors)
+ {
+ _baseMemory.Reprotect(Size, _hostPageSize, partitionAfter._firstPageMemoryProtection, throwOnFail: false);
+ }
+
+ _firstPageMemoryForUnmap = firstPageMemory;
+ _firstPageOffsetForLateMap = firstPageOffset;
+ }
+ else
+ {
+ MemoryBlock firstPageMemoryForUnmap = _firstPageMemoryForUnmap;
+
+ if (firstPageMemoryForUnmap != null)
+ {
+ _baseMemory.UnmapView(firstPageMemoryForUnmap, Size, _hostPageSize);
+ _firstPageMemoryForUnmap = null;
+ }
+ }
+
+ _cachedFirstPagePa = firstPagePa;
+ _cachedLastPagePa = lastPagePa;
+ }
+ }
+
+ public void ReprotectBridge(MemoryPermission protection)
+ {
+ if (_firstPageMemoryForUnmap != null)
+ {
+ _baseMemory.Reprotect(Size, _hostPageSize, protection, throwOnFail: false);
+ }
+ }
+
+ private (MemoryBlock, ulong) GetFirstPageMemoryAndOffset()
+ {
+ _treeLock.EnterReadLock();
+
+ try
+ {
+ PrivateMapping map = _privateTree.GetNode(Address);
+
+ if (map != null && map.PrivateAllocation.IsValid)
+ {
+ return (map.PrivateAllocation.Memory, map.PrivateAllocation.Offset + (Address - map.Address));
+ }
+ }
+ finally
+ {
+ _treeLock.ExitReadLock();
+ }
+
+ return (_backingMemory, _firstPagePa.Value);
+ }
+
+ public PrivateRange GetPrivateAllocation(ulong va)
+ {
+ _treeLock.EnterReadLock();
+
+ try
+ {
+ PrivateMapping map = _privateTree.GetNode(va);
+
+ if (map != null && map.PrivateAllocation.IsValid)
+ {
+ return new(map.PrivateAllocation.Memory, map.PrivateAllocation.Offset + (va - map.Address), map.Size - (va - map.Address));
+ }
+ }
+ finally
+ {
+ _treeLock.ExitReadLock();
+ }
+
+ return PrivateRange.Empty;
+ }
+
+ private void Update(ulong va, ulong pa, ulong size, MappingType type)
+ {
+ _treeLock.EnterWriteLock();
+
+ try
+ {
+ Mapping map = _mappingTree.GetNode(va);
+
+ Update(map, va, pa, size, type);
+ }
+ finally
+ {
+ _treeLock.ExitWriteLock();
+ }
+ }
+
+ private Mapping Update(Mapping map, ulong va, ulong pa, ulong size, MappingType type)
+ {
+ ulong endAddress = va + size;
+
+ for (; map != null; map = map.Successor)
+ {
+ if (map.Address < va)
+ {
+ _mappingTree.Add(map.Split(va));
+ }
+
+ if (map.EndAddress > endAddress)
+ {
+ Mapping newMap = map.Split(endAddress);
+ _mappingTree.Add(newMap);
+ map = newMap;
+ }
+
+ switch (type)
+ {
+ case MappingType.None:
+ ulong alignment = _hostPageSize;
+
+ bool unmappedBefore = map.Predecessor == null ||
+ (map.Predecessor.Type == MappingType.None && map.Predecessor.Address <= BitUtils.AlignDown(va, alignment));
+
+ bool unmappedAfter = map.Successor == null ||
+ (map.Successor.Type == MappingType.None && map.Successor.EndAddress >= BitUtils.AlignUp(endAddress, alignment));
+
+ UnmapPrivate(va, size, unmappedBefore, unmappedAfter);
+ break;
+ case MappingType.Private:
+ MapPrivate(va, size);
+ break;
+ }
+
+ map.UpdateState(type);
+ map = TryCoalesce(map);
+
+ if (map.EndAddress >= endAddress)
+ {
+ break;
+ }
+ }
+
+ return map;
+ }
+
+ private Mapping TryCoalesce(Mapping map)
+ {
+ Mapping previousMap = map.Predecessor;
+ Mapping nextMap = map.Successor;
+
+ if (previousMap != null && CanCoalesce(previousMap, map))
+ {
+ previousMap.Extend(map.Size);
+ _mappingTree.Remove(map);
+ map = previousMap;
+ }
+
+ if (nextMap != null && CanCoalesce(map, nextMap))
+ {
+ map.Extend(nextMap.Size);
+ _mappingTree.Remove(nextMap);
+ }
+
+ return map;
+ }
+
+ private static bool CanCoalesce(Mapping left, Mapping right)
+ {
+ return left.Type == right.Type;
+ }
+
+ private void MapPrivate(ulong va, ulong size)
+ {
+ ulong endAddress = va + size;
+
+ ulong alignment = _hostPageSize;
+
+ // Expand the range outwards based on page size to ensure that at least the requested region is mapped.
+ ulong vaAligned = BitUtils.AlignDown(va, alignment);
+ ulong endAddressAligned = BitUtils.AlignUp(endAddress, alignment);
+
+ PrivateMapping map = _privateTree.GetNode(va);
+
+ for (; map != null; map = map.Successor)
+ {
+ if (!map.PrivateAllocation.IsValid)
+ {
+ if (map.Address < vaAligned)
+ {
+ _privateTree.Add(map.Split(vaAligned));
+ }
+
+ if (map.EndAddress > endAddressAligned)
+ {
+ PrivateMapping newMap = map.Split(endAddressAligned);
+ _privateTree.Add(newMap);
+ map = newMap;
+ }
+
+ map.Map(_baseMemory, Address, _privateMemoryAllocator.Allocate(map.Size, _hostPageSize));
+ }
+
+ if (map.EndAddress >= endAddressAligned)
+ {
+ break;
+ }
+ }
+ }
+
+ private void UnmapPrivate(ulong va, ulong size, bool unmappedBefore, bool unmappedAfter)
+ {
+ ulong endAddress = va + size;
+
+ ulong alignment = _hostPageSize;
+
+ // If the adjacent mappings are unmapped, expand the range outwards,
+ // otherwise shrink it inwards. We must ensure we won't unmap pages that might still be in use.
+ ulong vaAligned = unmappedBefore ? BitUtils.AlignDown(va, alignment) : BitUtils.AlignUp(va, alignment);
+ ulong endAddressAligned = unmappedAfter ? BitUtils.AlignUp(endAddress, alignment) : BitUtils.AlignDown(endAddress, alignment);
+
+ if (endAddressAligned <= vaAligned)
+ {
+ return;
+ }
+
+ PrivateMapping map = _privateTree.GetNode(vaAligned);
+
+ for (; map != null; map = map.Successor)
+ {
+ if (map.PrivateAllocation.IsValid)
+ {
+ if (map.Address < vaAligned)
+ {
+ _privateTree.Add(map.Split(vaAligned));
+ }
+
+ if (map.EndAddress > endAddressAligned)
+ {
+ PrivateMapping newMap = map.Split(endAddressAligned);
+ _privateTree.Add(newMap);
+ map = newMap;
+ }
+
+ map.Unmap(_baseMemory, Address);
+ map = TryCoalesce(map);
+ }
+
+ if (map.EndAddress >= endAddressAligned)
+ {
+ break;
+ }
+ }
+ }
+
+ private PrivateMapping TryCoalesce(PrivateMapping map)
+ {
+ PrivateMapping previousMap = map.Predecessor;
+ PrivateMapping nextMap = map.Successor;
+
+ if (previousMap != null && CanCoalesce(previousMap, map))
+ {
+ previousMap.Extend(map.Size);
+ _privateTree.Remove(map);
+ map = previousMap;
+ }
+
+ if (nextMap != null && CanCoalesce(map, nextMap))
+ {
+ map.Extend(nextMap.Size);
+ _privateTree.Remove(nextMap);
+ }
+
+ return map;
+ }
+
+ private static bool CanCoalesce(PrivateMapping left, PrivateMapping right)
+ {
+ return !left.PrivateAllocation.IsValid && !right.PrivateAllocation.IsValid;
+ }
+
+ private void LateMap()
+ {
+ // Map all existing private allocations.
+ // This is necessary to ensure mirrors that are lazily created have the same mappings as the main one.
+
+ PrivateMapping map = _privateTree.GetNode(Address);
+
+ for (; map != null; map = map.Successor)
+ {
+ if (map.PrivateAllocation.IsValid)
+ {
+ _baseMemory.LateMapView(map.PrivateAllocation.Memory, map.PrivateAllocation.Offset, map.Address - Address, map.Size);
+ }
+ }
+
+ MemoryBlock firstPageMemory = _firstPageMemoryForUnmap;
+ ulong firstPageOffset = _firstPageOffsetForLateMap;
+
+ if (firstPageMemory != null)
+ {
+ _baseMemory.LateMapView(firstPageMemory, firstPageOffset, Size, _hostPageSize);
+ }
+ }
+
+ public PrivateRange GetFirstPrivateAllocation(ulong va, ulong size, out ulong nextVa)
+ {
+ _treeLock.EnterReadLock();
+
+ try
+ {
+ PrivateMapping map = _privateTree.GetNode(va);
+
+ nextVa = map.EndAddress;
+
+ if (map != null && map.PrivateAllocation.IsValid)
+ {
+ ulong startOffset = va - map.Address;
+
+ return new(
+ map.PrivateAllocation.Memory,
+ map.PrivateAllocation.Offset + startOffset,
+ Math.Min(map.PrivateAllocation.Size - startOffset, size));
+ }
+ }
+ finally
+ {
+ _treeLock.ExitReadLock();
+ }
+
+ return PrivateRange.Empty;
+ }
+
+ public bool HasPrivateAllocation(ulong va, ulong size, ulong startVa, ulong startSize, ref PrivateRange range)
+ {
+ ulong endVa = va + size;
+
+ _treeLock.EnterReadLock();
+
+ try
+ {
+ for (PrivateMapping map = _privateTree.GetNode(va); map != null && map.Address < endVa; map = map.Successor)
+ {
+ if (map.PrivateAllocation.IsValid)
+ {
+ if (map.Address <= startVa && map.EndAddress >= startVa + startSize)
+ {
+ ulong startOffset = startVa - map.Address;
+
+ range = new(
+ map.PrivateAllocation.Memory,
+ map.PrivateAllocation.Offset + startOffset,
+ Math.Min(map.PrivateAllocation.Size - startOffset, startSize));
+ }
+
+ return true;
+ }
+ }
+ }
+ finally
+ {
+ _treeLock.ExitReadLock();
+ }
+
+ return false;
+ }
+
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+
+ _privateMemoryAllocator.Dispose();
+ _baseMemory.Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartitionAllocator.cs b/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartitionAllocator.cs
new file mode 100644
index 0000000000..44dedb6404
--- /dev/null
+++ b/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartitionAllocator.cs
@@ -0,0 +1,202 @@
+using Ryujinx.Common;
+using Ryujinx.Common.Collections;
+using Ryujinx.Memory;
+using Ryujinx.Memory.Tracking;
+using System;
+
+namespace Ryujinx.Cpu.Jit.HostTracked
+{
+ readonly struct AddressSpacePartitionAllocation : IDisposable
+ {
+ private readonly AddressSpacePartitionAllocator _owner;
+ private readonly PrivateMemoryAllocatorImpl.Allocation _allocation;
+
+ public IntPtr Pointer => (IntPtr)((ulong)_allocation.Block.Memory.Pointer + _allocation.Offset);
+
+ public bool IsValid => _owner != null;
+
+ public AddressSpacePartitionAllocation(
+ AddressSpacePartitionAllocator owner,
+ PrivateMemoryAllocatorImpl.Allocation allocation)
+ {
+ _owner = owner;
+ _allocation = allocation;
+ }
+
+ public void RegisterMapping(ulong va, ulong endVa)
+ {
+ _allocation.Block.AddMapping(_allocation.Offset, _allocation.Size, va, endVa);
+ }
+
+ public void MapView(MemoryBlock srcBlock, ulong srcOffset, ulong dstOffset, ulong size)
+ {
+ _allocation.Block.Memory.MapView(srcBlock, srcOffset, _allocation.Offset + dstOffset, size);
+ }
+
+ public void UnmapView(MemoryBlock srcBlock, ulong offset, ulong size)
+ {
+ _allocation.Block.Memory.UnmapView(srcBlock, _allocation.Offset + offset, size);
+ }
+
+ public void Reprotect(ulong offset, ulong size, MemoryPermission permission, bool throwOnFail)
+ {
+ _allocation.Block.Memory.Reprotect(_allocation.Offset + offset, size, permission, throwOnFail);
+ }
+
+ public IntPtr GetPointer(ulong offset, ulong size)
+ {
+ return _allocation.Block.Memory.GetPointer(_allocation.Offset + offset, size);
+ }
+
+ public void Dispose()
+ {
+ _allocation.Block.RemoveMapping(_allocation.Offset, _allocation.Size);
+ _owner.Free(_allocation.Block, _allocation.Offset, _allocation.Size);
+ }
+ }
+
+ class AddressSpacePartitionAllocator : PrivateMemoryAllocatorImpl
+ {
+ private const ulong DefaultBlockAlignment = 1UL << 32; // 4GB
+
+ public class Block : PrivateMemoryAllocator.Block
+ {
+ private readonly MemoryTracking _tracking;
+ private readonly Func _readPtCallback;
+ private readonly MemoryEhMeilleure _memoryEh;
+
+ private class Mapping : IntrusiveRedBlackTreeNode, IComparable, IComparable
+ {
+ public ulong Address { get; }
+ public ulong Size { get; }
+ public ulong EndAddress => Address + Size;
+ public ulong Va { get; }
+ public ulong EndVa { get; }
+
+ public Mapping(ulong address, ulong size, ulong va, ulong endVa)
+ {
+ Address = address;
+ Size = size;
+ Va = va;
+ EndVa = endVa;
+ }
+
+ public int CompareTo(Mapping other)
+ {
+ if (Address < other.Address)
+ {
+ return -1;
+ }
+ else if (Address <= other.EndAddress - 1UL)
+ {
+ return 0;
+ }
+ else
+ {
+ return 1;
+ }
+ }
+
+ public int CompareTo(ulong address)
+ {
+ if (address < Address)
+ {
+ return -1;
+ }
+ else if (address <= EndAddress - 1UL)
+ {
+ return 0;
+ }
+ else
+ {
+ return 1;
+ }
+ }
+ }
+
+ private readonly AddressIntrusiveRedBlackTree _mappingTree;
+ private readonly object _lock;
+
+ public Block(MemoryTracking tracking, Func readPtCallback, MemoryBlock memory, ulong size, object locker) : base(memory, size)
+ {
+ _tracking = tracking;
+ _readPtCallback = readPtCallback;
+ _memoryEh = new(memory, null, tracking, VirtualMemoryEvent);
+ _mappingTree = new();
+ _lock = locker;
+ }
+
+ public void AddMapping(ulong offset, ulong size, ulong va, ulong endVa)
+ {
+ _mappingTree.Add(new(offset, size, va, endVa));
+ }
+
+ public void RemoveMapping(ulong offset, ulong size)
+ {
+ _mappingTree.Remove(_mappingTree.GetNode(offset));
+ }
+
+ private ulong VirtualMemoryEvent(ulong address, ulong size, bool write)
+ {
+ Mapping map;
+
+ lock (_lock)
+ {
+ map = _mappingTree.GetNode(address);
+ }
+
+ if (map == null)
+ {
+ return 0;
+ }
+
+ address -= map.Address;
+
+ ulong addressAligned = BitUtils.AlignDown(address, AddressSpacePartition.GuestPageSize);
+ ulong endAddressAligned = BitUtils.AlignUp(address + size, AddressSpacePartition.GuestPageSize);
+ ulong sizeAligned = endAddressAligned - addressAligned;
+
+ if (!_tracking.VirtualMemoryEvent(map.Va + addressAligned, sizeAligned, write))
+ {
+ return 0;
+ }
+
+ return _readPtCallback(map.Va + address);
+ }
+
+ public override void Destroy()
+ {
+ _memoryEh.Dispose();
+
+ base.Destroy();
+ }
+ }
+
+ private readonly MemoryTracking _tracking;
+ private readonly Func _readPtCallback;
+ private readonly object _lock;
+
+ public AddressSpacePartitionAllocator(
+ MemoryTracking tracking,
+ Func readPtCallback,
+ object locker) : base(DefaultBlockAlignment, MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible)
+ {
+ _tracking = tracking;
+ _readPtCallback = readPtCallback;
+ _lock = locker;
+ }
+
+ public AddressSpacePartitionAllocation Allocate(ulong va, ulong size)
+ {
+ AddressSpacePartitionAllocation allocation = new(this, Allocate(size, MemoryBlock.GetPageSize(), CreateBlock));
+ allocation.RegisterMapping(va, va + size);
+
+ return allocation;
+ }
+
+ private Block CreateBlock(MemoryBlock memory, ulong size)
+ {
+ return new Block(_tracking, _readPtCallback, memory, size, _lock);
+ }
+ }
+}
diff --git a/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartitionMultiAllocation.cs b/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartitionMultiAllocation.cs
new file mode 100644
index 0000000000..3b065583f8
--- /dev/null
+++ b/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartitionMultiAllocation.cs
@@ -0,0 +1,101 @@
+using Ryujinx.Memory;
+using System;
+using System.Diagnostics;
+
+namespace Ryujinx.Cpu.Jit.HostTracked
+{
+ class AddressSpacePartitionMultiAllocation : IDisposable
+ {
+ private readonly AddressSpacePartitionAllocation _baseMemory;
+ private AddressSpacePartitionAllocation _baseMemoryRo;
+ private AddressSpacePartitionAllocation _baseMemoryNone;
+
+ public AddressSpacePartitionMultiAllocation(AddressSpacePartitionAllocation baseMemory)
+ {
+ _baseMemory = baseMemory;
+ }
+
+ public void MapView(MemoryBlock srcBlock, ulong srcOffset, ulong dstOffset, ulong size)
+ {
+ _baseMemory.MapView(srcBlock, srcOffset, dstOffset, size);
+
+ if (_baseMemoryRo.IsValid)
+ {
+ _baseMemoryRo.MapView(srcBlock, srcOffset, dstOffset, size);
+ _baseMemoryRo.Reprotect(dstOffset, size, MemoryPermission.Read, false);
+ }
+ }
+
+ public void LateMapView(MemoryBlock srcBlock, ulong srcOffset, ulong dstOffset, ulong size)
+ {
+ _baseMemoryRo.MapView(srcBlock, srcOffset, dstOffset, size);
+ _baseMemoryRo.Reprotect(dstOffset, size, MemoryPermission.Read, false);
+ }
+
+ public void UnmapView(MemoryBlock srcBlock, ulong offset, ulong size)
+ {
+ _baseMemory.UnmapView(srcBlock, offset, size);
+
+ if (_baseMemoryRo.IsValid)
+ {
+ _baseMemoryRo.UnmapView(srcBlock, offset, size);
+ }
+ }
+
+ public void Reprotect(ulong offset, ulong size, MemoryPermission permission, bool throwOnFail)
+ {
+ _baseMemory.Reprotect(offset, size, permission, throwOnFail);
+ }
+
+ public IntPtr GetPointer(ulong offset, ulong size)
+ {
+ return _baseMemory.GetPointer(offset, size);
+ }
+
+ public bool LazyInitMirrorForProtection(AddressSpacePartitioned addressSpace, ulong blockAddress, ulong blockSize, MemoryPermission permission)
+ {
+ if (permission == MemoryPermission.None && !_baseMemoryNone.IsValid)
+ {
+ _baseMemoryNone = addressSpace.CreateAsPartitionAllocation(blockAddress, blockSize);
+ }
+ else if (permission == MemoryPermission.Read && !_baseMemoryRo.IsValid)
+ {
+ _baseMemoryRo = addressSpace.CreateAsPartitionAllocation(blockAddress, blockSize);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public IntPtr GetPointerForProtection(ulong offset, ulong size, MemoryPermission permission)
+ {
+ AddressSpacePartitionAllocation allocation = permission switch
+ {
+ MemoryPermission.ReadAndWrite => _baseMemory,
+ MemoryPermission.Read => _baseMemoryRo,
+ MemoryPermission.None => _baseMemoryNone,
+ _ => throw new ArgumentException($"Invalid protection \"{permission}\"."),
+ };
+
+ Debug.Assert(allocation.IsValid);
+
+ return allocation.GetPointer(offset, size);
+ }
+
+ public void Dispose()
+ {
+ _baseMemory.Dispose();
+
+ if (_baseMemoryRo.IsValid)
+ {
+ _baseMemoryRo.Dispose();
+ }
+
+ if (_baseMemoryNone.IsValid)
+ {
+ _baseMemoryNone.Dispose();
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartitioned.cs b/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartitioned.cs
new file mode 100644
index 0000000000..2cf2c248b2
--- /dev/null
+++ b/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartitioned.cs
@@ -0,0 +1,407 @@
+using Ryujinx.Common;
+using Ryujinx.Memory;
+using Ryujinx.Memory.Tracking;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace Ryujinx.Cpu.Jit.HostTracked
+{
+ class AddressSpacePartitioned : IDisposable
+ {
+ private const int PartitionBits = 25;
+ private const ulong PartitionSize = 1UL << PartitionBits;
+
+ private readonly MemoryBlock _backingMemory;
+ private readonly List _partitions;
+ private readonly AddressSpacePartitionAllocator _asAllocator;
+ private readonly Action _updatePtCallback;
+ private readonly bool _useProtectionMirrors;
+
+ public AddressSpacePartitioned(MemoryTracking tracking, MemoryBlock backingMemory, NativePageTable nativePageTable, bool useProtectionMirrors)
+ {
+ _backingMemory = backingMemory;
+ _partitions = new();
+ _asAllocator = new(tracking, nativePageTable.Read, _partitions);
+ _updatePtCallback = nativePageTable.Update;
+ _useProtectionMirrors = useProtectionMirrors;
+ }
+
+ public void Map(ulong va, ulong pa, ulong size)
+ {
+ ulong endVa = va + size;
+
+ lock (_partitions)
+ {
+ EnsurePartitionsLocked(va, size);
+
+ while (va < endVa)
+ {
+ int partitionIndex = FindPartitionIndexLocked(va);
+ AddressSpacePartition partition = _partitions[partitionIndex];
+
+ (ulong clampedVa, ulong clampedEndVa) = ClampRange(partition, va, endVa);
+
+ partition.Map(clampedVa, pa, clampedEndVa - clampedVa);
+
+ ulong currentSize = clampedEndVa - clampedVa;
+
+ va += currentSize;
+ pa += currentSize;
+
+ InsertOrRemoveBridgeIfNeeded(partitionIndex);
+ }
+ }
+ }
+
+ public void Unmap(ulong va, ulong size)
+ {
+ ulong endVa = va + size;
+
+ while (va < endVa)
+ {
+ AddressSpacePartition partition;
+
+ lock (_partitions)
+ {
+ int partitionIndex = FindPartitionIndexLocked(va);
+ if (partitionIndex < 0)
+ {
+ va += PartitionSize - (va & (PartitionSize - 1));
+
+ continue;
+ }
+
+ partition = _partitions[partitionIndex];
+
+ (ulong clampedVa, ulong clampedEndVa) = ClampRange(partition, va, endVa);
+
+ partition.Unmap(clampedVa, clampedEndVa - clampedVa);
+
+ va += clampedEndVa - clampedVa;
+
+ InsertOrRemoveBridgeIfNeeded(partitionIndex);
+
+ if (partition.IsEmpty())
+ {
+ _partitions.Remove(partition);
+ partition.Dispose();
+ }
+ }
+ }
+ }
+
+ public void Reprotect(ulong va, ulong size, MemoryPermission protection)
+ {
+ ulong endVa = va + size;
+
+ lock (_partitions)
+ {
+ while (va < endVa)
+ {
+ AddressSpacePartition partition = FindPartitionWithIndex(va, out int partitionIndex);
+
+ if (partition == null)
+ {
+ va += PartitionSize - (va & (PartitionSize - 1));
+
+ continue;
+ }
+
+ (ulong clampedVa, ulong clampedEndVa) = ClampRange(partition, va, endVa);
+
+ if (_useProtectionMirrors)
+ {
+ partition.Reprotect(clampedVa, clampedEndVa - clampedVa, protection, this, _updatePtCallback);
+ }
+ else
+ {
+ partition.ReprotectAligned(clampedVa, clampedEndVa - clampedVa, protection);
+
+ if (clampedVa == partition.Address &&
+ partitionIndex > 0 &&
+ _partitions[partitionIndex - 1].EndAddress == partition.Address)
+ {
+ _partitions[partitionIndex - 1].ReprotectBridge(protection);
+ }
+ }
+
+ va += clampedEndVa - clampedVa;
+ }
+ }
+ }
+
+ public PrivateRange GetPrivateAllocation(ulong va)
+ {
+ AddressSpacePartition partition = FindPartition(va);
+
+ if (partition == null)
+ {
+ return PrivateRange.Empty;
+ }
+
+ return partition.GetPrivateAllocation(va);
+ }
+
+ public PrivateRange GetFirstPrivateAllocation(ulong va, ulong size, out ulong nextVa)
+ {
+ AddressSpacePartition partition = FindPartition(va);
+
+ if (partition == null)
+ {
+ nextVa = (va & ~(PartitionSize - 1)) + PartitionSize;
+
+ return PrivateRange.Empty;
+ }
+
+ return partition.GetFirstPrivateAllocation(va, size, out nextVa);
+ }
+
+ public bool HasAnyPrivateAllocation(ulong va, ulong size, out PrivateRange range)
+ {
+ range = PrivateRange.Empty;
+
+ ulong startVa = va;
+ ulong endVa = va + size;
+
+ while (va < endVa)
+ {
+ AddressSpacePartition partition = FindPartition(va);
+
+ if (partition == null)
+ {
+ va += PartitionSize - (va & (PartitionSize - 1));
+
+ continue;
+ }
+
+ (ulong clampedVa, ulong clampedEndVa) = ClampRange(partition, va, endVa);
+
+ if (partition.HasPrivateAllocation(clampedVa, clampedEndVa - clampedVa, startVa, size, ref range))
+ {
+ return true;
+ }
+
+ va += clampedEndVa - clampedVa;
+ }
+
+ return false;
+ }
+
+ private void InsertOrRemoveBridgeIfNeeded(int partitionIndex)
+ {
+ if (partitionIndex > 0)
+ {
+ if (_partitions[partitionIndex - 1].EndAddress == _partitions[partitionIndex].Address)
+ {
+ _partitions[partitionIndex - 1].InsertBridgeAtEnd(_partitions[partitionIndex], _useProtectionMirrors);
+ }
+ else
+ {
+ _partitions[partitionIndex - 1].InsertBridgeAtEnd(null, _useProtectionMirrors);
+ }
+ }
+
+ if (partitionIndex + 1 < _partitions.Count && _partitions[partitionIndex].EndAddress == _partitions[partitionIndex + 1].Address)
+ {
+ _partitions[partitionIndex].InsertBridgeAtEnd(_partitions[partitionIndex + 1], _useProtectionMirrors);
+ }
+ else
+ {
+ _partitions[partitionIndex].InsertBridgeAtEnd(null, _useProtectionMirrors);
+ }
+ }
+
+ public IntPtr GetPointer(ulong va, ulong size)
+ {
+ AddressSpacePartition partition = FindPartition(va);
+
+ return partition.GetPointer(va, size);
+ }
+
+ private static (ulong, ulong) ClampRange(AddressSpacePartition partition, ulong va, ulong endVa)
+ {
+ if (va < partition.Address)
+ {
+ va = partition.Address;
+ }
+
+ if (endVa > partition.EndAddress)
+ {
+ endVa = partition.EndAddress;
+ }
+
+ return (va, endVa);
+ }
+
+ private AddressSpacePartition FindPartition(ulong va)
+ {
+ lock (_partitions)
+ {
+ int index = FindPartitionIndexLocked(va);
+ if (index >= 0)
+ {
+ return _partitions[index];
+ }
+ }
+
+ return null;
+ }
+
+ private AddressSpacePartition FindPartitionWithIndex(ulong va, out int index)
+ {
+ lock (_partitions)
+ {
+ index = FindPartitionIndexLocked(va);
+ if (index >= 0)
+ {
+ return _partitions[index];
+ }
+ }
+
+ return null;
+ }
+
+ private int FindPartitionIndexLocked(ulong va)
+ {
+ int left = 0;
+ int middle;
+ int right = _partitions.Count - 1;
+
+ while (left <= right)
+ {
+ middle = left + ((right - left) >> 1);
+
+ AddressSpacePartition partition = _partitions[middle];
+
+ if (partition.Address <= va && partition.EndAddress > va)
+ {
+ return middle;
+ }
+
+ if (partition.Address >= va)
+ {
+ right = middle - 1;
+ }
+ else
+ {
+ left = middle + 1;
+ }
+ }
+
+ return -1;
+ }
+
+ private void EnsurePartitionsLocked(ulong va, ulong size)
+ {
+ ulong endVa = BitUtils.AlignUp(va + size, PartitionSize);
+ va = BitUtils.AlignDown(va, PartitionSize);
+
+ for (int i = 0; i < _partitions.Count && va < endVa; i++)
+ {
+ AddressSpacePartition partition = _partitions[i];
+
+ if (partition.Address <= va && partition.EndAddress > va)
+ {
+ if (partition.EndAddress >= endVa)
+ {
+ // Fully mapped already.
+ va = endVa;
+
+ break;
+ }
+
+ ulong gapSize;
+
+ if (i + 1 < _partitions.Count)
+ {
+ AddressSpacePartition nextPartition = _partitions[i + 1];
+
+ if (partition.EndAddress == nextPartition.Address)
+ {
+ va = partition.EndAddress;
+
+ continue;
+ }
+
+ gapSize = Math.Min(endVa, nextPartition.Address) - partition.EndAddress;
+ }
+ else
+ {
+ gapSize = endVa - partition.EndAddress;
+ }
+
+ _partitions.Insert(i + 1, CreateAsPartition(partition.EndAddress, gapSize));
+ va = partition.EndAddress + gapSize;
+ i++;
+ }
+ else if (partition.EndAddress > va)
+ {
+ Debug.Assert(partition.Address > va);
+
+ ulong gapSize;
+
+ if (partition.Address < endVa)
+ {
+ gapSize = partition.Address - va;
+ }
+ else
+ {
+ gapSize = endVa - va;
+ }
+
+ _partitions.Insert(i, CreateAsPartition(va, gapSize));
+ va = Math.Min(partition.EndAddress, endVa);
+ i++;
+ }
+ }
+
+ if (va < endVa)
+ {
+ _partitions.Add(CreateAsPartition(va, endVa - va));
+ }
+
+ ValidatePartitionList();
+ }
+
+ [Conditional("DEBUG")]
+ private void ValidatePartitionList()
+ {
+ for (int i = 1; i < _partitions.Count; i++)
+ {
+ Debug.Assert(_partitions[i].Address > _partitions[i - 1].Address);
+ Debug.Assert(_partitions[i].EndAddress > _partitions[i - 1].EndAddress);
+ }
+ }
+
+ private AddressSpacePartition CreateAsPartition(ulong va, ulong size)
+ {
+ return new(CreateAsPartitionAllocation(va, size), _backingMemory, va, size);
+ }
+
+ public AddressSpacePartitionAllocation CreateAsPartitionAllocation(ulong va, ulong size)
+ {
+ return _asAllocator.Allocate(va, size + MemoryBlock.GetPageSize());
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ foreach (AddressSpacePartition partition in _partitions)
+ {
+ partition.Dispose();
+ }
+
+ _partitions.Clear();
+ _asAllocator.Dispose();
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/Ryujinx.Cpu/Jit/HostTracked/NativePageTable.cs b/src/Ryujinx.Cpu/Jit/HostTracked/NativePageTable.cs
new file mode 100644
index 0000000000..e3174e3fc5
--- /dev/null
+++ b/src/Ryujinx.Cpu/Jit/HostTracked/NativePageTable.cs
@@ -0,0 +1,223 @@
+using Ryujinx.Cpu.Signal;
+using Ryujinx.Memory;
+using System;
+using System.Diagnostics;
+using System.Numerics;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Cpu.Jit.HostTracked
+{
+ sealed class NativePageTable : IDisposable
+ {
+ private delegate ulong TrackingEventDelegate(ulong address, ulong size, bool write);
+
+ private const int PageBits = 12;
+ private const int PageSize = 1 << PageBits;
+ private const int PageMask = PageSize - 1;
+
+ private const int PteSize = 8;
+
+ private readonly int _bitsPerPtPage;
+ private readonly int _entriesPerPtPage;
+ private readonly int _pageCommitmentBits;
+
+ private readonly PageTable _pageTable;
+ private readonly MemoryBlock _nativePageTable;
+ private readonly ulong[] _pageCommitmentBitmap;
+ private readonly ulong _hostPageSize;
+
+ private readonly TrackingEventDelegate _trackingEvent;
+
+ private bool _disposed;
+
+ public IntPtr PageTablePointer => _nativePageTable.Pointer;
+
+ public NativePageTable(ulong asSize)
+ {
+ ulong hostPageSize = MemoryBlock.GetPageSize();
+
+ _entriesPerPtPage = (int)(hostPageSize / sizeof(ulong));
+ _bitsPerPtPage = BitOperations.Log2((uint)_entriesPerPtPage);
+ _pageCommitmentBits = PageBits + _bitsPerPtPage;
+
+ _hostPageSize = hostPageSize;
+ _pageTable = new PageTable();
+ _nativePageTable = new MemoryBlock((asSize / PageSize) * PteSize + _hostPageSize, MemoryAllocationFlags.Reserve);
+ _pageCommitmentBitmap = new ulong[(asSize >> _pageCommitmentBits) / (sizeof(ulong) * 8)];
+
+ ulong ptStart = (ulong)_nativePageTable.Pointer;
+ ulong ptEnd = ptStart + _nativePageTable.Size;
+
+ _trackingEvent = VirtualMemoryEvent;
+
+ bool added = NativeSignalHandler.AddTrackedRegion((nuint)ptStart, (nuint)ptEnd, Marshal.GetFunctionPointerForDelegate(_trackingEvent));
+
+ if (!added)
+ {
+ throw new InvalidOperationException("Number of allowed tracked regions exceeded.");
+ }
+ }
+
+ public void Map(ulong va, ulong pa, ulong size, AddressSpacePartitioned addressSpace, MemoryBlock backingMemory, bool privateMap)
+ {
+ while (size != 0)
+ {
+ _pageTable.Map(va, pa);
+
+ EnsureCommitment(va);
+
+ if (privateMap)
+ {
+ _nativePageTable.Write((va / PageSize) * PteSize, GetPte(va, addressSpace.GetPointer(va, PageSize)));
+ }
+ else
+ {
+ _nativePageTable.Write((va / PageSize) * PteSize, GetPte(va, backingMemory.GetPointer(pa, PageSize)));
+ }
+
+ va += PageSize;
+ pa += PageSize;
+ size -= PageSize;
+ }
+ }
+
+ public void Unmap(ulong va, ulong size)
+ {
+ IntPtr guardPagePtr = GetGuardPagePointer();
+
+ while (size != 0)
+ {
+ _pageTable.Unmap(va);
+ _nativePageTable.Write((va / PageSize) * PteSize, GetPte(va, guardPagePtr));
+
+ va += PageSize;
+ size -= PageSize;
+ }
+ }
+
+ public ulong Read(ulong va)
+ {
+ ulong pte = _nativePageTable.Read((va / PageSize) * PteSize);
+
+ pte += va & ~(ulong)PageMask;
+
+ return pte + (va & PageMask);
+ }
+
+ public void Update(ulong va, IntPtr ptr, ulong size)
+ {
+ ulong remainingSize = size;
+
+ while (remainingSize != 0)
+ {
+ EnsureCommitment(va);
+
+ _nativePageTable.Write((va / PageSize) * PteSize, GetPte(va, ptr));
+
+ va += PageSize;
+ ptr += PageSize;
+ remainingSize -= PageSize;
+ }
+ }
+
+ private void EnsureCommitment(ulong va)
+ {
+ ulong bit = va >> _pageCommitmentBits;
+
+ int index = (int)(bit / (sizeof(ulong) * 8));
+ int shift = (int)(bit % (sizeof(ulong) * 8));
+
+ ulong mask = 1UL << shift;
+
+ ulong oldMask = _pageCommitmentBitmap[index];
+
+ if ((oldMask & mask) == 0)
+ {
+ lock (_pageCommitmentBitmap)
+ {
+ oldMask = _pageCommitmentBitmap[index];
+
+ if ((oldMask & mask) != 0)
+ {
+ return;
+ }
+
+ _nativePageTable.Commit(bit * _hostPageSize, _hostPageSize);
+
+ Span pageSpan = MemoryMarshal.Cast(_nativePageTable.GetSpan(bit * _hostPageSize, (int)_hostPageSize));
+
+ Debug.Assert(pageSpan.Length == _entriesPerPtPage);
+
+ IntPtr guardPagePtr = GetGuardPagePointer();
+
+ for (int i = 0; i < pageSpan.Length; i++)
+ {
+ pageSpan[i] = GetPte((bit << _pageCommitmentBits) | ((ulong)i * PageSize), guardPagePtr);
+ }
+
+ _pageCommitmentBitmap[index] = oldMask | mask;
+ }
+ }
+ }
+
+ private IntPtr GetGuardPagePointer()
+ {
+ return _nativePageTable.GetPointer(_nativePageTable.Size - _hostPageSize, _hostPageSize);
+ }
+
+ private static ulong GetPte(ulong va, IntPtr ptr)
+ {
+ Debug.Assert((va & PageMask) == 0);
+
+ return (ulong)ptr - va;
+ }
+
+ public ulong GetPhysicalAddress(ulong va)
+ {
+ return _pageTable.Read(va) + (va & PageMask);
+ }
+
+ private ulong VirtualMemoryEvent(ulong address, ulong size, bool write)
+ {
+ if (address < _nativePageTable.Size - _hostPageSize)
+ {
+ // Some prefetch instructions do not cause faults with invalid addresses.
+ // Retry if we are hitting a case where the page table is unmapped, the next
+ // run will execute the actual instruction.
+ // The address loaded from the page table will be invalid, and it should hit the else case
+ // if the instruction faults on unmapped or protected memory.
+
+ ulong va = address * (PageSize / sizeof(ulong));
+
+ EnsureCommitment(va);
+
+ return (ulong)_nativePageTable.Pointer + address;
+ }
+ else
+ {
+ throw new InvalidMemoryRegionException();
+ }
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ NativeSignalHandler.RemoveTrackedRegion((nuint)_nativePageTable.Pointer);
+
+ _nativePageTable.Dispose();
+ }
+
+ _disposed = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/Ryujinx.Cpu/Jit/JitCpuContext.cs b/src/Ryujinx.Cpu/Jit/JitCpuContext.cs
index dce0490a41..9893c59b29 100644
--- a/src/Ryujinx.Cpu/Jit/JitCpuContext.cs
+++ b/src/Ryujinx.Cpu/Jit/JitCpuContext.cs
@@ -15,9 +15,9 @@ namespace Ryujinx.Cpu.Jit
_tickSource = tickSource;
_translator = new Translator(new JitMemoryAllocator(forJit: true), memory, for64Bit);
- if (memory.Type.IsHostMapped())
+ if (memory.Type.IsHostMappedOrTracked())
{
- NativeSignalHandler.InitializeSignalHandler(MemoryBlock.GetPageSize());
+ NativeSignalHandler.InitializeSignalHandler();
}
memory.UnmapEvent += UnmapHandler;
diff --git a/src/Ryujinx.Cpu/Jit/MemoryManager.cs b/src/Ryujinx.Cpu/Jit/MemoryManager.cs
index b9a547025c..6f594ec2fd 100644
--- a/src/Ryujinx.Cpu/Jit/MemoryManager.cs
+++ b/src/Ryujinx.Cpu/Jit/MemoryManager.cs
@@ -3,6 +3,7 @@ using Ryujinx.Memory;
using Ryujinx.Memory.Range;
using Ryujinx.Memory.Tracking;
using System;
+using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
@@ -14,12 +15,8 @@ namespace Ryujinx.Cpu.Jit
///
/// Represents a CPU memory manager.
///
- public sealed class MemoryManager : MemoryManagerBase, IMemoryManager, IVirtualMemoryManagerTracked, IWritableBlock
+ public sealed class MemoryManager : VirtualMemoryManagerRefCountedBase, IMemoryManager, IVirtualMemoryManagerTracked
{
- public const int PageBits = 12;
- public const int PageSize = 1 << PageBits;
- public const int PageMask = PageSize - 1;
-
private const int PteSize = 8;
private const int PointerTagBit = 62;
@@ -28,17 +25,17 @@ namespace Ryujinx.Cpu.Jit
private readonly InvalidAccessHandler _invalidAccessHandler;
///
- public bool Supports4KBPages => true;
+ public bool UsesPrivateAllocations => false;
///
/// Address space width in bits.
///
public int AddressSpaceBits { get; }
- private readonly ulong _addressSpaceSize;
-
private readonly MemoryBlock _pageTable;
+ private readonly ManagedPageFlags _pages;
+
///
/// Page table base pointer.
///
@@ -50,6 +47,8 @@ namespace Ryujinx.Cpu.Jit
public event Action UnmapEvent;
+ protected override ulong AddressSpaceSize { get; }
+
///
/// Creates a new instance of the memory manager.
///
@@ -71,9 +70,11 @@ namespace Ryujinx.Cpu.Jit
}
AddressSpaceBits = asBits;
- _addressSpaceSize = asSize;
+ AddressSpaceSize = asSize;
_pageTable = new MemoryBlock((asSize / PageSize) * PteSize);
+ _pages = new ManagedPageFlags(AddressSpaceBits);
+
Tracking = new MemoryTracking(this, PageSize);
}
@@ -93,15 +94,10 @@ namespace Ryujinx.Cpu.Jit
remainingSize -= PageSize;
}
+ _pages.AddMapping(oVa, size);
Tracking.Map(oVa, size);
}
- ///
- public void MapForeign(ulong va, nuint hostPointer, ulong size)
- {
- throw new NotSupportedException();
- }
-
///
public void Unmap(ulong va, ulong size)
{
@@ -115,6 +111,7 @@ namespace Ryujinx.Cpu.Jit
UnmapEvent?.Invoke(va, size);
Tracking.Unmap(va, size);
+ _pages.RemoveMapping(va, size);
ulong remainingSize = size;
while (remainingSize != 0)
@@ -126,18 +123,29 @@ namespace Ryujinx.Cpu.Jit
}
}
- ///
- public T Read(ulong va) where T : unmanaged
- {
- return MemoryMarshal.Cast(GetSpan(va, Unsafe.SizeOf()))[0];
- }
-
- ///
- public T ReadTracked(ulong va) where T : unmanaged
+ public override T ReadTracked(ulong va)
{
try
{
- SignalMemoryTracking(va, (ulong)Unsafe.SizeOf(), false);
+ return base.ReadTracked(va);
+ }
+ catch (InvalidMemoryRegionException)
+ {
+ if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
+ {
+ throw;
+ }
+
+ return default;
+ }
+ }
+
+ ///
+ public T ReadGuest(ulong va) where T : unmanaged
+ {
+ try
+ {
+ SignalMemoryTrackingImpl(va, (ulong)Unsafe.SizeOf(), false, true);
return Read(va);
}
@@ -153,112 +161,26 @@ namespace Ryujinx.Cpu.Jit
}
///
- public void Read(ulong va, Span data)
- {
- ReadImpl(va, data);
- }
-
- ///
- public void Write(ulong va, T value) where T : unmanaged
- {
- Write(va, MemoryMarshal.Cast(MemoryMarshal.CreateSpan(ref value, 1)));
- }
-
- ///
- public void Write(ulong va, ReadOnlySpan data)
- {
- if (data.Length == 0)
- {
- return;
- }
-
- SignalMemoryTracking(va, (ulong)data.Length, true);
-
- WriteImpl(va, data);
- }
-
- ///
- public void WriteUntracked(ulong va, ReadOnlySpan data)
- {
- if (data.Length == 0)
- {
- return;
- }
-
- WriteImpl(va, data);
- }
-
- ///
- public bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan data)
- {
- if (data.Length == 0)
- {
- return false;
- }
-
- SignalMemoryTracking(va, (ulong)data.Length, false);
-
- if (IsContiguousAndMapped(va, data.Length))
- {
- var target = _backingMemory.GetSpan(GetPhysicalAddressInternal(va), data.Length);
-
- bool changed = !data.SequenceEqual(target);
-
- if (changed)
- {
- data.CopyTo(target);
- }
-
- return changed;
- }
- else
- {
- WriteImpl(va, data);
-
- return true;
- }
- }
-
- ///
- /// Writes data to CPU mapped memory.
- ///
- /// Virtual address to write the data into
- /// Data to be written
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void WriteImpl(ulong va, ReadOnlySpan data)
+ public override void Read(ulong va, Span data)
{
try
{
- AssertValidAddressAndSize(va, (ulong)data.Length);
-
- if (IsContiguousAndMapped(va, data.Length))
+ base.Read(va, data);
+ }
+ catch (InvalidMemoryRegionException)
+ {
+ if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
{
- data.CopyTo(_backingMemory.GetSpan(GetPhysicalAddressInternal(va), data.Length));
+ throw;
}
- else
- {
- int offset = 0, size;
+ }
+ }
- if ((va & PageMask) != 0)
- {
- ulong pa = GetPhysicalAddressInternal(va);
-
- size = Math.Min(data.Length, PageSize - (int)(va & PageMask));
-
- data[..size].CopyTo(_backingMemory.GetSpan(pa, size));
-
- offset += size;
- }
-
- for (; offset < data.Length; offset += size)
- {
- ulong pa = GetPhysicalAddressInternal(va + (ulong)offset);
-
- size = Math.Min(data.Length - offset, PageSize);
-
- data.Slice(offset, size).CopyTo(_backingMemory.GetSpan(pa, size));
- }
- }
+ public override void Write(ulong va, ReadOnlySpan data)
+ {
+ try
+ {
+ base.Write(va, data);
}
catch (InvalidMemoryRegionException)
{
@@ -270,60 +192,47 @@ namespace Ryujinx.Cpu.Jit
}
///
- public ReadOnlySpan GetSpan(ulong va, int size, bool tracked = false)
+ public void WriteGuest(ulong va, T value) where T : unmanaged
{
- if (size == 0)
+ Span data = MemoryMarshal.Cast(MemoryMarshal.CreateSpan(ref value, 1));
+
+ SignalMemoryTrackingImpl(va, (ulong)data.Length, true, true);
+
+ Write(va, data);
+ }
+
+ public override void WriteUntracked(ulong va, ReadOnlySpan data)
+ {
+ try
{
- return ReadOnlySpan.Empty;
+ base.WriteUntracked(va, data);
}
-
- if (tracked)
+ catch (InvalidMemoryRegionException)
{
- SignalMemoryTracking(va, (ulong)size, false);
- }
-
- if (IsContiguousAndMapped(va, size))
- {
- return _backingMemory.GetSpan(GetPhysicalAddressInternal(va), size);
- }
- else
- {
- Span data = new byte[size];
-
- ReadImpl(va, data);
-
- return data;
+ if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
+ {
+ throw;
+ }
}
}
- ///
- public WritableRegion GetWritableRegion(ulong va, int size, bool tracked = false)
+ public override ReadOnlySequence GetReadOnlySequence(ulong va, int size, bool tracked = false)
{
- if (size == 0)
+ try
{
- return new WritableRegion(null, va, Memory.Empty);
+ return base.GetReadOnlySequence(va, size, tracked);
}
-
- if (IsContiguousAndMapped(va, size))
+ catch (InvalidMemoryRegionException)
{
- if (tracked)
+ if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
{
- SignalMemoryTracking(va, (ulong)size, true);
+ throw;
}
- return new WritableRegion(null, va, _backingMemory.GetMemory(GetPhysicalAddressInternal(va), size));
- }
- else
- {
- Memory memory = new byte[size];
-
- GetSpan(va, size).CopyTo(memory.Span);
-
- return new WritableRegion(this, va, memory, tracked);
+ return ReadOnlySequence.Empty;
}
}
- ///
public ref T GetRef(ulong va) where T : unmanaged
{
if (!IsContiguous(va, Unsafe.SizeOf()))
@@ -336,56 +245,6 @@ namespace Ryujinx.Cpu.Jit
return ref _backingMemory.GetRef(GetPhysicalAddressInternal(va));
}
- ///
- /// Computes the number of pages in a virtual address range.
- ///
- /// Virtual address of the range
- /// Size of the range
- /// The virtual address of the beginning of the first page
- /// This function does not differentiate between allocated and unallocated pages.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static int GetPagesCount(ulong va, uint size, out ulong startVa)
- {
- // WARNING: Always check if ulong does not overflow during the operations.
- startVa = va & ~(ulong)PageMask;
- ulong vaSpan = (va - startVa + size + PageMask) & ~(ulong)PageMask;
-
- return (int)(vaSpan / PageSize);
- }
-
- private static void ThrowMemoryNotContiguous() => throw new MemoryNotContiguousException();
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private bool IsContiguousAndMapped(ulong va, int size) => IsContiguous(va, size) && IsMapped(va);
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private bool IsContiguous(ulong va, int size)
- {
- if (!ValidateAddress(va) || !ValidateAddressAndSize(va, (ulong)size))
- {
- return false;
- }
-
- int pages = GetPagesCount(va, (uint)size, out va);
-
- for (int page = 0; page < pages - 1; page++)
- {
- if (!ValidateAddress(va + PageSize))
- {
- return false;
- }
-
- if (GetPhysicalAddressInternal(va) + PageSize != GetPhysicalAddressInternal(va + PageSize))
- {
- return false;
- }
-
- va += PageSize;
- }
-
- return true;
- }
-
///
public IEnumerable GetHostRegions(ulong va, ulong size)
{
@@ -462,48 +321,6 @@ namespace Ryujinx.Cpu.Jit
return regions;
}
- private void ReadImpl(ulong va, Span data)
- {
- if (data.Length == 0)
- {
- return;
- }
-
- try
- {
- AssertValidAddressAndSize(va, (ulong)data.Length);
-
- int offset = 0, size;
-
- if ((va & PageMask) != 0)
- {
- ulong pa = GetPhysicalAddressInternal(va);
-
- size = Math.Min(data.Length, PageSize - (int)(va & PageMask));
-
- _backingMemory.GetSpan(pa, size).CopyTo(data[..size]);
-
- offset += size;
- }
-
- for (; offset < data.Length; offset += size)
- {
- ulong pa = GetPhysicalAddressInternal(va + (ulong)offset);
-
- size = Math.Min(data.Length - offset, PageSize);
-
- _backingMemory.GetSpan(pa, size).CopyTo(data.Slice(offset, size));
- }
- }
- catch (InvalidMemoryRegionException)
- {
- if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
- {
- throw;
- }
- }
- }
-
///
public bool IsRangeMapped(ulong va, ulong size)
{
@@ -532,9 +349,8 @@ namespace Ryujinx.Cpu.Jit
return true;
}
- ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public bool IsMapped(ulong va)
+ public override bool IsMapped(ulong va)
{
if (!ValidateAddress(va))
{
@@ -544,40 +360,9 @@ namespace Ryujinx.Cpu.Jit
return _pageTable.Read((va / PageSize) * PteSize) != 0;
}
- private bool ValidateAddress(ulong va)
+ private nuint GetPhysicalAddressInternal(ulong va)
{
- return va < _addressSpaceSize;
- }
-
- ///
- /// Checks if the combination of virtual address and size is part of the addressable space.
- ///
- /// Virtual address of the range
- /// Size of the range in bytes
- /// True if the combination of virtual address and size is part of the addressable space
- private bool ValidateAddressAndSize(ulong va, ulong size)
- {
- ulong endVa = va + size;
- return endVa >= va && endVa >= size && endVa <= _addressSpaceSize;
- }
-
- ///
- /// Ensures the combination of virtual address and size is part of the addressable space.
- ///
- /// Virtual address of the range
- /// Size of the range in bytes
- /// Throw when the memory region specified outside the addressable space
- private void AssertValidAddressAndSize(ulong va, ulong size)
- {
- if (!ValidateAddressAndSize(va, size))
- {
- throw new InvalidMemoryRegionException($"va=0x{va:X16}, size=0x{size:X16}");
- }
- }
-
- private ulong GetPhysicalAddressInternal(ulong va)
- {
- return PteToPa(_pageTable.Read((va / PageSize) * PteSize) & ~(0xffffUL << 48)) + (va & PageMask);
+ return (nuint)(PteToPa(_pageTable.Read((va / PageSize) * PteSize) & ~(0xffffUL << 48)) + (va & PageMask));
}
///
@@ -587,50 +372,57 @@ namespace Ryujinx.Cpu.Jit
}
///
- public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
+ public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection, bool guest)
{
AssertValidAddressAndSize(va, size);
- // Protection is inverted on software pages, since the default value is 0.
- protection = (~protection) & MemoryPermission.ReadAndWrite;
-
- long tag = protection switch
+ if (guest)
{
- MemoryPermission.None => 0L,
- MemoryPermission.Write => 2L << PointerTagBit,
- _ => 3L << PointerTagBit,
- };
+ // Protection is inverted on software pages, since the default value is 0.
+ protection = (~protection) & MemoryPermission.ReadAndWrite;
- int pages = GetPagesCount(va, (uint)size, out va);
- ulong pageStart = va >> PageBits;
- long invTagMask = ~(0xffffL << 48);
-
- for (int page = 0; page < pages; page++)
- {
- ref long pageRef = ref _pageTable.GetRef(pageStart * PteSize);
-
- long pte;
-
- do
+ long tag = protection switch
{
- pte = Volatile.Read(ref pageRef);
- }
- while (pte != 0 && Interlocked.CompareExchange(ref pageRef, (pte & invTagMask) | tag, pte) != pte);
+ MemoryPermission.None => 0L,
+ MemoryPermission.Write => 2L << PointerTagBit,
+ _ => 3L << PointerTagBit,
+ };
- pageStart++;
+ int pages = GetPagesCount(va, (uint)size, out va);
+ ulong pageStart = va >> PageBits;
+ long invTagMask = ~(0xffffL << 48);
+
+ for (int page = 0; page < pages; page++)
+ {
+ ref long pageRef = ref _pageTable.GetRef(pageStart * PteSize);
+
+ long pte;
+
+ do
+ {
+ pte = Volatile.Read(ref pageRef);
+ }
+ while (pte != 0 && Interlocked.CompareExchange(ref pageRef, (pte & invTagMask) | tag, pte) != pte);
+
+ pageStart++;
+ }
+ }
+ else
+ {
+ _pages.TrackingReprotect(va, size, protection);
}
}
///
- public RegionHandle BeginTracking(ulong address, ulong size, int id)
+ public RegionHandle BeginTracking(ulong address, ulong size, int id, RegionFlags flags = RegionFlags.None)
{
- return Tracking.BeginTracking(address, size, id);
+ return Tracking.BeginTracking(address, size, id, flags);
}
///
- public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable handles, ulong granularity, int id)
+ public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable handles, ulong granularity, int id, RegionFlags flags = RegionFlags.None)
{
- return Tracking.BeginGranularTracking(address, size, handles, granularity, id);
+ return Tracking.BeginGranularTracking(address, size, handles, granularity, id, flags);
}
///
@@ -639,8 +431,7 @@ namespace Ryujinx.Cpu.Jit
return Tracking.BeginSmartGranularTracking(address, size, granularity, id);
}
- ///
- public void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null)
+ private void SignalMemoryTrackingImpl(ulong va, ulong size, bool write, bool guest, bool precise = false, int? exemptId = null)
{
AssertValidAddressAndSize(va, size);
@@ -650,31 +441,45 @@ namespace Ryujinx.Cpu.Jit
return;
}
- // We emulate guard pages for software memory access. This makes for an easy transition to
- // tracking using host guard pages in future, but also supporting platforms where this is not possible.
+ // If the memory tracking is coming from the guest, use the tag bits in the page table entry.
+ // Otherwise, use the managed page flags.
- // Write tag includes read protection, since we don't have any read actions that aren't performed before write too.
- long tag = (write ? 3L : 1L) << PointerTagBit;
-
- int pages = GetPagesCount(va, (uint)size, out _);
- ulong pageStart = va >> PageBits;
-
- for (int page = 0; page < pages; page++)
+ if (guest)
{
- ref long pageRef = ref _pageTable.GetRef(pageStart * PteSize);
+ // We emulate guard pages for software memory access. This makes for an easy transition to
+ // tracking using host guard pages in future, but also supporting platforms where this is not possible.
- long pte;
+ // Write tag includes read protection, since we don't have any read actions that aren't performed before write too.
+ long tag = (write ? 3L : 1L) << PointerTagBit;
- pte = Volatile.Read(ref pageRef);
+ int pages = GetPagesCount(va, (uint)size, out _);
+ ulong pageStart = va >> PageBits;
- if ((pte & tag) != 0)
+ for (int page = 0; page < pages; page++)
{
- Tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId);
- break;
- }
+ ref long pageRef = ref _pageTable.GetRef(pageStart * PteSize);
- pageStart++;
+ long pte = Volatile.Read(ref pageRef);
+
+ if ((pte & tag) != 0)
+ {
+ Tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId, true);
+ break;
+ }
+
+ pageStart++;
+ }
}
+ else
+ {
+ _pages.SignalMemoryTracking(Tracking, va, size, write, exemptId);
+ }
+ }
+
+ ///
+ public override void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null)
+ {
+ SignalMemoryTrackingImpl(va, size, write, false, precise, exemptId);
}
private ulong PaToPte(ulong pa)
@@ -691,5 +496,17 @@ namespace Ryujinx.Cpu.Jit
/// Disposes of resources used by the memory manager.
///
protected override void Destroy() => _pageTable.Dispose();
+
+ protected override Memory GetPhysicalAddressMemory(nuint pa, int size)
+ => _backingMemory.GetMemory(pa, size);
+
+ protected override Span GetPhysicalAddressSpan(nuint pa, int size)
+ => _backingMemory.GetSpan(pa, size);
+
+ protected override nuint TranslateVirtualAddressChecked(ulong va)
+ => GetPhysicalAddressInternal(va);
+
+ protected override nuint TranslateVirtualAddressUnchecked(ulong va)
+ => GetPhysicalAddressInternal(va);
}
}
diff --git a/src/Ryujinx.Cpu/Jit/MemoryManagerHostMapped.cs b/src/Ryujinx.Cpu/Jit/MemoryManagerHostMapped.cs
index 2b315e8413..4639ab913d 100644
--- a/src/Ryujinx.Cpu/Jit/MemoryManagerHostMapped.cs
+++ b/src/Ryujinx.Cpu/Jit/MemoryManagerHostMapped.cs
@@ -3,52 +3,31 @@ using Ryujinx.Memory;
using Ryujinx.Memory.Range;
using Ryujinx.Memory.Tracking;
using System;
+using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
-using System.Threading;
namespace Ryujinx.Cpu.Jit
{
///
/// Represents a CPU memory manager which maps guest virtual memory directly onto a host virtual region.
///
- public sealed class MemoryManagerHostMapped : MemoryManagerBase, IMemoryManager, IVirtualMemoryManagerTracked, IWritableBlock
+ public sealed class MemoryManagerHostMapped : VirtualMemoryManagerRefCountedBase, IMemoryManager, IVirtualMemoryManagerTracked
{
- public const int PageBits = 12;
- public const int PageSize = 1 << PageBits;
- public const int PageMask = PageSize - 1;
-
- public const int PageToPteShift = 5; // 32 pages (2 bits each) in one ulong page table entry.
- public const ulong BlockMappedMask = 0x5555555555555555; // First bit of each table entry set.
-
- private enum HostMappedPtBits : ulong
- {
- Unmapped = 0,
- Mapped,
- WriteTracked,
- ReadWriteTracked,
-
- MappedReplicated = 0x5555555555555555,
- WriteTrackedReplicated = 0xaaaaaaaaaaaaaaaa,
- ReadWriteTrackedReplicated = ulong.MaxValue,
- }
-
private readonly InvalidAccessHandler _invalidAccessHandler;
private readonly bool _unsafeMode;
private readonly AddressSpace _addressSpace;
- public ulong AddressSpaceSize { get; }
-
private readonly PageTable _pageTable;
private readonly MemoryEhMeilleure _memoryEh;
- private readonly ulong[] _pageBitmap;
+ private readonly ManagedPageFlags _pages;
///
- public bool Supports4KBPages => MemoryBlock.GetPageSize() == PageSize;
+ public bool UsesPrivateAllocations => false;
public int AddressSpaceBits { get; }
@@ -60,6 +39,8 @@ namespace Ryujinx.Cpu.Jit
public event Action UnmapEvent;
+ protected override ulong AddressSpaceSize { get; }
+
///
/// Creates a new instance of the host mapped memory manager.
///
@@ -85,48 +66,12 @@ namespace Ryujinx.Cpu.Jit
AddressSpaceBits = asBits;
- _pageBitmap = new ulong[1 << (AddressSpaceBits - (PageBits + PageToPteShift))];
+ _pages = new ManagedPageFlags(AddressSpaceBits);
Tracking = new MemoryTracking(this, (int)MemoryBlock.GetPageSize(), invalidAccessHandler);
_memoryEh = new MemoryEhMeilleure(_addressSpace.Base, _addressSpace.Mirror, Tracking);
}
- ///
- /// Checks if the virtual address is part of the addressable space.
- ///
- /// Virtual address
- /// True if the virtual address is part of the addressable space
- private bool ValidateAddress(ulong va)
- {
- return va < AddressSpaceSize;
- }
-
- ///
- /// Checks if the combination of virtual address and size is part of the addressable space.
- ///
- /// Virtual address of the range
- /// Size of the range in bytes
- /// True if the combination of virtual address and size is part of the addressable space
- private bool ValidateAddressAndSize(ulong va, ulong size)
- {
- ulong endVa = va + size;
- return endVa >= va && endVa >= size && endVa <= AddressSpaceSize;
- }
-
- ///
- /// Ensures the combination of virtual address and size is part of the addressable space.
- ///
- /// Virtual address of the range
- /// Size of the range in bytes
- /// Throw when the memory region specified outside the addressable space
- private void AssertValidAddressAndSize(ulong va, ulong size)
- {
- if (!ValidateAddressAndSize(va, size))
- {
- throw new InvalidMemoryRegionException($"va=0x{va:X16}, size=0x{size:X16}");
- }
- }
-
///
/// Ensures the combination of virtual address and size is part of the addressable space and fully mapped.
///
@@ -134,7 +79,7 @@ namespace Ryujinx.Cpu.Jit
/// Size of the range in bytes
private void AssertMapped(ulong va, ulong size)
{
- if (!ValidateAddressAndSize(va, size) || !IsRangeMappedImpl(va, size))
+ if (!ValidateAddressAndSize(va, size) || !_pages.IsRangeMapped(va, size))
{
throw new InvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}");
}
@@ -146,18 +91,12 @@ namespace Ryujinx.Cpu.Jit
AssertValidAddressAndSize(va, size);
_addressSpace.Map(va, pa, size, flags);
- AddMapping(va, size);
+ _pages.AddMapping(va, size);
PtMap(va, pa, size);
Tracking.Map(va, size);
}
- ///
- public void MapForeign(ulong va, nuint hostPointer, ulong size)
- {
- throw new NotSupportedException();
- }
-
///
public void Unmap(ulong va, ulong size)
{
@@ -166,7 +105,7 @@ namespace Ryujinx.Cpu.Jit
UnmapEvent?.Invoke(va, size);
Tracking.Unmap(va, size);
- RemoveMapping(va, size);
+ _pages.RemoveMapping(va, size);
PtUnmap(va, size);
_addressSpace.Unmap(va, size);
}
@@ -194,8 +133,7 @@ namespace Ryujinx.Cpu.Jit
}
}
- ///
- public T Read(ulong va) where T : unmanaged
+ public override T Read(ulong va)
{
try
{
@@ -214,14 +152,11 @@ namespace Ryujinx.Cpu.Jit
}
}
- ///
- public T ReadTracked(ulong va) where T : unmanaged
+ public override T ReadTracked(ulong va)
{
try
{
- SignalMemoryTracking(va, (ulong)Unsafe.SizeOf(), false);
-
- return Read(va);
+ return base.ReadTracked(va);
}
catch (InvalidMemoryRegionException)
{
@@ -234,8 +169,7 @@ namespace Ryujinx.Cpu.Jit
}
}
- ///
- public void Read(ulong va, Span data)
+ public override void Read(ulong va, Span data)
{
try
{
@@ -252,9 +186,7 @@ namespace Ryujinx.Cpu.Jit
}
}
-
- ///
- public void Write(ulong va, T value) where T : unmanaged
+ public override void Write(ulong va, T value)
{
try
{
@@ -271,8 +203,7 @@ namespace Ryujinx.Cpu.Jit
}
}
- ///
- public void Write(ulong va, ReadOnlySpan data)
+ public override void Write(ulong va, ReadOnlySpan data)
{
try
{
@@ -289,8 +220,7 @@ namespace Ryujinx.Cpu.Jit
}
}
- ///
- public void WriteUntracked(ulong va, ReadOnlySpan data)
+ public override void WriteUntracked(ulong va, ReadOnlySpan data)
{
try
{
@@ -307,8 +237,7 @@ namespace Ryujinx.Cpu.Jit
}
}
- ///
- public bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan data)
+ public override bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan data)
{
try
{
@@ -335,8 +264,21 @@ namespace Ryujinx.Cpu.Jit
}
}
- ///
- public ReadOnlySpan GetSpan(ulong va, int size, bool tracked = false)
+ public override ReadOnlySequence GetReadOnlySequence(ulong va, int size, bool tracked = false)
+ {
+ if (tracked)
+ {
+ SignalMemoryTracking(va, (ulong)size, write: false);
+ }
+ else
+ {
+ AssertMapped(va, (ulong)size);
+ }
+
+ return new ReadOnlySequence(_addressSpace.Mirror.GetMemory(va, size));
+ }
+
+ public override ReadOnlySpan GetSpan(ulong va, int size, bool tracked = false)
{
if (tracked)
{
@@ -350,8 +292,7 @@ namespace Ryujinx.Cpu.Jit
return _addressSpace.Mirror.GetSpan(va, size);
}
- ///
- public WritableRegion GetWritableRegion(ulong va, int size, bool tracked = false)
+ public override WritableRegion GetWritableRegion(ulong va, int size, bool tracked = false)
{
if (tracked)
{
@@ -365,7 +306,6 @@ namespace Ryujinx.Cpu.Jit
return _addressSpace.Mirror.GetWritableRegion(va, size);
}
- ///
public ref T GetRef(ulong va) where T : unmanaged
{
SignalMemoryTracking(va, (ulong)Unsafe.SizeOf(), true);
@@ -373,26 +313,10 @@ namespace Ryujinx.Cpu.Jit
return ref _addressSpace.Mirror.GetRef(va);
}
- ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public bool IsMapped(ulong va)
+ public override bool IsMapped(ulong va)
{
- return ValidateAddress(va) && IsMappedImpl(va);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private bool IsMappedImpl(ulong va)
- {
- ulong page = va >> PageBits;
-
- int bit = (int)((page & 31) << 1);
-
- int pageIndex = (int)(page >> PageToPteShift);
- ref ulong pageRef = ref _pageBitmap[pageIndex];
-
- ulong pte = Volatile.Read(ref pageRef);
-
- return ((pte >> bit) & 3) != 0;
+ return ValidateAddress(va) && _pages.IsMapped(va);
}
///
@@ -400,58 +324,7 @@ namespace Ryujinx.Cpu.Jit
{
AssertValidAddressAndSize(va, size);
- return IsRangeMappedImpl(va, size);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void GetPageBlockRange(ulong pageStart, ulong pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex)
- {
- startMask = ulong.MaxValue << ((int)(pageStart & 31) << 1);
- endMask = ulong.MaxValue >> (64 - ((int)(pageEnd & 31) << 1));
-
- pageIndex = (int)(pageStart >> PageToPteShift);
- pageEndIndex = (int)((pageEnd - 1) >> PageToPteShift);
- }
-
- private bool IsRangeMappedImpl(ulong va, ulong size)
- {
- int pages = GetPagesCount(va, size, out _);
-
- if (pages == 1)
- {
- return IsMappedImpl(va);
- }
-
- ulong pageStart = va >> PageBits;
- ulong pageEnd = pageStart + (ulong)pages;
-
- GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
-
- // Check if either bit in each 2 bit page entry is set.
- // OR the block with itself shifted down by 1, and check the first bit of each entry.
-
- ulong mask = BlockMappedMask & startMask;
-
- while (pageIndex <= pageEndIndex)
- {
- if (pageIndex == pageEndIndex)
- {
- mask &= endMask;
- }
-
- ref ulong pageRef = ref _pageBitmap[pageIndex++];
- ulong pte = Volatile.Read(ref pageRef);
-
- pte |= pte >> 1;
- if ((pte & mask) != mask)
- {
- return false;
- }
-
- mask = BlockMappedMask;
- }
-
- return true;
+ return _pages.IsRangeMapped(va, size);
}
///
@@ -512,11 +385,10 @@ namespace Ryujinx.Cpu.Jit
return _pageTable.Read(va) + (va & PageMask);
}
- ///
///
/// This function also validates that the given range is both valid and mapped, and will throw if it is not.
///
- public void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null)
+ public override void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null)
{
AssertValidAddressAndSize(va, size);
@@ -526,93 +398,7 @@ namespace Ryujinx.Cpu.Jit
return;
}
- // Software table, used for managed memory tracking.
-
- int pages = GetPagesCount(va, size, out _);
- ulong pageStart = va >> PageBits;
-
- if (pages == 1)
- {
- ulong tag = (ulong)(write ? HostMappedPtBits.WriteTracked : HostMappedPtBits.ReadWriteTracked);
-
- int bit = (int)((pageStart & 31) << 1);
-
- int pageIndex = (int)(pageStart >> PageToPteShift);
- ref ulong pageRef = ref _pageBitmap[pageIndex];
-
- ulong pte = Volatile.Read(ref pageRef);
- ulong state = ((pte >> bit) & 3);
-
- if (state >= tag)
- {
- Tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId);
- return;
- }
- else if (state == 0)
- {
- ThrowInvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}");
- }
- }
- else
- {
- ulong pageEnd = pageStart + (ulong)pages;
-
- GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
-
- ulong mask = startMask;
-
- ulong anyTrackingTag = (ulong)HostMappedPtBits.WriteTrackedReplicated;
-
- while (pageIndex <= pageEndIndex)
- {
- if (pageIndex == pageEndIndex)
- {
- mask &= endMask;
- }
-
- ref ulong pageRef = ref _pageBitmap[pageIndex++];
-
- ulong pte = Volatile.Read(ref pageRef);
- ulong mappedMask = mask & BlockMappedMask;
-
- ulong mappedPte = pte | (pte >> 1);
- if ((mappedPte & mappedMask) != mappedMask)
- {
- ThrowInvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}");
- }
-
- pte &= mask;
- if ((pte & anyTrackingTag) != 0) // Search for any tracking.
- {
- // Writes trigger any tracking.
- // Only trigger tracking from reads if both bits are set on any page.
- if (write || (pte & (pte >> 1) & BlockMappedMask) != 0)
- {
- Tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId);
- break;
- }
- }
-
- mask = ulong.MaxValue;
- }
- }
- }
-
- ///
- /// Computes the number of pages in a virtual address range.
- ///
- /// Virtual address of the range
- /// Size of the range
- /// The virtual address of the beginning of the first page
- /// This function does not differentiate between allocated and unallocated pages.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static int GetPagesCount(ulong va, ulong size, out ulong startVa)
- {
- // WARNING: Always check if ulong does not overflow during the operations.
- startVa = va & ~(ulong)PageMask;
- ulong vaSpan = (va - startVa + size + PageMask) & ~(ulong)PageMask;
-
- return (int)(vaSpan / PageSize);
+ _pages.SignalMemoryTracking(Tracking, va, size, write, exemptId);
}
///
@@ -622,103 +408,28 @@ namespace Ryujinx.Cpu.Jit
}
///
- public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
+ public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection, bool guest)
{
- // Protection is inverted on software pages, since the default value is 0.
- protection = (~protection) & MemoryPermission.ReadAndWrite;
-
- int pages = GetPagesCount(va, size, out va);
- ulong pageStart = va >> PageBits;
-
- if (pages == 1)
+ if (guest)
{
- ulong protTag = protection switch
- {
- MemoryPermission.None => (ulong)HostMappedPtBits.Mapped,
- MemoryPermission.Write => (ulong)HostMappedPtBits.WriteTracked,
- _ => (ulong)HostMappedPtBits.ReadWriteTracked,
- };
-
- int bit = (int)((pageStart & 31) << 1);
-
- ulong tagMask = 3UL << bit;
- ulong invTagMask = ~tagMask;
-
- ulong tag = protTag << bit;
-
- int pageIndex = (int)(pageStart >> PageToPteShift);
- ref ulong pageRef = ref _pageBitmap[pageIndex];
-
- ulong pte;
-
- do
- {
- pte = Volatile.Read(ref pageRef);
- }
- while ((pte & tagMask) != 0 && Interlocked.CompareExchange(ref pageRef, (pte & invTagMask) | tag, pte) != pte);
+ _addressSpace.Base.Reprotect(va, size, protection, false);
}
else
{
- ulong pageEnd = pageStart + (ulong)pages;
-
- GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
-
- ulong mask = startMask;
-
- ulong protTag = protection switch
- {
- MemoryPermission.None => (ulong)HostMappedPtBits.MappedReplicated,
- MemoryPermission.Write => (ulong)HostMappedPtBits.WriteTrackedReplicated,
- _ => (ulong)HostMappedPtBits.ReadWriteTrackedReplicated,
- };
-
- while (pageIndex <= pageEndIndex)
- {
- if (pageIndex == pageEndIndex)
- {
- mask &= endMask;
- }
-
- ref ulong pageRef = ref _pageBitmap[pageIndex++];
-
- ulong pte;
- ulong mappedMask;
-
- // Change the protection of all 2 bit entries that are mapped.
- do
- {
- pte = Volatile.Read(ref pageRef);
-
- mappedMask = pte | (pte >> 1);
- mappedMask |= (mappedMask & BlockMappedMask) << 1;
- mappedMask &= mask; // Only update mapped pages within the given range.
- }
- while (Interlocked.CompareExchange(ref pageRef, (pte & (~mappedMask)) | (protTag & mappedMask), pte) != pte);
-
- mask = ulong.MaxValue;
- }
+ _pages.TrackingReprotect(va, size, protection);
}
-
- protection = protection switch
- {
- MemoryPermission.None => MemoryPermission.ReadAndWrite,
- MemoryPermission.Write => MemoryPermission.Read,
- _ => MemoryPermission.None,
- };
-
- _addressSpace.Base.Reprotect(va, size, protection, false);
}
///
- public RegionHandle BeginTracking(ulong address, ulong size, int id)
+ public RegionHandle BeginTracking(ulong address, ulong size, int id, RegionFlags flags = RegionFlags.None)
{
- return Tracking.BeginTracking(address, size, id);
+ return Tracking.BeginTracking(address, size, id, flags);
}
///
- public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable handles, ulong granularity, int id)
+ public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable handles, ulong granularity, int id, RegionFlags flags = RegionFlags.None)
{
- return Tracking.BeginGranularTracking(address, size, handles, granularity, id);
+ return Tracking.BeginGranularTracking(address, size, handles, granularity, id, flags);
}
///
@@ -727,86 +438,6 @@ namespace Ryujinx.Cpu.Jit
return Tracking.BeginSmartGranularTracking(address, size, granularity, id);
}
- ///
- /// Adds the given address mapping to the page table.
- ///
- /// Virtual memory address
- /// Size to be mapped
- private void AddMapping(ulong va, ulong size)
- {
- int pages = GetPagesCount(va, size, out _);
- ulong pageStart = va >> PageBits;
- ulong pageEnd = pageStart + (ulong)pages;
-
- GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
-
- ulong mask = startMask;
-
- while (pageIndex <= pageEndIndex)
- {
- if (pageIndex == pageEndIndex)
- {
- mask &= endMask;
- }
-
- ref ulong pageRef = ref _pageBitmap[pageIndex++];
-
- ulong pte;
- ulong mappedMask;
-
- // Map all 2-bit entries that are unmapped.
- do
- {
- pte = Volatile.Read(ref pageRef);
-
- mappedMask = pte | (pte >> 1);
- mappedMask |= (mappedMask & BlockMappedMask) << 1;
- mappedMask |= ~mask; // Treat everything outside the range as mapped, thus unchanged.
- }
- while (Interlocked.CompareExchange(ref pageRef, (pte & mappedMask) | (BlockMappedMask & (~mappedMask)), pte) != pte);
-
- mask = ulong.MaxValue;
- }
- }
-
- ///
- /// Removes the given address mapping from the page table.
- ///
- /// Virtual memory address
- /// Size to be unmapped
- private void RemoveMapping(ulong va, ulong size)
- {
- int pages = GetPagesCount(va, size, out _);
- ulong pageStart = va >> PageBits;
- ulong pageEnd = pageStart + (ulong)pages;
-
- GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
-
- startMask = ~startMask;
- endMask = ~endMask;
-
- ulong mask = startMask;
-
- while (pageIndex <= pageEndIndex)
- {
- if (pageIndex == pageEndIndex)
- {
- mask |= endMask;
- }
-
- ref ulong pageRef = ref _pageBitmap[pageIndex++];
- ulong pte;
-
- do
- {
- pte = Volatile.Read(ref pageRef);
- }
- while (Interlocked.CompareExchange(ref pageRef, pte & mask, pte) != pte);
-
- mask = 0;
- }
- }
-
///
/// Disposes of resources used by the memory manager.
///
@@ -816,6 +447,16 @@ namespace Ryujinx.Cpu.Jit
_memoryEh.Dispose();
}
- private static void ThrowInvalidMemoryRegionException(string message) => throw new InvalidMemoryRegionException(message);
+ protected override Memory GetPhysicalAddressMemory(nuint pa, int size)
+ => _addressSpace.Mirror.GetMemory(pa, size);
+
+ protected override Span GetPhysicalAddressSpan(nuint pa, int size)
+ => _addressSpace.Mirror.GetSpan(pa, size);
+
+ protected override nuint TranslateVirtualAddressChecked(ulong va)
+ => (nuint)GetPhysicalAddressChecked(va);
+
+ protected override nuint TranslateVirtualAddressUnchecked(ulong va)
+ => (nuint)GetPhysicalAddressInternal(va);
}
}
diff --git a/src/Ryujinx.Cpu/Jit/MemoryManagerHostTracked.cs b/src/Ryujinx.Cpu/Jit/MemoryManagerHostTracked.cs
new file mode 100644
index 0000000000..663d0aeb15
--- /dev/null
+++ b/src/Ryujinx.Cpu/Jit/MemoryManagerHostTracked.cs
@@ -0,0 +1,634 @@
+using ARMeilleure.Memory;
+using Ryujinx.Common.Memory;
+using Ryujinx.Cpu.Jit.HostTracked;
+using Ryujinx.Cpu.Signal;
+using Ryujinx.Memory;
+using Ryujinx.Memory.Range;
+using Ryujinx.Memory.Tracking;
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+namespace Ryujinx.Cpu.Jit
+{
+ ///
+ /// Represents a CPU memory manager which maps guest virtual memory directly onto a host virtual region.
+ ///
+ public sealed class MemoryManagerHostTracked : VirtualMemoryManagerRefCountedBase, IMemoryManager, IVirtualMemoryManagerTracked
+ {
+ private readonly InvalidAccessHandler _invalidAccessHandler;
+ private readonly bool _unsafeMode;
+
+ private readonly MemoryBlock _backingMemory;
+
+ public int AddressSpaceBits { get; }
+
+ public MemoryTracking Tracking { get; }
+
+ private readonly NativePageTable _nativePageTable;
+ private readonly AddressSpacePartitioned _addressSpace;
+
+ private readonly ManagedPageFlags _pages;
+
+ protected override ulong AddressSpaceSize { get; }
+
+ ///
+ public bool UsesPrivateAllocations => true;
+
+ public IntPtr PageTablePointer => _nativePageTable.PageTablePointer;
+
+ public MemoryManagerType Type => _unsafeMode ? MemoryManagerType.HostTrackedUnsafe : MemoryManagerType.HostTracked;
+
+ public event Action UnmapEvent;
+
+ ///
+ /// Creates a new instance of the host tracked memory manager.
+ ///
+ /// Physical backing memory where virtual memory will be mapped to
+ /// Size of the address space
+ /// True if unmanaged access should not be masked (unsafe), false otherwise.
+ /// Optional function to handle invalid memory accesses
+ public MemoryManagerHostTracked(MemoryBlock backingMemory, ulong addressSpaceSize, bool unsafeMode, InvalidAccessHandler invalidAccessHandler)
+ {
+ bool useProtectionMirrors = MemoryBlock.GetPageSize() > PageSize;
+
+ Tracking = new MemoryTracking(this, PageSize, invalidAccessHandler, useProtectionMirrors);
+
+ _backingMemory = backingMemory;
+ _invalidAccessHandler = invalidAccessHandler;
+ _unsafeMode = unsafeMode;
+ AddressSpaceSize = addressSpaceSize;
+
+ ulong asSize = PageSize;
+ int asBits = PageBits;
+
+ while (asSize < AddressSpaceSize)
+ {
+ asSize <<= 1;
+ asBits++;
+ }
+
+ AddressSpaceBits = asBits;
+
+ if (useProtectionMirrors && !NativeSignalHandler.SupportsFaultAddressPatching())
+ {
+ // Currently we require being able to change the fault address to something else
+ // in order to "emulate" 4KB granularity protection on systems with larger page size.
+
+ throw new PlatformNotSupportedException();
+ }
+
+ _pages = new ManagedPageFlags(asBits);
+ _nativePageTable = new(asSize);
+ _addressSpace = new(Tracking, backingMemory, _nativePageTable, useProtectionMirrors);
+ }
+
+ public override ReadOnlySequence GetReadOnlySequence(ulong va, int size, bool tracked = false)
+ {
+ if (size == 0)
+ {
+ return ReadOnlySequence.Empty;
+ }
+
+ try
+ {
+ if (tracked)
+ {
+ SignalMemoryTracking(va, (ulong)size, false);
+ }
+ else
+ {
+ AssertValidAddressAndSize(va, (ulong)size);
+ }
+
+ ulong endVa = va + (ulong)size;
+ int offset = 0;
+
+ BytesReadOnlySequenceSegment first = null, last = null;
+
+ while (va < endVa)
+ {
+ (MemoryBlock memory, ulong rangeOffset, ulong copySize) = GetMemoryOffsetAndSize(va, (ulong)(size - offset));
+
+ Memory physicalMemory = memory.GetMemory(rangeOffset, (int)copySize);
+
+ if (first is null)
+ {
+ first = last = new BytesReadOnlySequenceSegment(physicalMemory);
+ }
+ else
+ {
+ if (last.IsContiguousWith(physicalMemory, out nuint contiguousStart, out int contiguousSize))
+ {
+ Memory contiguousPhysicalMemory = new NativeMemoryManager(contiguousStart, contiguousSize).Memory;
+
+ last.Replace(contiguousPhysicalMemory);
+ }
+ else
+ {
+ last = last.Append(physicalMemory);
+ }
+ }
+
+ va += copySize;
+ offset += (int)copySize;
+ }
+
+ return new ReadOnlySequence(first, 0, last, (int)(size - last.RunningIndex));
+ }
+ catch (InvalidMemoryRegionException)
+ {
+ if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
+ {
+ throw;
+ }
+
+ return ReadOnlySequence.Empty;
+ }
+ }
+
+ ///
+ public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags)
+ {
+ AssertValidAddressAndSize(va, size);
+
+ if (flags.HasFlag(MemoryMapFlags.Private))
+ {
+ _addressSpace.Map(va, pa, size);
+ }
+
+ _pages.AddMapping(va, size);
+ _nativePageTable.Map(va, pa, size, _addressSpace, _backingMemory, flags.HasFlag(MemoryMapFlags.Private));
+
+ Tracking.Map(va, size);
+ }
+
+ ///
+ public void Unmap(ulong va, ulong size)
+ {
+ AssertValidAddressAndSize(va, size);
+
+ _addressSpace.Unmap(va, size);
+
+ UnmapEvent?.Invoke(va, size);
+ Tracking.Unmap(va, size);
+
+ _pages.RemoveMapping(va, size);
+ _nativePageTable.Unmap(va, size);
+ }
+
+ public override T ReadTracked(ulong va)
+ {
+ try
+ {
+ return base.ReadTracked(va);
+ }
+ catch (InvalidMemoryRegionException)
+ {
+ if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
+ {
+ throw;
+ }
+
+ return default;
+ }
+ }
+
+ public override void Read(ulong va, Span data)
+ {
+ if (data.Length == 0)
+ {
+ return;
+ }
+
+ try
+ {
+ AssertValidAddressAndSize(va, (ulong)data.Length);
+
+ ulong endVa = va + (ulong)data.Length;
+ int offset = 0;
+
+ while (va < endVa)
+ {
+ (MemoryBlock memory, ulong rangeOffset, ulong copySize) = GetMemoryOffsetAndSize(va, (ulong)(data.Length - offset));
+
+ memory.GetSpan(rangeOffset, (int)copySize).CopyTo(data.Slice(offset, (int)copySize));
+
+ va += copySize;
+ offset += (int)copySize;
+ }
+ }
+ catch (InvalidMemoryRegionException)
+ {
+ if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
+ {
+ throw;
+ }
+ }
+ }
+
+ public override bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan data)
+ {
+ if (data.Length == 0)
+ {
+ return false;
+ }
+
+ SignalMemoryTracking(va, (ulong)data.Length, false);
+
+ if (TryGetVirtualContiguous(va, data.Length, out MemoryBlock memoryBlock, out ulong offset))
+ {
+ var target = memoryBlock.GetSpan(offset, data.Length);
+
+ bool changed = !data.SequenceEqual(target);
+
+ if (changed)
+ {
+ data.CopyTo(target);
+ }
+
+ return changed;
+ }
+ else
+ {
+ WriteImpl(va, data);
+
+ return true;
+ }
+ }
+
+ public override ReadOnlySpan GetSpan(ulong va, int size, bool tracked = false)
+ {
+ if (size == 0)
+ {
+ return ReadOnlySpan.Empty;
+ }
+
+ if (tracked)
+ {
+ SignalMemoryTracking(va, (ulong)size, false);
+ }
+
+ if (TryGetVirtualContiguous(va, size, out MemoryBlock memoryBlock, out ulong offset))
+ {
+ return memoryBlock.GetSpan(offset, size);
+ }
+ else
+ {
+ Span data = new byte[size];
+
+ Read(va, data);
+
+ return data;
+ }
+ }
+
+ public override WritableRegion GetWritableRegion(ulong va, int size, bool tracked = false)
+ {
+ if (size == 0)
+ {
+ return new WritableRegion(null, va, Memory.Empty);
+ }
+
+ if (tracked)
+ {
+ SignalMemoryTracking(va, (ulong)size, true);
+ }
+
+ if (TryGetVirtualContiguous(va, size, out MemoryBlock memoryBlock, out ulong offset))
+ {
+ return new WritableRegion(null, va, memoryBlock.GetMemory(offset, size));
+ }
+ else
+ {
+ IMemoryOwner memoryOwner = ByteMemoryPool.Rent(size);
+
+ Read(va, memoryOwner.Memory.Span);
+
+ return new WritableRegion(this, va, memoryOwner);
+ }
+ }
+
+ public ref T GetRef(ulong va) where T : unmanaged
+ {
+ if (!TryGetVirtualContiguous(va, Unsafe.SizeOf(), out MemoryBlock memory, out ulong offset))
+ {
+ ThrowMemoryNotContiguous();
+ }
+
+ SignalMemoryTracking(va, (ulong)Unsafe.SizeOf(), true);
+
+ return ref memory.GetRef(offset);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool IsMapped(ulong va)
+ {
+ return ValidateAddress(va) && _pages.IsMapped(va);
+ }
+
+ public bool IsRangeMapped(ulong va, ulong size)
+ {
+ AssertValidAddressAndSize(va, size);
+
+ return _pages.IsRangeMapped(va, size);
+ }
+
+ private bool TryGetVirtualContiguous(ulong va, int size, out MemoryBlock memory, out ulong offset)
+ {
+ if (_addressSpace.HasAnyPrivateAllocation(va, (ulong)size, out PrivateRange range))
+ {
+ // If we have a private allocation overlapping the range,
+ // then the access is only considered contiguous if it covers the entire range.
+
+ if (range.Memory != null)
+ {
+ memory = range.Memory;
+ offset = range.Offset;
+
+ return true;
+ }
+
+ memory = null;
+ offset = 0;
+
+ return false;
+ }
+
+ memory = _backingMemory;
+ offset = GetPhysicalAddressInternal(va);
+
+ return IsPhysicalContiguous(va, size);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool IsPhysicalContiguous(ulong va, int size)
+ {
+ if (!ValidateAddress(va) || !ValidateAddressAndSize(va, (ulong)size))
+ {
+ return false;
+ }
+
+ int pages = GetPagesCount(va, (uint)size, out va);
+
+ for (int page = 0; page < pages - 1; page++)
+ {
+ if (!ValidateAddress(va + PageSize))
+ {
+ return false;
+ }
+
+ if (GetPhysicalAddressInternal(va) + PageSize != GetPhysicalAddressInternal(va + PageSize))
+ {
+ return false;
+ }
+
+ va += PageSize;
+ }
+
+ return true;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private ulong GetContiguousSize(ulong va, ulong size)
+ {
+ ulong contiguousSize = PageSize - (va & PageMask);
+
+ if (!ValidateAddress(va) || !ValidateAddressAndSize(va, size))
+ {
+ return contiguousSize;
+ }
+
+ int pages = GetPagesCount(va, size, out va);
+
+ for (int page = 0; page < pages - 1; page++)
+ {
+ if (!ValidateAddress(va + PageSize))
+ {
+ return contiguousSize;
+ }
+
+ if (GetPhysicalAddressInternal(va) + PageSize != GetPhysicalAddressInternal(va + PageSize))
+ {
+ return contiguousSize;
+ }
+
+ va += PageSize;
+ contiguousSize += PageSize;
+ }
+
+ return Math.Min(contiguousSize, size);
+ }
+
+ private (MemoryBlock, ulong, ulong) GetMemoryOffsetAndSize(ulong va, ulong size)
+ {
+ PrivateRange privateRange = _addressSpace.GetFirstPrivateAllocation(va, size, out ulong nextVa);
+
+ if (privateRange.Memory != null)
+ {
+ return (privateRange.Memory, privateRange.Offset, privateRange.Size);
+ }
+
+ ulong physSize = GetContiguousSize(va, Math.Min(size, nextVa - va));
+
+ return (_backingMemory, GetPhysicalAddressChecked(va), physSize);
+ }
+
+ public IEnumerable GetHostRegions(ulong va, ulong size)
+ {
+ if (!ValidateAddressAndSize(va, size))
+ {
+ return null;
+ }
+
+ var regions = new List();
+ ulong endVa = va + size;
+
+ try
+ {
+ while (va < endVa)
+ {
+ (MemoryBlock memory, ulong rangeOffset, ulong rangeSize) = GetMemoryOffsetAndSize(va, endVa - va);
+
+ regions.Add(new((UIntPtr)memory.GetPointer(rangeOffset, rangeSize), rangeSize));
+
+ va += rangeSize;
+ }
+ }
+ catch (InvalidMemoryRegionException)
+ {
+ return null;
+ }
+
+ return regions;
+ }
+
+ public IEnumerable GetPhysicalRegions(ulong va, ulong size)
+ {
+ if (size == 0)
+ {
+ return Enumerable.Empty();
+ }
+
+ return GetPhysicalRegionsImpl(va, size);
+ }
+
+ private List GetPhysicalRegionsImpl(ulong va, ulong size)
+ {
+ if (!ValidateAddress(va) || !ValidateAddressAndSize(va, size))
+ {
+ return null;
+ }
+
+ int pages = GetPagesCount(va, (uint)size, out va);
+
+ var regions = new List();
+
+ ulong regionStart = GetPhysicalAddressInternal(va);
+ ulong regionSize = PageSize;
+
+ for (int page = 0; page < pages - 1; page++)
+ {
+ if (!ValidateAddress(va + PageSize))
+ {
+ return null;
+ }
+
+ ulong newPa = GetPhysicalAddressInternal(va + PageSize);
+
+ if (GetPhysicalAddressInternal(va) + PageSize != newPa)
+ {
+ regions.Add(new MemoryRange(regionStart, regionSize));
+ regionStart = newPa;
+ regionSize = 0;
+ }
+
+ va += PageSize;
+ regionSize += PageSize;
+ }
+
+ regions.Add(new MemoryRange(regionStart, regionSize));
+
+ return regions;
+ }
+
+ ///
+ ///
+ /// This function also validates that the given range is both valid and mapped, and will throw if it is not.
+ ///
+ public override void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null)
+ {
+ AssertValidAddressAndSize(va, size);
+
+ if (precise)
+ {
+ Tracking.VirtualMemoryEvent(va, size, write, precise: true, exemptId);
+ return;
+ }
+
+ // Software table, used for managed memory tracking.
+
+ _pages.SignalMemoryTracking(Tracking, va, size, write, exemptId);
+ }
+
+ public RegionHandle BeginTracking(ulong address, ulong size, int id, RegionFlags flags = RegionFlags.None)
+ {
+ return Tracking.BeginTracking(address, size, id, flags);
+ }
+
+ public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable handles, ulong granularity, int id, RegionFlags flags = RegionFlags.None)
+ {
+ return Tracking.BeginGranularTracking(address, size, handles, granularity, id, flags);
+ }
+
+ public SmartMultiRegionHandle BeginSmartGranularTracking(ulong address, ulong size, ulong granularity, int id)
+ {
+ return Tracking.BeginSmartGranularTracking(address, size, granularity, id);
+ }
+
+ private ulong GetPhysicalAddressChecked(ulong va)
+ {
+ if (!IsMapped(va))
+ {
+ ThrowInvalidMemoryRegionException($"Not mapped: va=0x{va:X16}");
+ }
+
+ return GetPhysicalAddressInternal(va);
+ }
+
+ private ulong GetPhysicalAddressInternal(ulong va)
+ {
+ return _nativePageTable.GetPhysicalAddress(va);
+ }
+
+ ///
+ public void Reprotect(ulong va, ulong size, MemoryPermission protection)
+ {
+ // TODO
+ }
+
+ ///
+ public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection, bool guest)
+ {
+ if (guest)
+ {
+ _addressSpace.Reprotect(va, size, protection);
+ }
+ else
+ {
+ _pages.TrackingReprotect(va, size, protection);
+ }
+ }
+
+ ///
+ /// Disposes of resources used by the memory manager.
+ ///
+ protected override void Destroy()
+ {
+ _addressSpace.Dispose();
+ _nativePageTable.Dispose();
+ }
+
+ protected override Memory GetPhysicalAddressMemory(nuint pa, int size)
+ => _backingMemory.GetMemory(pa, size);
+
+ protected override Span GetPhysicalAddressSpan(nuint pa, int size)
+ => _backingMemory.GetSpan(pa, size);
+
+ protected override void WriteImpl(ulong va, ReadOnlySpan data)
+ {
+ try
+ {
+ AssertValidAddressAndSize(va, (ulong)data.Length);
+
+ ulong endVa = va + (ulong)data.Length;
+ int offset = 0;
+
+ while (va < endVa)
+ {
+ (MemoryBlock memory, ulong rangeOffset, ulong copySize) = GetMemoryOffsetAndSize(va, (ulong)(data.Length - offset));
+
+ data.Slice(offset, (int)copySize).CopyTo(memory.GetSpan(rangeOffset, (int)copySize));
+
+ va += copySize;
+ offset += (int)copySize;
+ }
+ }
+ catch (InvalidMemoryRegionException)
+ {
+ if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
+ {
+ throw;
+ }
+ }
+ }
+
+ protected override nuint TranslateVirtualAddressChecked(ulong va)
+ => (nuint)GetPhysicalAddressChecked(va);
+
+ protected override nuint TranslateVirtualAddressUnchecked(ulong va)
+ => (nuint)GetPhysicalAddressInternal(va);
+ }
+}
diff --git a/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitMemory.cs b/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitMemory.cs
index 6ab4b94953..d8caee6e74 100644
--- a/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitMemory.cs
+++ b/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitMemory.cs
@@ -1126,11 +1126,23 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64
Operand destination64 = new(destination.Kind, OperandType.I64, destination.Value);
Operand basePointer = new(regAlloc.FixedPageTableRegister, RegisterType.Integer, OperandType.I64);
- if (mmType == MemoryManagerType.HostMapped || mmType == MemoryManagerType.HostMappedUnsafe)
- {
- // We don't need to mask the address for the safe mode, since it is already naturally limited to 32-bit
- // and can never reach out of the guest address space.
+ // We don't need to mask the address for the safe mode, since it is already naturally limited to 32-bit
+ // and can never reach out of the guest address space.
+ if (mmType.IsHostTracked())
+ {
+ int tempRegister = regAlloc.AllocateTempGprRegister();
+
+ Operand pte = new(tempRegister, RegisterType.Integer, OperandType.I64);
+
+ asm.Lsr(pte, guestAddress, new Operand(OperandKind.Constant, OperandType.I32, 12));
+ asm.LdrRr(pte, basePointer, pte, ArmExtensionType.Uxtx, true);
+ asm.Add(destination64, pte, guestAddress);
+
+ regAlloc.FreeTempGprRegister(tempRegister);
+ }
+ else if (mmType.IsHostMapped())
+ {
asm.Add(destination64, basePointer, guestAddress);
}
else
diff --git a/src/Ryujinx.Cpu/LightningJit/Arm64/InstName.cs b/src/Ryujinx.Cpu/LightningJit/Arm64/InstName.cs
index 58d78ae6e3..3391a2c145 100644
--- a/src/Ryujinx.Cpu/LightningJit/Arm64/InstName.cs
+++ b/src/Ryujinx.Cpu/LightningJit/Arm64/InstName.cs
@@ -1106,6 +1106,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64
case InstName.Mrs:
case InstName.MsrImm:
case InstName.MsrReg:
+ case InstName.Sysl:
return true;
}
@@ -1130,5 +1131,37 @@ namespace Ryujinx.Cpu.LightningJit.Arm64
return false;
}
+
+ public static bool IsPartialRegisterUpdateMemory(this InstName name)
+ {
+ switch (name)
+ {
+ case InstName.Ld1AdvsimdSnglAsNoPostIndex:
+ case InstName.Ld1AdvsimdSnglAsPostIndex:
+ case InstName.Ld2AdvsimdSnglAsNoPostIndex:
+ case InstName.Ld2AdvsimdSnglAsPostIndex:
+ case InstName.Ld3AdvsimdSnglAsNoPostIndex:
+ case InstName.Ld3AdvsimdSnglAsPostIndex:
+ case InstName.Ld4AdvsimdSnglAsNoPostIndex:
+ case InstName.Ld4AdvsimdSnglAsPostIndex:
+ return true;
+ }
+
+ return false;
+ }
+
+ public static bool IsPrefetchMemory(this InstName name)
+ {
+ switch (name)
+ {
+ case InstName.PrfmImm:
+ case InstName.PrfmLit:
+ case InstName.PrfmReg:
+ case InstName.Prfum:
+ return true;
+ }
+
+ return false;
+ }
}
}
diff --git a/src/Ryujinx.Cpu/LightningJit/Arm64/RegisterAllocator.cs b/src/Ryujinx.Cpu/LightningJit/Arm64/RegisterAllocator.cs
index c9a932093d..1c6eab0de2 100644
--- a/src/Ryujinx.Cpu/LightningJit/Arm64/RegisterAllocator.cs
+++ b/src/Ryujinx.Cpu/LightningJit/Arm64/RegisterAllocator.cs
@@ -1,15 +1,12 @@
+using ARMeilleure.Memory;
using Ryujinx.Cpu.LightningJit.CodeGen.Arm64;
using System;
-using System.Diagnostics;
using System.Numerics;
namespace Ryujinx.Cpu.LightningJit.Arm64
{
class RegisterAllocator
{
- public const int MaxTemps = 1;
- public const int MaxTempsInclFixed = MaxTemps + 2;
-
private uint _gprMask;
private readonly uint _fpSimdMask;
private readonly uint _pStateMask;
@@ -25,7 +22,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64
public uint AllFpSimdMask => _fpSimdMask;
public uint AllPStateMask => _pStateMask;
- public RegisterAllocator(uint gprMask, uint fpSimdMask, uint pStateMask, bool hasHostCall)
+ public RegisterAllocator(MemoryManagerType mmType, uint gprMask, uint fpSimdMask, uint pStateMask, bool hasHostCall)
{
_gprMask = gprMask;
_fpSimdMask = fpSimdMask;
@@ -56,7 +53,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64
BuildRegisterMap(_registerMap);
- Span tempRegisters = stackalloc int[MaxTemps];
+ Span tempRegisters = stackalloc int[CalculateMaxTemps(mmType)];
for (int index = 0; index < tempRegisters.Length; index++)
{
@@ -150,5 +147,15 @@ namespace Ryujinx.Cpu.LightningJit.Arm64
{
mask &= ~(1u << index);
}
+
+ public static int CalculateMaxTemps(MemoryManagerType mmType)
+ {
+ return mmType.IsHostMapped() ? 1 : 2;
+ }
+
+ public static int CalculateMaxTempsInclFixed(MemoryManagerType mmType)
+ {
+ return CalculateMaxTemps(mmType) + 2;
+ }
}
}
diff --git a/src/Ryujinx.Cpu/LightningJit/Arm64/RegisterUtils.cs b/src/Ryujinx.Cpu/LightningJit/Arm64/RegisterUtils.cs
index eb3fc229fe..191e03e7b1 100644
--- a/src/Ryujinx.Cpu/LightningJit/Arm64/RegisterUtils.cs
+++ b/src/Ryujinx.Cpu/LightningJit/Arm64/RegisterUtils.cs
@@ -247,7 +247,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64
}
}
- if (!flags.HasFlag(InstFlags.ReadRt))
+ if (!flags.HasFlag(InstFlags.ReadRt) || name.IsPartialRegisterUpdateMemory())
{
if (flags.HasFlag(InstFlags.Rt))
{
@@ -281,7 +281,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64
gprMask |= MaskFromIndex(ExtractRd(flags, encoding));
}
- if (!flags.HasFlag(InstFlags.ReadRt))
+ if (!flags.HasFlag(InstFlags.ReadRt) || name.IsPartialRegisterUpdateMemory())
{
if (flags.HasFlag(InstFlags.Rt))
{
diff --git a/src/Ryujinx.Cpu/LightningJit/Arm64/SysUtils.cs b/src/Ryujinx.Cpu/LightningJit/Arm64/SysUtils.cs
new file mode 100644
index 0000000000..69689a3910
--- /dev/null
+++ b/src/Ryujinx.Cpu/LightningJit/Arm64/SysUtils.cs
@@ -0,0 +1,48 @@
+using System.Diagnostics;
+
+namespace Ryujinx.Cpu.LightningJit.Arm64
+{
+ static class SysUtils
+ {
+ public static (uint, uint, uint, uint) UnpackOp1CRnCRmOp2(uint encoding)
+ {
+ uint op1 = (encoding >> 16) & 7;
+ uint crn = (encoding >> 12) & 0xf;
+ uint crm = (encoding >> 8) & 0xf;
+ uint op2 = (encoding >> 5) & 7;
+
+ return (op1, crn, crm, op2);
+ }
+
+ public static bool IsCacheInstEl0(uint encoding)
+ {
+ (uint op1, uint crn, uint crm, uint op2) = UnpackOp1CRnCRmOp2(encoding);
+
+ return ((op1 << 11) | (crn << 7) | (crm << 3) | op2) switch
+ {
+ 0b011_0111_0100_001 => true, // DC ZVA
+ 0b011_0111_1010_001 => true, // DC CVAC
+ 0b011_0111_1100_001 => true, // DC CVAP
+ 0b011_0111_1011_001 => true, // DC CVAU
+ 0b011_0111_1110_001 => true, // DC CIVAC
+ 0b011_0111_0101_001 => true, // IC IVAU
+ _ => false,
+ };
+ }
+
+ public static bool IsCacheInstUciTrapped(uint encoding)
+ {
+ (uint op1, uint crn, uint crm, uint op2) = UnpackOp1CRnCRmOp2(encoding);
+
+ return ((op1 << 11) | (crn << 7) | (crm << 3) | op2) switch
+ {
+ 0b011_0111_1010_001 => true, // DC CVAC
+ 0b011_0111_1100_001 => true, // DC CVAP
+ 0b011_0111_1011_001 => true, // DC CVAU
+ 0b011_0111_1110_001 => true, // DC CIVAC
+ 0b011_0111_0101_001 => true, // IC IVAU
+ _ => false,
+ };
+ }
+ }
+}
diff --git a/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/Compiler.cs b/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/Compiler.cs
index 7ef3bf49b9..7a6d761e8d 100644
--- a/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/Compiler.cs
+++ b/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/Compiler.cs
@@ -316,7 +316,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
uint pStateUseMask = multiBlock.GlobalUseMask.PStateMask;
CodeWriter writer = new();
- RegisterAllocator regAlloc = new(gprUseMask, fpSimdUseMask, pStateUseMask, multiBlock.HasHostCall);
+ RegisterAllocator regAlloc = new(memoryManager.Type, gprUseMask, fpSimdUseMask, pStateUseMask, multiBlock.HasHostCall);
RegisterSaveRestore rsr = new(
regAlloc.AllGprMask & AbiConstants.GprCalleeSavedRegsMask,
regAlloc.AllFpSimdMask & AbiConstants.FpSimdCalleeSavedRegsMask,
diff --git a/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/Decoder.cs b/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/Decoder.cs
index e9ba8ba215..d5e1eb19c2 100644
--- a/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/Decoder.cs
+++ b/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/Decoder.cs
@@ -257,7 +257,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
(name, flags, AddressForm addressForm) = InstTable.GetInstNameAndFlags(encoding, cpuPreset.Version, cpuPreset.Features);
- if (name.IsPrivileged())
+ if (name.IsPrivileged() || (name == InstName.Sys && IsPrivilegedSys(encoding)))
{
name = InstName.UdfPermUndef;
flags = InstFlags.None;
@@ -274,7 +274,8 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
uint tempGprUseMask = gprUseMask | instGprReadMask | instGprWriteMask;
- if (CalculateAvailableTemps(tempGprUseMask) < CalculateRequiredGprTemps(tempGprUseMask) || totalInsts++ >= MaxInstructionsPerFunction)
+ if (CalculateAvailableTemps(tempGprUseMask) < CalculateRequiredGprTemps(memoryManager.Type, tempGprUseMask) ||
+ totalInsts++ >= MaxInstructionsPerFunction)
{
isTruncated = true;
address -= 4UL;
@@ -341,6 +342,11 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
return new(startAddress, address, insts, !isTruncated && !name.IsException(), isTruncated, isLoopEnd);
}
+ private static bool IsPrivilegedSys(uint encoding)
+ {
+ return !SysUtils.IsCacheInstEl0(encoding);
+ }
+
private static bool IsMrsNzcv(uint encoding)
{
return (encoding & ~0x1fu) == 0xd53b4200u;
@@ -373,9 +379,9 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
return false;
}
- private static int CalculateRequiredGprTemps(uint gprUseMask)
+ private static int CalculateRequiredGprTemps(MemoryManagerType mmType, uint gprUseMask)
{
- return BitOperations.PopCount(gprUseMask & RegisterUtils.ReservedRegsMask) + RegisterAllocator.MaxTempsInclFixed;
+ return BitOperations.PopCount(gprUseMask & RegisterUtils.ReservedRegsMask) + RegisterAllocator.CalculateMaxTempsInclFixed(mmType);
}
private static int CalculateAvailableTemps(uint gprUseMask)
diff --git a/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitMemory.cs b/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitMemory.cs
index ece1520fda..790a7de95b 100644
--- a/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitMemory.cs
+++ b/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitMemory.cs
@@ -13,6 +13,14 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
public static void RewriteSysInstruction(int asBits, MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, uint encoding)
{
+ // TODO: Handle IC instruction, it should invalidate the JIT cache.
+
+ if (InstEmitSystem.IsCacheInstForbidden(encoding))
+ {
+ // Current OS does not allow cache maintenance instructions from user mode, just do nothing.
+ return;
+ }
+
int rtIndex = RegisterUtils.ExtractRt(encoding);
if (rtIndex == RegisterUtils.ZrIndex)
{
@@ -47,6 +55,16 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
ulong pc,
uint encoding)
{
+ if (name.IsPrefetchMemory() && mmType == MemoryManagerType.HostTrackedUnsafe)
+ {
+ // Prefetch to invalid addresses do not cause faults, so for memory manager
+ // types where we need to access the page table before doing the prefetch,
+ // we should make sure we won't try to access an out of bounds page table region.
+ // To do this, we force the masked memory manager variant to be used.
+
+ mmType = MemoryManagerType.HostTracked;
+ }
+
switch (addressForm)
{
case AddressForm.OffsetReg:
@@ -503,18 +521,48 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
WriteAddressTranslation(asBits, mmType, regAlloc, ref asm, destination, guestAddress);
}
- private static void WriteAddressTranslation(int asBits, MemoryManagerType mmType, RegisterAllocator regAlloc, ref Assembler asm, Operand destination, ulong guestAddress)
+ private static void WriteAddressTranslation(
+ int asBits,
+ MemoryManagerType mmType,
+ RegisterAllocator regAlloc,
+ ref Assembler asm,
+ Operand destination,
+ ulong guestAddress)
{
asm.Mov(destination, guestAddress);
WriteAddressTranslation(asBits, mmType, regAlloc, ref asm, destination, destination);
}
- private static void WriteAddressTranslation(int asBits, MemoryManagerType mmType, RegisterAllocator regAlloc, ref Assembler asm, Operand destination, Operand guestAddress)
+ private static void WriteAddressTranslation(
+ int asBits,
+ MemoryManagerType mmType,
+ RegisterAllocator regAlloc,
+ ref Assembler asm,
+ Operand destination,
+ Operand guestAddress)
{
Operand basePointer = new(regAlloc.FixedPageTableRegister, RegisterType.Integer, OperandType.I64);
- if (mmType == MemoryManagerType.HostMapped || mmType == MemoryManagerType.HostMappedUnsafe)
+ if (mmType.IsHostTracked())
+ {
+ int tempRegister = regAlloc.AllocateTempGprRegister();
+
+ Operand pte = new(tempRegister, RegisterType.Integer, OperandType.I64);
+
+ asm.Lsr(pte, guestAddress, new Operand(OperandKind.Constant, OperandType.I32, 12));
+
+ if (mmType == MemoryManagerType.HostTracked)
+ {
+ asm.And(pte, pte, new Operand(OperandKind.Constant, OperandType.I64, ulong.MaxValue >> (64 - (asBits - 12))));
+ }
+
+ asm.LdrRr(pte, basePointer, pte, ArmExtensionType.Uxtx, true);
+ asm.Add(destination, pte, guestAddress);
+
+ regAlloc.FreeTempGprRegister(tempRegister);
+ }
+ else if (mmType.IsHostMapped())
{
if (mmType == MemoryManagerType.HostMapped)
{
diff --git a/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitSystem.cs b/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitSystem.cs
index 3d4204fc13..82cb29d731 100644
--- a/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitSystem.cs
+++ b/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitSystem.cs
@@ -69,7 +69,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
asm.LdrRiUn(Register((int)rd), Register(regAlloc.FixedContextRegister), NativeContextOffsets.TpidrEl0Offset);
}
}
- else if ((encoding & ~0x1f) == 0xd53b0020 && IsAppleOS()) // mrs x0, ctr_el0
+ else if ((encoding & ~0x1f) == 0xd53b0020 && IsCtrEl0AccessForbidden()) // mrs x0, ctr_el0
{
uint rd = encoding & 0x1f;
@@ -115,7 +115,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
{
return true;
}
- else if ((encoding & ~0x1f) == 0xd53b0020 && IsAppleOS()) // mrs x0, ctr_el0
+ else if ((encoding & ~0x1f) == 0xd53b0020 && IsCtrEl0AccessForbidden()) // mrs x0, ctr_el0
{
return true;
}
@@ -127,9 +127,16 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
return false;
}
- private static bool IsAppleOS()
+ private static bool IsCtrEl0AccessForbidden()
{
- return OperatingSystem.IsMacOS() || OperatingSystem.IsIOS();
+ // Only Linux allows accessing CTR_EL0 from user mode.
+ return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS();
+ }
+
+ public static bool IsCacheInstForbidden(uint encoding)
+ {
+ // Windows does not allow the cache maintenance instructions to be used from user mode.
+ return OperatingSystem.IsWindows() && SysUtils.IsCacheInstUciTrapped(encoding);
}
public static bool NeedsContextStoreLoad(InstName name)
diff --git a/src/Ryujinx.Cpu/LightningJit/Translator.cs b/src/Ryujinx.Cpu/LightningJit/Translator.cs
index c883c1d601..d624102534 100644
--- a/src/Ryujinx.Cpu/LightningJit/Translator.cs
+++ b/src/Ryujinx.Cpu/LightningJit/Translator.cs
@@ -68,9 +68,9 @@ namespace Ryujinx.Cpu.LightningJit
FunctionTable.Fill = (ulong)Stubs.SlowDispatchStub;
- if (memory.Type.IsHostMapped())
+ if (memory.Type.IsHostMappedOrTracked())
{
- NativeSignalHandler.InitializeSignalHandler(MemoryBlock.GetPageSize());
+ NativeSignalHandler.InitializeSignalHandler();
}
}
diff --git a/src/Ryujinx.Cpu/ManagedPageFlags.cs b/src/Ryujinx.Cpu/ManagedPageFlags.cs
new file mode 100644
index 0000000000..a839dae676
--- /dev/null
+++ b/src/Ryujinx.Cpu/ManagedPageFlags.cs
@@ -0,0 +1,389 @@
+using Ryujinx.Memory;
+using Ryujinx.Memory.Tracking;
+using System;
+using System.Runtime.CompilerServices;
+using System.Threading;
+
+namespace Ryujinx.Cpu
+{
+ ///
+ /// A page bitmap that keeps track of mapped state and tracking protection
+ /// for managed memory accesses (not using host page protection).
+ ///
+ internal readonly struct ManagedPageFlags
+ {
+ public const int PageBits = 12;
+ public const int PageSize = 1 << PageBits;
+ public const int PageMask = PageSize - 1;
+
+ private readonly ulong[] _pageBitmap;
+
+ public const int PageToPteShift = 5; // 32 pages (2 bits each) in one ulong page table entry.
+ public const ulong BlockMappedMask = 0x5555555555555555; // First bit of each table entry set.
+
+ private enum ManagedPtBits : ulong
+ {
+ Unmapped = 0,
+ Mapped,
+ WriteTracked,
+ ReadWriteTracked,
+
+ MappedReplicated = 0x5555555555555555,
+ WriteTrackedReplicated = 0xaaaaaaaaaaaaaaaa,
+ ReadWriteTrackedReplicated = ulong.MaxValue,
+ }
+
+ public ManagedPageFlags(int addressSpaceBits)
+ {
+ int bits = Math.Max(0, addressSpaceBits - (PageBits + PageToPteShift));
+ _pageBitmap = new ulong[1 << bits];
+ }
+
+ ///
+ /// Computes the number of pages in a virtual address range.
+ ///
+ /// Virtual address of the range
+ /// Size of the range
+ /// The virtual address of the beginning of the first page
+ /// This function does not differentiate between allocated and unallocated pages.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int GetPagesCount(ulong va, ulong size, out ulong startVa)
+ {
+ // WARNING: Always check if ulong does not overflow during the operations.
+ startVa = va & ~(ulong)PageMask;
+ ulong vaSpan = (va - startVa + size + PageMask) & ~(ulong)PageMask;
+
+ return (int)(vaSpan / PageSize);
+ }
+
+ ///
+ /// Checks if the page at a given CPU virtual address is mapped.
+ ///
+ /// Virtual address to check
+ /// True if the address is mapped, false otherwise
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public readonly bool IsMapped(ulong va)
+ {
+ ulong page = va >> PageBits;
+
+ int bit = (int)((page & 31) << 1);
+
+ int pageIndex = (int)(page >> PageToPteShift);
+ ref ulong pageRef = ref _pageBitmap[pageIndex];
+
+ ulong pte = Volatile.Read(ref pageRef);
+
+ return ((pte >> bit) & 3) != 0;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void GetPageBlockRange(ulong pageStart, ulong pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex)
+ {
+ startMask = ulong.MaxValue << ((int)(pageStart & 31) << 1);
+ endMask = ulong.MaxValue >> (64 - ((int)(pageEnd & 31) << 1));
+
+ pageIndex = (int)(pageStart >> PageToPteShift);
+ pageEndIndex = (int)((pageEnd - 1) >> PageToPteShift);
+ }
+
+ ///
+ /// Checks if a memory range is mapped.
+ ///
+ /// Virtual address of the range
+ /// Size of the range in bytes
+ /// True if the entire range is mapped, false otherwise
+ public readonly bool IsRangeMapped(ulong va, ulong size)
+ {
+ int pages = GetPagesCount(va, size, out _);
+
+ if (pages == 1)
+ {
+ return IsMapped(va);
+ }
+
+ ulong pageStart = va >> PageBits;
+ ulong pageEnd = pageStart + (ulong)pages;
+
+ GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
+
+ // Check if either bit in each 2 bit page entry is set.
+ // OR the block with itself shifted down by 1, and check the first bit of each entry.
+
+ ulong mask = BlockMappedMask & startMask;
+
+ while (pageIndex <= pageEndIndex)
+ {
+ if (pageIndex == pageEndIndex)
+ {
+ mask &= endMask;
+ }
+
+ ref ulong pageRef = ref _pageBitmap[pageIndex++];
+ ulong pte = Volatile.Read(ref pageRef);
+
+ pte |= pte >> 1;
+ if ((pte & mask) != mask)
+ {
+ return false;
+ }
+
+ mask = BlockMappedMask;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Reprotect a region of virtual memory for tracking.
+ ///
+ /// Virtual address base
+ /// Size of the region to protect
+ /// Memory protection to set
+ public readonly void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
+ {
+ // Protection is inverted on software pages, since the default value is 0.
+ protection = (~protection) & MemoryPermission.ReadAndWrite;
+
+ int pages = GetPagesCount(va, size, out va);
+ ulong pageStart = va >> PageBits;
+
+ if (pages == 1)
+ {
+ ulong protTag = protection switch
+ {
+ MemoryPermission.None => (ulong)ManagedPtBits.Mapped,
+ MemoryPermission.Write => (ulong)ManagedPtBits.WriteTracked,
+ _ => (ulong)ManagedPtBits.ReadWriteTracked,
+ };
+
+ int bit = (int)((pageStart & 31) << 1);
+
+ ulong tagMask = 3UL << bit;
+ ulong invTagMask = ~tagMask;
+
+ ulong tag = protTag << bit;
+
+ int pageIndex = (int)(pageStart >> PageToPteShift);
+ ref ulong pageRef = ref _pageBitmap[pageIndex];
+
+ ulong pte;
+
+ do
+ {
+ pte = Volatile.Read(ref pageRef);
+ }
+ while ((pte & tagMask) != 0 && Interlocked.CompareExchange(ref pageRef, (pte & invTagMask) | tag, pte) != pte);
+ }
+ else
+ {
+ ulong pageEnd = pageStart + (ulong)pages;
+
+ GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
+
+ ulong mask = startMask;
+
+ ulong protTag = protection switch
+ {
+ MemoryPermission.None => (ulong)ManagedPtBits.MappedReplicated,
+ MemoryPermission.Write => (ulong)ManagedPtBits.WriteTrackedReplicated,
+ _ => (ulong)ManagedPtBits.ReadWriteTrackedReplicated,
+ };
+
+ while (pageIndex <= pageEndIndex)
+ {
+ if (pageIndex == pageEndIndex)
+ {
+ mask &= endMask;
+ }
+
+ ref ulong pageRef = ref _pageBitmap[pageIndex++];
+
+ ulong pte;
+ ulong mappedMask;
+
+ // Change the protection of all 2 bit entries that are mapped.
+ do
+ {
+ pte = Volatile.Read(ref pageRef);
+
+ mappedMask = pte | (pte >> 1);
+ mappedMask |= (mappedMask & BlockMappedMask) << 1;
+ mappedMask &= mask; // Only update mapped pages within the given range.
+ }
+ while (Interlocked.CompareExchange(ref pageRef, (pte & (~mappedMask)) | (protTag & mappedMask), pte) != pte);
+
+ mask = ulong.MaxValue;
+ }
+ }
+ }
+
+ ///
+ /// Alerts the memory tracking that a given region has been read from or written to.
+ /// This should be called before read/write is performed.
+ ///
+ /// Memory tracking structure to call when pages are protected
+ /// Virtual address of the region
+ /// Size of the region
+ /// True if the region was written, false if read
+ /// Optional ID of the handles that should not be signalled
+ ///
+ /// This function also validates that the given range is both valid and mapped, and will throw if it is not.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public readonly void SignalMemoryTracking(MemoryTracking tracking, ulong va, ulong size, bool write, int? exemptId = null)
+ {
+ // Software table, used for managed memory tracking.
+
+ int pages = GetPagesCount(va, size, out _);
+ ulong pageStart = va >> PageBits;
+
+ if (pages == 1)
+ {
+ ulong tag = (ulong)(write ? ManagedPtBits.WriteTracked : ManagedPtBits.ReadWriteTracked);
+
+ int bit = (int)((pageStart & 31) << 1);
+
+ int pageIndex = (int)(pageStart >> PageToPteShift);
+ ref ulong pageRef = ref _pageBitmap[pageIndex];
+
+ ulong pte = Volatile.Read(ref pageRef);
+ ulong state = ((pte >> bit) & 3);
+
+ if (state >= tag)
+ {
+ tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId);
+ return;
+ }
+ else if (state == 0)
+ {
+ ThrowInvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}");
+ }
+ }
+ else
+ {
+ ulong pageEnd = pageStart + (ulong)pages;
+
+ GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
+
+ ulong mask = startMask;
+
+ ulong anyTrackingTag = (ulong)ManagedPtBits.WriteTrackedReplicated;
+
+ while (pageIndex <= pageEndIndex)
+ {
+ if (pageIndex == pageEndIndex)
+ {
+ mask &= endMask;
+ }
+
+ ref ulong pageRef = ref _pageBitmap[pageIndex++];
+
+ ulong pte = Volatile.Read(ref pageRef);
+ ulong mappedMask = mask & BlockMappedMask;
+
+ ulong mappedPte = pte | (pte >> 1);
+ if ((mappedPte & mappedMask) != mappedMask)
+ {
+ ThrowInvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}");
+ }
+
+ pte &= mask;
+ if ((pte & anyTrackingTag) != 0) // Search for any tracking.
+ {
+ // Writes trigger any tracking.
+ // Only trigger tracking from reads if both bits are set on any page.
+ if (write || (pte & (pte >> 1) & BlockMappedMask) != 0)
+ {
+ tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId);
+ break;
+ }
+ }
+
+ mask = ulong.MaxValue;
+ }
+ }
+ }
+
+ ///
+ /// Adds the given address mapping to the page table.
+ ///
+ /// Virtual memory address
+ /// Size to be mapped
+ public readonly void AddMapping(ulong va, ulong size)
+ {
+ int pages = GetPagesCount(va, size, out _);
+ ulong pageStart = va >> PageBits;
+ ulong pageEnd = pageStart + (ulong)pages;
+
+ GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
+
+ ulong mask = startMask;
+
+ while (pageIndex <= pageEndIndex)
+ {
+ if (pageIndex == pageEndIndex)
+ {
+ mask &= endMask;
+ }
+
+ ref ulong pageRef = ref _pageBitmap[pageIndex++];
+
+ ulong pte;
+ ulong mappedMask;
+
+ // Map all 2-bit entries that are unmapped.
+ do
+ {
+ pte = Volatile.Read(ref pageRef);
+
+ mappedMask = pte | (pte >> 1);
+ mappedMask |= (mappedMask & BlockMappedMask) << 1;
+ mappedMask |= ~mask; // Treat everything outside the range as mapped, thus unchanged.
+ }
+ while (Interlocked.CompareExchange(ref pageRef, (pte & mappedMask) | (BlockMappedMask & (~mappedMask)), pte) != pte);
+
+ mask = ulong.MaxValue;
+ }
+ }
+
+ ///
+ /// Removes the given address mapping from the page table.
+ ///
+ /// Virtual memory address
+ /// Size to be unmapped
+ public readonly void RemoveMapping(ulong va, ulong size)
+ {
+ int pages = GetPagesCount(va, size, out _);
+ ulong pageStart = va >> PageBits;
+ ulong pageEnd = pageStart + (ulong)pages;
+
+ GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
+
+ startMask = ~startMask;
+ endMask = ~endMask;
+
+ ulong mask = startMask;
+
+ while (pageIndex <= pageEndIndex)
+ {
+ if (pageIndex == pageEndIndex)
+ {
+ mask |= endMask;
+ }
+
+ ref ulong pageRef = ref _pageBitmap[pageIndex++];
+ ulong pte;
+
+ do
+ {
+ pte = Volatile.Read(ref pageRef);
+ }
+ while (Interlocked.CompareExchange(ref pageRef, pte & mask, pte) != pte);
+
+ mask = 0;
+ }
+ }
+
+ private static void ThrowInvalidMemoryRegionException(string message) => throw new InvalidMemoryRegionException(message);
+ }
+}
diff --git a/src/Ryujinx.Cpu/MemoryEhMeilleure.cs b/src/Ryujinx.Cpu/MemoryEhMeilleure.cs
index f3a5b056bc..379ace9413 100644
--- a/src/Ryujinx.Cpu/MemoryEhMeilleure.cs
+++ b/src/Ryujinx.Cpu/MemoryEhMeilleure.cs
@@ -1,3 +1,4 @@
+using Ryujinx.Common;
using Ryujinx.Cpu.Signal;
using Ryujinx.Memory;
using Ryujinx.Memory.Tracking;
@@ -8,19 +9,27 @@ namespace Ryujinx.Cpu
{
public class MemoryEhMeilleure : IDisposable
{
- private delegate bool TrackingEventDelegate(ulong address, ulong size, bool write);
+ public delegate ulong TrackingEventDelegate(ulong address, ulong size, bool write);
+ private readonly MemoryTracking _tracking;
private readonly TrackingEventDelegate _trackingEvent;
+ private readonly ulong _pageSize;
+
private readonly ulong _baseAddress;
private readonly ulong _mirrorAddress;
- public MemoryEhMeilleure(MemoryBlock addressSpace, MemoryBlock addressSpaceMirror, MemoryTracking tracking)
+ public MemoryEhMeilleure(MemoryBlock addressSpace, MemoryBlock addressSpaceMirror, MemoryTracking tracking, TrackingEventDelegate trackingEvent = null)
{
_baseAddress = (ulong)addressSpace.Pointer;
+
ulong endAddress = _baseAddress + addressSpace.Size;
- _trackingEvent = tracking.VirtualMemoryEvent;
+ _tracking = tracking;
+ _trackingEvent = trackingEvent ?? VirtualMemoryEvent;
+
+ _pageSize = MemoryBlock.GetPageSize();
+
bool added = NativeSignalHandler.AddTrackedRegion((nuint)_baseAddress, (nuint)endAddress, Marshal.GetFunctionPointerForDelegate(_trackingEvent));
if (!added)
@@ -28,7 +37,7 @@ namespace Ryujinx.Cpu
throw new InvalidOperationException("Number of allowed tracked regions exceeded.");
}
- if (OperatingSystem.IsWindows())
+ if (OperatingSystem.IsWindows() && addressSpaceMirror != null)
{
// Add a tracking event with no signal handler for the mirror on Windows.
// The native handler has its own code to check for the partial overlap race when regions are protected by accident,
@@ -46,6 +55,21 @@ namespace Ryujinx.Cpu
}
}
+ private ulong VirtualMemoryEvent(ulong address, ulong size, bool write)
+ {
+ ulong pageSize = _pageSize;
+ ulong addressAligned = BitUtils.AlignDown(address, pageSize);
+ ulong endAddressAligned = BitUtils.AlignUp(address + size, pageSize);
+ ulong sizeAligned = endAddressAligned - addressAligned;
+
+ if (_tracking.VirtualMemoryEvent(addressAligned, sizeAligned, write))
+ {
+ return _baseAddress + address;
+ }
+
+ return 0;
+ }
+
public void Dispose()
{
GC.SuppressFinalize(this);
diff --git a/src/Ryujinx.Cpu/PrivateMemoryAllocator.cs b/src/Ryujinx.Cpu/PrivateMemoryAllocator.cs
index ce8e834198..8db74f1e92 100644
--- a/src/Ryujinx.Cpu/PrivateMemoryAllocator.cs
+++ b/src/Ryujinx.Cpu/PrivateMemoryAllocator.cs
@@ -143,7 +143,7 @@ namespace Ryujinx.Cpu
}
}
- public PrivateMemoryAllocator(int blockAlignment, MemoryAllocationFlags allocationFlags) : base(blockAlignment, allocationFlags)
+ public PrivateMemoryAllocator(ulong blockAlignment, MemoryAllocationFlags allocationFlags) : base(blockAlignment, allocationFlags)
{
}
@@ -180,10 +180,10 @@ namespace Ryujinx.Cpu
private readonly List _blocks;
- private readonly int _blockAlignment;
+ private readonly ulong _blockAlignment;
private readonly MemoryAllocationFlags _allocationFlags;
- public PrivateMemoryAllocatorImpl(int blockAlignment, MemoryAllocationFlags allocationFlags)
+ public PrivateMemoryAllocatorImpl(ulong blockAlignment, MemoryAllocationFlags allocationFlags)
{
_blocks = new List();
_blockAlignment = blockAlignment;
@@ -212,7 +212,7 @@ namespace Ryujinx.Cpu
}
}
- ulong blockAlignedSize = BitUtils.AlignUp(size, (ulong)_blockAlignment);
+ ulong blockAlignedSize = BitUtils.AlignUp(size, _blockAlignment);
var memory = new MemoryBlock(blockAlignedSize, _allocationFlags);
var newBlock = createBlock(memory, blockAlignedSize);
diff --git a/src/Ryujinx.Cpu/Signal/NativeSignalHandler.cs b/src/Ryujinx.Cpu/Signal/NativeSignalHandler.cs
index 5a9d92cc4f..93e6083298 100644
--- a/src/Ryujinx.Cpu/Signal/NativeSignalHandler.cs
+++ b/src/Ryujinx.Cpu/Signal/NativeSignalHandler.cs
@@ -70,7 +70,7 @@ namespace Ryujinx.Cpu.Signal
config = new SignalHandlerConfig();
}
- public static void InitializeSignalHandler(ulong pageSize, Func customSignalHandlerFactory = null)
+ public static void InitializeSignalHandler(Func customSignalHandlerFactory = null)
{
if (_initialized)
{
@@ -90,7 +90,7 @@ namespace Ryujinx.Cpu.Signal
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
{
- _signalHandlerPtr = MapCode(NativeSignalHandlerGenerator.GenerateUnixSignalHandler(_handlerConfig, rangeStructSize, pageSize));
+ _signalHandlerPtr = MapCode(NativeSignalHandlerGenerator.GenerateUnixSignalHandler(_handlerConfig, rangeStructSize));
if (customSignalHandlerFactory != null)
{
@@ -107,7 +107,7 @@ namespace Ryujinx.Cpu.Signal
config.StructAddressOffset = 40; // ExceptionInformation1
config.StructWriteOffset = 32; // ExceptionInformation0
- _signalHandlerPtr = MapCode(NativeSignalHandlerGenerator.GenerateWindowsSignalHandler(_handlerConfig, rangeStructSize, pageSize));
+ _signalHandlerPtr = MapCode(NativeSignalHandlerGenerator.GenerateWindowsSignalHandler(_handlerConfig, rangeStructSize));
if (customSignalHandlerFactory != null)
{
@@ -175,5 +175,10 @@ namespace Ryujinx.Cpu.Signal
return false;
}
+
+ public static bool SupportsFaultAddressPatching()
+ {
+ return NativeSignalHandlerGenerator.SupportsFaultAddressPatchingForHost();
+ }
}
}
diff --git a/src/Ryujinx.Cpu/MemoryManagerBase.cs b/src/Ryujinx.Cpu/VirtualMemoryManagerRefCountedBase.cs
similarity index 86%
rename from src/Ryujinx.Cpu/MemoryManagerBase.cs
rename to src/Ryujinx.Cpu/VirtualMemoryManagerRefCountedBase.cs
index 3288e3a498..3c7b338055 100644
--- a/src/Ryujinx.Cpu/MemoryManagerBase.cs
+++ b/src/Ryujinx.Cpu/VirtualMemoryManagerRefCountedBase.cs
@@ -4,7 +4,7 @@ using System.Threading;
namespace Ryujinx.Cpu
{
- public abstract class MemoryManagerBase : IRefCounted
+ public abstract class VirtualMemoryManagerRefCountedBase : VirtualMemoryManagerBase, IRefCounted
{
private int _referenceCount;
diff --git a/src/Ryujinx.Graphics.Device/DeviceMemoryManager.cs b/src/Ryujinx.Graphics.Device/DeviceMemoryManager.cs
index d64ed30954..fc075a2643 100644
--- a/src/Ryujinx.Graphics.Device/DeviceMemoryManager.cs
+++ b/src/Ryujinx.Graphics.Device/DeviceMemoryManager.cs
@@ -1,5 +1,7 @@
+using Ryujinx.Common.Memory;
using Ryujinx.Memory;
using System;
+using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -143,11 +145,11 @@ namespace Ryujinx.Graphics.Device
}
else
{
- Memory memory = new byte[size];
+ IMemoryOwner memoryOwner = ByteMemoryPool.Rent(size);
- GetSpan(va, size).CopyTo(memory.Span);
+ GetSpan(va, size).CopyTo(memoryOwner.Memory.Span);
- return new WritableRegion(this, va, memory, tracked: true);
+ return new WritableRegion(this, va, memoryOwner, tracked: true);
}
}
diff --git a/src/Ryujinx.Graphics.GAL/Capabilities.cs b/src/Ryujinx.Graphics.GAL/Capabilities.cs
index dc927eaba1..779ce5b5dc 100644
--- a/src/Ryujinx.Graphics.GAL/Capabilities.cs
+++ b/src/Ryujinx.Graphics.GAL/Capabilities.cs
@@ -36,6 +36,8 @@ namespace Ryujinx.Graphics.GAL
public readonly bool SupportsMismatchingViewFormat;
public readonly bool SupportsCubemapView;
public readonly bool SupportsNonConstantTextureOffset;
+ public readonly bool SupportsQuads;
+ public readonly bool SupportsSeparateSampler;
public readonly bool SupportsShaderBallot;
public readonly bool SupportsShaderBarrierDivergence;
public readonly bool SupportsShaderFloat64;
@@ -92,6 +94,8 @@ namespace Ryujinx.Graphics.GAL
bool supportsMismatchingViewFormat,
bool supportsCubemapView,
bool supportsNonConstantTextureOffset,
+ bool supportsQuads,
+ bool supportsSeparateSampler,
bool supportsShaderBallot,
bool supportsShaderBarrierDivergence,
bool supportsShaderFloat64,
@@ -144,6 +148,8 @@ namespace Ryujinx.Graphics.GAL
SupportsMismatchingViewFormat = supportsMismatchingViewFormat;
SupportsCubemapView = supportsCubemapView;
SupportsNonConstantTextureOffset = supportsNonConstantTextureOffset;
+ SupportsQuads = supportsQuads;
+ SupportsSeparateSampler = supportsSeparateSampler;
SupportsShaderBallot = supportsShaderBallot;
SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence;
SupportsShaderFloat64 = supportsShaderFloat64;
diff --git a/src/Ryujinx.Graphics.GAL/IImageArray.cs b/src/Ryujinx.Graphics.GAL/IImageArray.cs
new file mode 100644
index 0000000000..30cff50b15
--- /dev/null
+++ b/src/Ryujinx.Graphics.GAL/IImageArray.cs
@@ -0,0 +1,8 @@
+namespace Ryujinx.Graphics.GAL
+{
+ public interface IImageArray
+ {
+ void SetFormats(int index, Format[] imageFormats);
+ void SetImages(int index, ITexture[] images);
+ }
+}
diff --git a/src/Ryujinx.Graphics.GAL/IPipeline.cs b/src/Ryujinx.Graphics.GAL/IPipeline.cs
index 3ba084aa5b..9efb9e3e8f 100644
--- a/src/Ryujinx.Graphics.GAL/IPipeline.cs
+++ b/src/Ryujinx.Graphics.GAL/IPipeline.cs
@@ -59,6 +59,7 @@ namespace Ryujinx.Graphics.GAL
void SetIndexBuffer(BufferRange buffer, IndexType type);
void SetImage(ShaderStage stage, int binding, ITexture texture, Format imageFormat);
+ void SetImageArray(ShaderStage stage, int binding, IImageArray array);
void SetLineParameters(float width, bool smooth);
@@ -89,6 +90,7 @@ namespace Ryujinx.Graphics.GAL
void SetStorageBuffers(ReadOnlySpan buffers);
void SetTextureAndSampler(ShaderStage stage, int binding, ITexture texture, ISampler sampler);
+ void SetTextureArray(ShaderStage stage, int binding, ITextureArray array);
void SetTransformFeedbackBuffers(ReadOnlySpan buffers);
void SetUniformBuffers(ReadOnlySpan buffers);
diff --git a/src/Ryujinx.Graphics.GAL/IRenderer.cs b/src/Ryujinx.Graphics.GAL/IRenderer.cs
index 3bf56465eb..a3466e3966 100644
--- a/src/Ryujinx.Graphics.GAL/IRenderer.cs
+++ b/src/Ryujinx.Graphics.GAL/IRenderer.cs
@@ -21,10 +21,14 @@ namespace Ryujinx.Graphics.GAL
BufferHandle CreateBuffer(nint pointer, int size);
BufferHandle CreateBufferSparse(ReadOnlySpan storageBuffers);
+ IImageArray CreateImageArray(int size, bool isBuffer);
+
IProgram CreateProgram(ShaderSource[] shaders, ShaderInfo info);
ISampler CreateSampler(SamplerCreateInfo info);
ITexture CreateTexture(TextureCreateInfo info);
+ ITextureArray CreateTextureArray(int size, bool isBuffer);
+
bool PrepareHostMapping(nint address, ulong size);
void CreateSync(ulong id, bool strict);
diff --git a/src/Ryujinx.Graphics.GAL/ITexture.cs b/src/Ryujinx.Graphics.GAL/ITexture.cs
index 5a4623a66d..2d9c6b7990 100644
--- a/src/Ryujinx.Graphics.GAL/ITexture.cs
+++ b/src/Ryujinx.Graphics.GAL/ITexture.cs
@@ -1,4 +1,4 @@
-using Ryujinx.Common.Memory;
+using System.Buffers;
namespace Ryujinx.Graphics.GAL
{
@@ -17,10 +17,34 @@ namespace Ryujinx.Graphics.GAL
PinnedSpan GetData();
PinnedSpan GetData(int layer, int level);
- void SetData(SpanOrArray data);
- void SetData(SpanOrArray data, int layer, int level);
- void SetData(SpanOrArray data, int layer, int level, Rectangle region);
+ ///
+ /// Sets the texture data. The data passed as a will be disposed when
+ /// the operation completes.
+ ///
+ /// Texture data bytes
+ void SetData(IMemoryOwner data);
+
+ ///
+ /// Sets the texture data. The data passed as a will be disposed when
+ /// the operation completes.
+ ///
+ /// Texture data bytes
+ /// Target layer
+ /// Target level
+ void SetData(IMemoryOwner data, int layer, int level);
+
+ ///
+ /// Sets the texture data. The data passed as a will be disposed when
+ /// the operation completes.
+ ///
+ /// Texture data bytes
+ /// Target layer
+ /// Target level
+ /// Target sub-region of the texture to update
+ void SetData(IMemoryOwner data, int layer, int level, Rectangle region);
+
void SetStorage(BufferRange buffer);
+
void Release();
}
}
diff --git a/src/Ryujinx.Graphics.GAL/ITextureArray.cs b/src/Ryujinx.Graphics.GAL/ITextureArray.cs
new file mode 100644
index 0000000000..35c2116b54
--- /dev/null
+++ b/src/Ryujinx.Graphics.GAL/ITextureArray.cs
@@ -0,0 +1,8 @@
+namespace Ryujinx.Graphics.GAL
+{
+ public interface ITextureArray
+ {
+ void SetSamplers(int index, ISampler[] samplers);
+ void SetTextures(int index, ITexture[] textures);
+ }
+}
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs b/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
index 5bf3d32835..fd2919be4d 100644
--- a/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
@@ -1,10 +1,12 @@
using Ryujinx.Graphics.GAL.Multithreading.Commands;
using Ryujinx.Graphics.GAL.Multithreading.Commands.Buffer;
using Ryujinx.Graphics.GAL.Multithreading.Commands.CounterEvent;
+using Ryujinx.Graphics.GAL.Multithreading.Commands.ImageArray;
using Ryujinx.Graphics.GAL.Multithreading.Commands.Program;
using Ryujinx.Graphics.GAL.Multithreading.Commands.Renderer;
using Ryujinx.Graphics.GAL.Multithreading.Commands.Sampler;
using Ryujinx.Graphics.GAL.Multithreading.Commands.Texture;
+using Ryujinx.Graphics.GAL.Multithreading.Commands.TextureArray;
using Ryujinx.Graphics.GAL.Multithreading.Commands.Window;
using System;
using System.Linq;
@@ -46,10 +48,12 @@ namespace Ryujinx.Graphics.GAL.Multithreading
Register(CommandType.CreateBufferAccess);
Register(CommandType.CreateBufferSparse);
Register(CommandType.CreateHostBuffer);
+ Register(CommandType.CreateImageArray);
Register(CommandType.CreateProgram);
Register(CommandType.CreateSampler);
Register(CommandType.CreateSync);
Register(CommandType.CreateTexture);
+ Register(CommandType.CreateTextureArray);
Register(CommandType.GetCapabilities);
Register(CommandType.PreFrame);
Register(CommandType.ReportCounter);
@@ -63,6 +67,9 @@ namespace Ryujinx.Graphics.GAL.Multithreading
Register(CommandType.CounterEventDispose);
Register(CommandType.CounterEventFlush);
+ Register(CommandType.ImageArraySetFormats);
+ Register(CommandType.ImageArraySetImages);
+
Register(CommandType.ProgramDispose);
Register(CommandType.ProgramGetBinary);
Register(CommandType.ProgramCheckLink);
@@ -82,6 +89,9 @@ namespace Ryujinx.Graphics.GAL.Multithreading
Register(CommandType.TextureSetDataSliceRegion);
Register