Merge branch 'master' into 20-empty-nca-lockup

This commit is contained in:
Shyanne 2025-12-28 14:16:39 -05:00
commit 6938265651
88 changed files with 1369 additions and 1311 deletions

View file

@ -1,4 +1,4 @@
name: Canary release job name: Canary CI
on: on:
workflow_dispatch: workflow_dispatch:
@ -19,7 +19,6 @@ concurrency: release
env: env:
POWERSHELL_TELEMETRY_OPTOUT: 1 POWERSHELL_TELEMETRY_OPTOUT: 1
DOTNET_CLI_TELEMETRY_OPTOUT: 1 DOTNET_CLI_TELEMETRY_OPTOUT: 1
RYUJINX_BASE_VERSION: "1.3"
RYUJINX_TARGET_RELEASE_CHANNEL_NAME: "canary" RYUJINX_TARGET_RELEASE_CHANNEL_NAME: "canary"
RELEASE: 1 RELEASE: 1
@ -30,8 +29,8 @@ jobs:
strategy: strategy:
matrix: matrix:
platform: platform:
- { name: win-x64, os: windows-latest, zip_os_name: win_x64 } - { name: win-x64, os: ubuntu-latest, zip_os_name: win_x64 }
#- { name: win-arm64, os: windows-latest, zip_os_name: win_arm64 } #- { name: win-arm64, os: ubuntu-latest, zip_os_name: win_arm64 }
- { name: linux-x64, os: ubuntu-latest, zip_os_name: linux_x64 } - { name: linux-x64, os: ubuntu-latest, zip_os_name: linux_x64 }
- { name: linux-arm64, os: ubuntu-latest, zip_os_name: linux_arm64 } - { name: linux-arm64, os: ubuntu-latest, zip_os_name: linux_arm64 }
steps: steps:
@ -44,11 +43,25 @@ jobs:
- name: Overwrite csc problem matcher - name: Overwrite csc problem matcher
run: echo "::add-matcher::.github/csc.json" run: echo "::add-matcher::.github/csc.json"
- name: Install 7zip
run: |
sudo apt install -y 7zip
- name: Install gli
run: |
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get version info - name: Get version info
id: version_info id: version_info
run: | run: |
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT echo "build_version=$(gli get-next-version -c Canary -R)" >> $GITHUB_OUTPUT
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT echo "prev_build_version=$(gli get-current-version -c Canary -R)" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
shell: bash shell: bash
@ -69,33 +82,20 @@ jobs:
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./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 dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./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
- name: Packing Windows builds - name: Packing Windows builds
if: matrix.platform.os == 'windows-latest' if: contains(matrix.platform.name, 'win')
run: | run: |
pushd publish pushd publish
rm libarmeilleure-jitsupport.dylib rm libarmeilleure-jitsupport.dylib
7z a ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish 7z a ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish
popd popd
gh release download -R GreemDev/GLI -O gli.exe -p 'GitLabCli-win_x64.exe' gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip
./gli.exe --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/canary --command=UploadGenericPackage "Ryubing-Canary|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip"
shell: bash shell: bash
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install GitLabCli
if: matrix.platform.os == 'ubuntu-latest'
run: |
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64'
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Packing Linux builds - name: Packing Linux builds
if: matrix.platform.os == 'ubuntu-latest' if: contains(matrix.platform.name, 'linux')
run: | run: |
pushd publish pushd publish
rm libarmeilleure-jitsupport.dylib rm libarmeilleure-jitsupport.dylib
@ -103,11 +103,11 @@ jobs:
tar -czvf ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish tar -czvf ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish
popd popd
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/canary --command=UploadGenericPackage "Ryubing-Canary|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz" gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz
shell: bash shell: bash
- name: Build AppImage (Linux) - name: Build AppImage (Linux)
if: matrix.platform.os == 'ubuntu-latest' if: contains(matrix.platform.name, 'linux')
run: | run: |
BUILD_VERSION="${{ steps.version_info.outputs.build_version }}" BUILD_VERSION="${{ steps.version_info.outputs.build_version }}"
PLATFORM_NAME="${{ matrix.platform.name }}" PLATFORM_NAME="${{ matrix.platform.name }}"
@ -139,8 +139,8 @@ jobs:
pushd publish_appimage pushd publish_appimage
mv Ryujinx.AppImage ../release_output/ryujinx-canary-$BUILD_VERSION-$ARCH_NAME.AppImage mv Ryujinx.AppImage ../release_output/ryujinx-canary-$BUILD_VERSION-$ARCH_NAME.AppImage
popd popd
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/canary --command=UploadGenericPackage "Ryubing-Canary|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-canary-$BUILD_VERSION-$ARCH_NAME.AppImage" gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-canary-$BUILD_VERSION-$ARCH_NAME.AppImage
shell: bash shell: bash
macos_release: macos_release:
@ -159,10 +159,10 @@ jobs:
chmod +x llvm.sh chmod +x llvm.sh
sudo ./llvm.sh 17 sudo ./llvm.sh 17
- name: Install GitLabCli - name: Install gli
run: | run: |
mkdir -p $HOME/.bin mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64' gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
chmod +x gli chmod +x gli
mv gli $HOME/.bin/ mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH echo "$HOME/.bin" >> $GITHUB_PATH
@ -183,9 +183,10 @@ jobs:
- name: Get version info - name: Get version info
id: version_info id: version_info
run: | run: |
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT echo "build_version=$(gli get-next-version -c Canary -R)" >> $GITHUB_OUTPUT
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT echo "prev_build_version=$(gli get-current-version -c Canary -R)" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
shell: bash
- name: Configure for release - name: Configure for release
run: | run: |
@ -200,7 +201,7 @@ jobs:
- name: Publish macOS Ryujinx - name: Publish macOS Ryujinx
run: | 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 1 ./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 1
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/canary --command=UploadGenericPackage "Ryubing-Canary|${{ steps.version_info.outputs.build_version }}|publish_ava/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz" gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -r 5 -p publish_ava/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz
create_gitlab_release: create_gitlab_release:
name: Create GitLab Release name: Create GitLab Release
@ -210,37 +211,41 @@ jobs:
- release - release
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Get version info - name: Install gli
id: version_info
run: |
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
shell: bash
- name: Install GitLabCli
run: | run: |
mkdir -p $HOME/.bin mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64' gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
chmod +x gli chmod +x gli
mv gli $HOME/.bin/ mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH echo "$HOME/.bin" >> $GITHUB_PATH
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get version info
id: version_info
run: |
echo "build_version=$(gli get-next-version -c Canary -R)" >> $GITHUB_OUTPUT
echo "prev_build_version=$(gli get-current-version -c Canary -R)" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
shell: bash
- name: Create tag - name: Create tag
run: | run: |
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=CreateTag "Canary-${{ steps.version_info.outputs.build_version }}|${{ steps.version_info.outputs.git_short_hash }}" gli create-tag -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Canary-${{ steps.version_info.outputs.build_version }} -r ${{ steps.version_info.outputs.git_short_hash }}
- name: Create release - name: Create release
run: | run: |
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/canary --command=CreateReleaseFromGenericPackageFiles "Ryubing-Canary|${{ steps.version_info.outputs.build_version }}|main|Canary ${{ steps.version_info.outputs.build_version }}|**Full Changelog:** [${{ steps.version_info.outputs.prev_build_version }}...${{ steps.version_info.outputs.build_version }}](https://git.ryujinx.app/ryubing/ryujinx/-/compare/Canary-${{ steps.version_info.outputs.prev_build_version }}...Canary-${{ steps.version_info.outputs.build_version }})" gli create-release-from-generic-package-files -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -r main -t "Canary ${{ steps.version_info.outputs.build_version }}" -b "**Full Changelog:** [${{ steps.version_info.outputs.prev_build_version }}...${{ steps.version_info.outputs.build_version }}](https://git.ryujinx.app/ryubing/ryujinx/-/compare/Canary-${{ steps.version_info.outputs.prev_build_version }}...Canary-${{ steps.version_info.outputs.build_version }})"
- name: Send notification webhook - name: Send notification webhook
run: | run: |
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/canary --command=SendUpdateMessage "${{ steps.version_info.outputs.build_version }}|FF4500|${{ secrets.CANARY_DISCORD_WEBHOOK }}|https://avatars.githubusercontent.com/u/192939710?s=200&v=4|false" gli send-update-message -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -t ${{ steps.version_info.outputs.build_version }} -c FF4500 -w ${{ secrets.CANARY_DISCORD_WEBHOOK }} -i https://avatars.githubusercontent.com/u/192939710?s=200&v=4
- name: Notify update server of new builds - name: Notify update server of new builds
run: | run: |
curl 'https://update.ryujinx.app/api/v1/admin/refresh_cache?rc=canary' -X PATCH -H 'accept: */*' -H 'Authorization: ${{ secrets.UPDATE_SERVER_ADMIN_TOKEN }}' gli refresh-version-cache -T ${{ secrets.UPDATE_SERVER_ADMIN_TOKEN }} -c Canary
- name: Advance to the next version
run: |
gli increment-version -T ${{ secrets.UPDATE_SERVER_ADMIN_TOKEN }} -c Canary

View file

@ -1,224 +0,0 @@
name: Release job (Debug)
on:
workflow_dispatch:
inputs: {}
concurrency: release
env:
POWERSHELL_TELEMETRY_OPTOUT: 1
DOTNET_CLI_TELEMETRY_OPTOUT: 1
RYUJINX_BASE_VERSION: "1.3"
RYUJINX_TARGET_RELEASE_CHANNEL_NAME: "release"
RELEASE: 1
jobs:
release:
name: Release for ${{ matrix.platform.name }}
runs-on: ${{ matrix.platform.os }}
strategy:
matrix:
platform:
- { name: win-x64, os: windows-latest, zip_os_name: win_x64 }
#- { name: win-arm64, os: windows-latest, zip_os_name: win_arm64 }
- { name: linux-x64, os: ubuntu-latest, zip_os_name: linux_x64 }
- { name: linux-arm64, os: ubuntu-latest, zip_os_name: linux_arm64 }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Overwrite csc problem matcher
run: echo "::add-matcher::.github/csc.json"
- name: Get version info
id: version_info
run: |
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} + 10))" >> $GITHUB_OUTPUT
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
shell: bash
- name: Configure for release
run: |
sed -r --in-place 's/\%\%RYUJINX_BUILD_VERSION\%\%/${{ steps.version_info.outputs.build_version }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_BUILD_GIT_HASH\%\%/${{ steps.version_info.outputs.git_short_hash }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
shell: bash
- name: Create output dir
run: "mkdir release_output"
- name: Publish
run: |
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./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
- name: Packing Windows builds
if: matrix.platform.os == 'windows-latest'
run: |
pushd publish
rm libarmeilleure-jitsupport.dylib
7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish
popd
gh release download -R GreemDev/GLI -O gli.exe -p 'GitLabCli-win_x64.exe'
./gli.exe --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip"
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install GitLabCli
if: matrix.platform.os == 'ubuntu-latest'
run: |
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64'
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Packing Linux builds
if: matrix.platform.os == 'ubuntu-latest'
run: |
pushd publish
rm libarmeilleure-jitsupport.dylib
chmod +x Ryujinx.sh Ryujinx
tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish
popd
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz"
shell: bash
- name: Build AppImage (Linux)
if: matrix.platform.os == 'ubuntu-latest'
run: |
BUILD_VERSION="${{ steps.version_info.outputs.build_version }}"
PLATFORM_NAME="${{ matrix.platform.name }}"
sudo apt install -y zsync desktop-file-utils appstream
mkdir -p tools
export PATH="$PATH:$(readlink -f tools)"
# Setup appimagetool
wget -q -O tools/appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x tools/appimagetool
chmod +x distribution/linux/appimage/build-appimage.sh
# Explicitly set $ARCH for appimagetool ($ARCH_NAME is for the file name)
if [ "$PLATFORM_NAME" = "linux-x64" ]; then
ARCH_NAME=x64
export ARCH=x86_64
elif [ "$PLATFORM_NAME" = "linux-arm64" ]; then
ARCH_NAME=arm64
export ARCH=aarch64
else
echo "Unexpected PLATFORM_NAME "$PLATFORM_NAME""
exit 1
fi
export UFLAG="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|*-$ARCH_NAME.AppImage.zsync"
BUILDDIR=publish OUTDIR=publish_appimage distribution/linux/appimage/build-appimage.sh
pushd publish_appimage
mv Ryujinx.AppImage ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage
mv Ryujinx.AppImage.zsync ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage.zsync
popd
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage"
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage.zsync"
shell: bash
macos_release:
name: Release MacOS universal
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Setup LLVM 17
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 17
- name: Install GitLabCli
run: |
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64'
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install rcodesign
run: |
mkdir -p $HOME/.bin
gh release download -R indygreg/apple-platform-rs -O apple-codesign.tar.gz -p 'apple-codesign-*-x86_64-unknown-linux-musl.tar.gz'
tar -xzvf apple-codesign.tar.gz --wildcards '*/rcodesign' --strip-components=1
rm apple-codesign.tar.gz
mv rcodesign $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get version info
id: version_info
run: |
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} + 10))" >> $GITHUB_OUTPUT
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
- name: Configure for release
run: |
sed -r --in-place 's/\%\%RYUJINX_BUILD_VERSION\%\%/${{ steps.version_info.outputs.build_version }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_BUILD_GIT_HASH\%\%/${{ steps.version_info.outputs.git_short_hash }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
shell: bash
- name: Publish macOS Ryujinx
run: |
./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 0
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|publish/ryujinx-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz"
create_gitlab_release:
name: Create GitLab Release
runs-on: ubuntu-24.04
needs:
- macos_release
- release
steps:
- uses: actions/checkout@v4
- name: Get version info
id: version_info
run: |
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} + 10))" >> $GITHUB_OUTPUT
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
shell: bash
- name: Install GitLabCli
run: |
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64'
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create release
run: |
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=CreateReleaseFromGenericPackageFiles "Ryubing|${{ steps.version_info.outputs.build_version }}|${{ steps.version_info.outputs.git_short_hash }}|test|THIS IS NOT INTENDED FOR END USER USAGE"

View file

@ -1,15 +1,18 @@
name: Release job name: Stable CI
on: on:
workflow_dispatch: workflow_dispatch:
inputs: {} inputs:
is_bugfix_release:
description: "Bug fix release: If checked, this will increment the third number for only Stable, and leave the Major version alone for both Stable and Canary."
required: true
type: boolean
concurrency: release concurrency: release
env: env:
POWERSHELL_TELEMETRY_OPTOUT: 1 POWERSHELL_TELEMETRY_OPTOUT: 1
DOTNET_CLI_TELEMETRY_OPTOUT: 1 DOTNET_CLI_TELEMETRY_OPTOUT: 1
RYUJINX_BASE_VERSION: "1.3"
RYUJINX_TARGET_RELEASE_CHANNEL_NAME: "release" RYUJINX_TARGET_RELEASE_CHANNEL_NAME: "release"
RELEASE: 1 RELEASE: 1
@ -20,8 +23,8 @@ jobs:
strategy: strategy:
matrix: matrix:
platform: platform:
- { name: win-x64, os: windows-latest, zip_os_name: win_x64 } - { name: win-x64, os: ubuntu-latest, zip_os_name: win_x64 }
#- { name: win-arm64, os: windows-latest, zip_os_name: win_arm64 } #- { name: win-arm64, os: ubuntu-latest, zip_os_name: win_arm64 }
- { name: linux-x64, os: ubuntu-latest, zip_os_name: linux_x64 } - { name: linux-x64, os: ubuntu-latest, zip_os_name: linux_x64 }
- { name: linux-arm64, os: ubuntu-latest, zip_os_name: linux_arm64 } - { name: linux-arm64, os: ubuntu-latest, zip_os_name: linux_arm64 }
steps: steps:
@ -33,12 +36,30 @@ jobs:
- name: Overwrite csc problem matcher - name: Overwrite csc problem matcher
run: echo "::add-matcher::.github/csc.json" run: echo "::add-matcher::.github/csc.json"
- name: Install 7zip
run: |
sudo apt install -y 7zip
- name: Install gli
run: |
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get version info - name: Get version info
id: version_info id: version_info
run: | run: |
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT if [ '${{ inputs.is_bugfix_release }}' == 'false' ]; then
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT echo "build_version=$(gli get-next-version -m -c Stable -R)" >> $GITHUB_OUTPUT
else
echo "build_version=$(gli get-next-version -c Stable -R)" >> $GITHUB_OUTPUT
fi
echo "prev_build_version=$(gli get-current-version -c Stable -R)" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
shell: bash shell: bash
@ -58,47 +79,34 @@ jobs:
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./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 dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./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
- name: Packing Windows builds - name: Packing Windows builds
if: matrix.platform.os == 'windows-latest' if: contains(matrix.platform.name, 'win')
run: | run: |
pushd publish pushd publish
rm libarmeilleure-jitsupport.dylib rm libarmeilleure-jitsupport.dylib
7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish 7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish
popd popd
gh release download -R GreemDev/GLI -O gli.exe -p 'GitLabCli-win_x64.exe' gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip
./gli.exe --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip"
shell: bash shell: bash
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install GitLabCli
if: matrix.platform.os == 'ubuntu-latest'
run: |
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64'
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Packing Linux builds - name: Packing Linux builds
if: matrix.platform.os == 'ubuntu-latest' if: contains(matrix.platform.name, 'linux')
run: | run: |
pushd publish pushd publish
rm libarmeilleure-jitsupport.dylib rm libarmeilleure-jitsupport.dylib
chmod +x Ryujinx.sh Ryujinx chmod +x Ryujinx.sh Ryujinx
tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish
popd popd
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz" gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz
shell: bash shell: bash
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build AppImage (Linux) - name: Build AppImage (Linux)
if: matrix.platform.os == 'ubuntu-latest' if: contains(matrix.platform.name, 'linux')
run: | run: |
BUILD_VERSION="${{ steps.version_info.outputs.build_version }}" BUILD_VERSION="${{ steps.version_info.outputs.build_version }}"
PLATFORM_NAME="${{ matrix.platform.name }}" PLATFORM_NAME="${{ matrix.platform.name }}"
@ -131,7 +139,7 @@ jobs:
mv Ryujinx.AppImage ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage mv Ryujinx.AppImage ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage
popd popd
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage" gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage
shell: bash shell: bash
macos_release: macos_release:
@ -150,10 +158,10 @@ jobs:
chmod +x llvm.sh chmod +x llvm.sh
sudo ./llvm.sh 17 sudo ./llvm.sh 17
- name: Install GitLabCli - name: Install gli
run: | run: |
mkdir -p $HOME/.bin mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64' gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
chmod +x gli chmod +x gli
mv gli $HOME/.bin/ mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH echo "$HOME/.bin" >> $GITHUB_PATH
@ -174,9 +182,14 @@ jobs:
- name: Get version info - name: Get version info
id: version_info id: version_info
run: | run: |
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT if [ '${{ inputs.is_bugfix_release }}' == 'false' ]; then
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT echo "build_version=$(gli get-next-version -m -c Stable -R)" >> $GITHUB_OUTPUT
else
echo "build_version=$(gli get-next-version -c Stable -R)" >> $GITHUB_OUTPUT
fi
echo "prev_build_version=$(gli get-current-version -c Stable -R)" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
shell: bash
- name: Configure for release - name: Configure for release
run: | run: |
@ -189,7 +202,8 @@ jobs:
- name: Publish macOS Ryujinx - name: Publish macOS Ryujinx
run: | run: |
./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 0 ./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 0
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|publish/ryujinx-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz"
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -r 5 -p publish/ryujinx-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz
create_gitlab_release: create_gitlab_release:
name: Create GitLab Release name: Create GitLab Release
@ -200,32 +214,45 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Get version info - name: Install gli
id: version_info
run: |
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
shell: bash
- name: Install GitLabCli
run: | run: |
mkdir -p $HOME/.bin mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64' gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
chmod +x gli chmod +x gli
mv gli $HOME/.bin/ mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH echo "$HOME/.bin" >> $GITHUB_PATH
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get version info
id: version_info
run: |
if [ '${{ inputs.is_bugfix_release }}' == 'false' ]; then
echo "build_version=$(gli get-next-version -m -c Stable -R)" >> $GITHUB_OUTPUT
else
echo "build_version=$(gli get-next-version -c Stable -R)" >> $GITHUB_OUTPUT
fi
echo "prev_build_version=$(gli get-current-version -c Stable -R)" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
echo "commit_message=$(git log -1 --pretty=%B)" >> $GITHUB_OUTPUT
shell: bash
- name: Create release - name: Create release
run: | run: |
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=CreateReleaseFromGenericPackageFiles "Ryubing|${{ steps.version_info.outputs.build_version }}|${{ steps.version_info.outputs.git_short_hash }}|${{ steps.version_info.outputs.build_version }}|msd:${{ steps.version_info.outputs.build_version }}" gli create-release-from-generic-package-files -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -r ${{ steps.version_info.outputs.git_short_hash }} -t "${{ steps.version_info.outputs.build_version }}" -b "msd:${{ steps.version_info.outputs.build_version }}"
- name: Send notification webhook - name: Send notification webhook
run: | run: |
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=SendUpdateMessage "${{ steps.version_info.outputs.build_version }}|32cd32|${{ secrets.STABLE_DISCORD_WEBHOOK }}|https://avatars.githubusercontent.com/u/192939710?s=200&v=4|false" gli send-update-message -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -t ${{ steps.version_info.outputs.build_version }} -c 32cd32 -w ${{ secrets.STABLE_DISCORD_WEBHOOK }} -i https://avatars.githubusercontent.com/u/192939710?s=200&v=4
- name: Notify update server of new builds - name: Notify update server of new builds
run: | run: |
curl 'https://update.ryujinx.app/api/v1/admin/refresh_cache?rc=stable' -X PATCH -H 'accept: */*' -H 'Authorization: ${{ secrets.UPDATE_SERVER_ADMIN_TOKEN }}' gli refresh-version-cache -T ${{ secrets.UPDATE_SERVER_ADMIN_TOKEN }} -c Stable
- name: Advance to the next version
run: |
if [ '${{ inputs.is_bugfix_release }}' == 'false' ]; then
gli advance-version -T ${{ secrets.UPDATE_SERVER_ADMIN_TOKEN }}
else
gli increment-version -T ${{ secrets.UPDATE_SERVER_ADMIN_TOKEN }} -c Stable
fi

1
.gitignore vendored
View file

@ -100,6 +100,7 @@ DocProject/Help/html
# Click-Once directory # Click-Once directory
publish/ publish/
RyubingMaintainerTools/
# Publish Web Output # Publish Web Output
*.Publish.xml *.Publish.xml

24
assets/Languages.json Normal file
View file

@ -0,0 +1,24 @@
{
"Languages": {
"ar_SA": "اَلْعَرَبِيَّةُ",
"de_DE": "Deutsch",
"el_GR": "Ελληνικά",
"en_US": "English (US)",
"es_ES": "Español (ES)",
"fr_FR": "Français (FR)",
"he_IL": "עִברִית",
"it_IT": "Italiano",
"ja_JP": "日本語",
"ko_KR": "한국어",
"no_NO": "Norsk",
"pl_PL": "Polski",
"pt_BR": "Português (BR)",
"ru_RU": "Русский",
"sv_SE": "Svenska",
"th_TH": "ภาษาไทย",
"tr_TR": "Türkçe",
"uk_UA": "Українська",
"zh_CN": "简体中文",
"zh_TW": "繁體中文 (台灣)"
}
}

60
assets/Locales.md Normal file
View file

@ -0,0 +1,60 @@
# Ryubing Locales
Ryubing Locales uses a custom format, which uses a file for defining the supported languages and a folder of json files for the locales themselves.
Each json file holds the locales for a specific part of the emulator, e.g. the Setup Wizard locales are in `SetupWizard.json`, and each locale entry in the file includes all the supported languages in the same place.
## Languages
in the `/assets/` folder you will find the `Languages.json` file, which defines all the languages supported by the emulator.
The file includes a table of the langauge codes and their langauge names.
#Example of the format for Languages.json
{
"Languages": {
"ar_SA": "اَلْعَرَبِيَّةُ",
"en_US": "English (US)",
...
"zh_TW": "繁體中文 (台灣)"
}
}
## Locales
in the `/assets/Locales/` folder you will find the json files, which define all the locales supported by the emulator.
Each json file holds locales for a specific part of the emulator in a large array of locale objects.
Each locale is made up an ID used for lookup and a list of the languages and their matching translations.
Any empty string or null value will automatically use the English translation instead in the emulator.
### Format
When adding a new locale, you just need to add the ID and the en_US language translation, then the validation system will add default values for the rest of languages automatically, when rebuilding the project.
If you want to signal that a translation is supposed to match the English translation, you just have to replace the empty string with `null`.
When you want to check what translations are missing for a language just search for `"<lang_code>": ""`, e.g: `"en_US": ""` (but with any other language, as English will never be missing translations).
### Legacy file (Root.json)
Currently all older locales are stored in `Root.json`, but they are slowly being moved into newer, more descriptive json files, to make the locale system more accessible.
Do **not** add new locales to `Root.json`.
If no json file exists for the specific part of the emulator you're working on, you should instead add a new json file for that part.
#Example of the format for Root.json
{
"Locales": [
{
"ID": "MenuBarActionsOpenMiiEditor",
"Translations": {
"ar_SA": "",
"en_US": "Mii Editor",
...
"zh_TW": "Mii 編輯器"
}
},
{
"ID": "KeyNumber9",
"Translations": {
"ar_SA": "٩",
"en_US": "9",
...
"zh_TW": null
}
}
]
}

View file

@ -1,72 +1,5 @@
{ {
"Info": {
"Format1": "The Locale file uses a custom Unified format.",
"Format2": "The file starts with a list of all the supported languages.",
"Format3": "Each locale is made up an ID used for lookup and a list",
"Format4": "of the languages and their matching translations.",
"Format5": "When adding a new locale you just need to add the ID and",
"Format6": "the en_US language translation, then the validation system",
"Format7": "will add the rest of the languages automatically on rebuild.",
"Format8": "By default the languages will be added with an empty string.",
"Format9": "Any empty string or null value will automatically match the",
"Format10": "English translation.",
"Format11": "If you want to signal that a translation is supposed to",
"Format12": "match the English translation, you just have to replace the",
"Format13": "empty string with null.",
"Format14": "Translators who want to check what translations are missing",
"Format15": "for their language just need to search for:",
"Format16": "{'lang_code': ''} with double quotes instead of single",
"Format17": "e.g: {'en_US': ''} (but with any other language as English",
"Format18": "will never be missing translations)."
},
"Languages": [
"ar_SA",
"de_DE",
"el_GR",
"en_US",
"es_ES",
"fr_FR",
"he_IL",
"it_IT",
"ja_JP",
"ko_KR",
"no_NO",
"pl_PL",
"pt_BR",
"ru_RU",
"sv_SE",
"th_TH",
"tr_TR",
"uk_UA",
"zh_CN",
"zh_TW"
],
"Locales": [ "Locales": [
{
"ID": "Language",
"Translations": {
"ar_SA": "اَلْعَرَبِيَّةُ",
"de_DE": "Deutsch",
"el_GR": "Ελληνικά",
"en_US": "English (US)",
"es_ES": "Español (ES)",
"fr_FR": "Français (FR)",
"he_IL": "עִברִית",
"it_IT": "Italiano",
"ja_JP": "日本語",
"ko_KR": "한국어",
"no_NO": "Norsk",
"pl_PL": "Polski",
"pt_BR": "Português (BR)",
"ru_RU": "Русский",
"sv_SE": "Svenska",
"th_TH": "ภาษาไทย",
"tr_TR": "Türkçe",
"uk_UA": "Українська",
"zh_CN": "简体中文",
"zh_TW": "繁體中文 (台灣)"
}
},
{ {
"ID": "MenuBarActionsOpenMiiEditor", "ID": "MenuBarActionsOpenMiiEditor",
"Translations": { "Translations": {

View file

@ -19,6 +19,11 @@ namespace Ryujinx.Audio.Renderer.Parameter.Effect
/// The output channel indices that will be used by the <see cref="Dsp.AudioProcessor"/>. /// The output channel indices that will be used by the <see cref="Dsp.AudioProcessor"/>.
/// </summary> /// </summary>
public Array6<byte> Output; public Array6<byte> Output;
/// <summary>
/// Reserved/unused.
/// </summary>
private readonly uint _padding;
/// <summary> /// <summary>
/// Biquad filter numerator (b0, b1, b2). /// Biquad filter numerator (b0, b1, b2).

View file

@ -11,9 +11,7 @@ namespace Ryujinx.BuildValidationTasks
{ {
static readonly JsonSerializerOptions _jsonOptions = new() static readonly JsonSerializerOptions _jsonOptions = new()
{ {
WriteIndented = true, WriteIndented = true, NewLine = "\n", Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
NewLine = "\n",
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
}; };
public LocalesValidationTask() { } public LocalesValidationTask() { }
@ -22,77 +20,116 @@ namespace Ryujinx.BuildValidationTasks
{ {
Console.WriteLine("Running Locale Validation Task..."); Console.WriteLine("Running Locale Validation Task...");
string path = projectPath + "assets/locales.json"; bool encounteredIssue = false;
string langPath = projectPath + "assets/Languages.json";
string data; string data;
using (StreamReader sr = new(path)) using (StreamReader sr = new(langPath))
{ {
data = sr.ReadToEnd(); data = sr.ReadToEnd();
} }
LocalesJson json;
if (isGitRunner && data.Contains("\r\n")) if (isGitRunner && data.Contains("\r\n"))
throw new FormatException("locales.json is using CRLF line endings! It should be using LF line endings, rebuild locally to fix..."); throw new FormatException("Languages.json is using CRLF line endings! It should be using LF line endings, rebuild locally to fix...");
LanguagesJson langJson;
try try
{ {
json = JsonSerializer.Deserialize<LocalesJson>(data); langJson = JsonSerializer.Deserialize<LanguagesJson>(data);
} }
catch (JsonException e) catch (JsonException e)
{ {
throw new JsonException(e.Message); //shorter and easier stacktrace throw new JsonException(e.Message); //shorter and easier stacktrace
} }
bool encounteredIssue = false; foreach ((string code, string lang) in langJson.Languages)
for (int i = 0; i < json.Locales.Count; i++)
{ {
LocalesEntry locale = json.Locales[i]; if (string.IsNullOrEmpty(lang))
foreach (string langCode in json.Languages.Where(lang => !locale.Translations.ContainsKey(lang)))
{ {
encounteredIssue = true; throw new JsonException($"{code} language name missing!");
if (!isGitRunner)
{
locale.Translations.Add(langCode, string.Empty);
Console.WriteLine($"Added '{langCode}' to Locale '{locale.ID}'");
}
else
{
Console.WriteLine($"Missing '{langCode}' in Locale '{locale.ID}'!");
}
} }
foreach (string langCode in json.Languages.Where(lang => locale.Translations.ContainsKey(lang) && lang != "en_US" && locale.Translations[lang] == locale.Translations["en_US"]))
{
encounteredIssue = true;
if (!isGitRunner)
{
locale.Translations[langCode] = string.Empty;
Console.WriteLine($"Lanugage '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'! Resetting it...");
}
else
{
Console.WriteLine($"Lanugage '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'!");
}
}
locale.Translations = locale.Translations.OrderBy(pair => pair.Key).ToDictionary(pair => pair.Key, pair => pair.Value);
json.Locales[i] = locale;
} }
if (isGitRunner && encounteredIssue) string folderPath = projectPath + "assets/Locales/";
throw new JsonException("1 or more locales are invalid! Rebuild locally to fix...");
string jsonString = JsonSerializer.Serialize(json, _jsonOptions); string[] paths = Directory.GetFiles(folderPath, "*.json", SearchOption.AllDirectories);
using (StreamWriter sw = new(path)) foreach (string path in paths)
{ {
sw.Write(jsonString); using (StreamReader sr = new(path))
{
data = sr.ReadToEnd();
}
if (isGitRunner && data.Contains("\r\n"))
throw new FormatException($"{Path.GetFileName(path)} is using CRLF line endings! It should be using LF line endings, rebuild locally to fix...");
LocalesJson json;
try
{
json = JsonSerializer.Deserialize<LocalesJson>(data);
}
catch (JsonException e)
{
throw new JsonException(e.Message); //shorter and easier stacktrace
}
for (int i = 0; i < json.Locales.Count; i++)
{
LocalesEntry locale = json.Locales[i];
foreach (string langCode in
langJson.Languages.Keys.Where(lang => !locale.Translations.ContainsKey(lang)))
{
encounteredIssue = true;
if (!isGitRunner)
{
locale.Translations.Add(langCode, string.Empty);
Console.WriteLine($"Added '{langCode}' to Locale '{locale.ID}'");
}
else
{
Console.WriteLine($"Missing '{langCode}' in Locale '{locale.ID}'!");
}
}
foreach (string langCode in langJson.Languages.Keys.Where(lang =>
locale.Translations.ContainsKey(lang) && lang != "en_US" &&
locale.Translations[lang] == locale.Translations["en_US"]))
{
encounteredIssue = true;
if (!isGitRunner)
{
locale.Translations[langCode] = string.Empty;
Console.WriteLine(
$"Lanugage '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'! Resetting it...");
}
else
{
Console.WriteLine(
$"Lanugage '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'!");
}
}
locale.Translations = locale.Translations.OrderBy(pair => pair.Key)
.ToDictionary(pair => pair.Key, pair => pair.Value);
json.Locales[i] = locale;
}
if (isGitRunner && encounteredIssue)
throw new JsonException("1 or more locales are invalid! Rebuild locally to fix...");
string jsonString = JsonSerializer.Serialize(json, _jsonOptions);
using (StreamWriter sw = new(path))
{
sw.Write(jsonString);
}
} }
Console.WriteLine("Finished Locale Validation Task!"); Console.WriteLine("Finished Locale Validation Task!");
@ -100,10 +137,13 @@ namespace Ryujinx.BuildValidationTasks
return true; return true;
} }
struct LanguagesJson
{
public Dictionary<string, string> Languages { get; set; }
}
struct LocalesJson struct LocalesJson
{ {
public Dictionary<string, string> Info { get; set; }
public List<string> Languages { get; set; }
public List<LocalesEntry> Locales { get; set; } public List<LocalesEntry> Locales { get; set; }
} }

View file

@ -1,9 +1,8 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration namespace Ryujinx.Common.Configuration
{ {
[JsonConverter(typeof(TypedStringEnumConverter<AntiAliasing>))] [JsonConverter(typeof(JsonStringEnumConverter<AntiAliasing>))]
public enum AntiAliasing public enum AntiAliasing
{ {
None, None,

View file

@ -1,9 +1,8 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration namespace Ryujinx.Common.Configuration
{ {
[JsonConverter(typeof(TypedStringEnumConverter<AspectRatio>))] [JsonConverter(typeof(JsonStringEnumConverter<AspectRatio>))]
public enum AspectRatio public enum AspectRatio
{ {
Fixed4x3, Fixed4x3,

View file

@ -1,9 +1,8 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration namespace Ryujinx.Common.Configuration
{ {
[JsonConverter(typeof(TypedStringEnumConverter<BackendThreading>))] [JsonConverter(typeof(JsonStringEnumConverter<BackendThreading>))]
public enum BackendThreading public enum BackendThreading
{ {
Auto, Auto,

View file

@ -1,9 +1,8 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration namespace Ryujinx.Common.Configuration
{ {
[JsonConverter(typeof(TypedStringEnumConverter<GraphicsBackend>))] [JsonConverter(typeof(JsonStringEnumConverter<GraphicsBackend>))]
public enum GraphicsBackend public enum GraphicsBackend
{ {
Vulkan, Vulkan,

View file

@ -1,9 +1,8 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration namespace Ryujinx.Common.Configuration
{ {
[JsonConverter(typeof(TypedStringEnumConverter<GraphicsDebugLevel>))] [JsonConverter(typeof(JsonStringEnumConverter<GraphicsDebugLevel>))]
public enum GraphicsDebugLevel public enum GraphicsDebugLevel
{ {
None, None,

View file

@ -1,9 +1,8 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration.Hid.Controller namespace Ryujinx.Common.Configuration.Hid.Controller
{ {
[JsonConverter(typeof(TypedStringEnumConverter<GamepadInputId>))] [JsonConverter(typeof(JsonStringEnumConverter<GamepadInputId>))]
public enum GamepadInputId : byte public enum GamepadInputId : byte
{ {
Unbound, Unbound,

View file

@ -1,9 +1,8 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration.Hid.Controller.Motion namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
{ {
[JsonConverter(typeof(TypedStringEnumConverter<MotionInputBackendType>))] [JsonConverter(typeof(JsonStringEnumConverter<MotionInputBackendType>))]
public enum MotionInputBackendType : byte public enum MotionInputBackendType : byte
{ {
Invalid, Invalid,

View file

@ -1,15 +1,14 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration.Hid.Controller namespace Ryujinx.Common.Configuration.Hid.Controller
{ {
[JsonConverter(typeof(TypedStringEnumConverter<StickInputId>))] [JsonConverter(typeof(JsonStringEnumConverter<StickInputId>))]
public enum StickInputId : byte public enum StickInputId : byte
{ {
Unbound, Unbound,
Left, Left,
Right, Right,
Count, Count,
} }
} }

View file

@ -1,4 +1,3 @@
using Ryujinx.Common.Utilities;
using System; using System;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@ -6,7 +5,7 @@ namespace Ryujinx.Common.Configuration.Hid
{ {
// This enum was duplicated from Ryujinx.HLE.HOS.Services.Hid.PlayerIndex and should be kept identical // This enum was duplicated from Ryujinx.HLE.HOS.Services.Hid.PlayerIndex and should be kept identical
[Flags] [Flags]
[JsonConverter(typeof(TypedStringEnumConverter<ControllerType>))] [JsonConverter(typeof(JsonStringEnumConverter<ControllerType>))]
public enum ControllerType public enum ControllerType
{ {
None, None,

View file

@ -1,9 +1,8 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration.Hid namespace Ryujinx.Common.Configuration.Hid
{ {
[JsonConverter(typeof(TypedStringEnumConverter<InputBackendType>))] [JsonConverter(typeof(JsonStringEnumConverter<InputBackendType>))]
public enum InputBackendType public enum InputBackendType
{ {
Invalid, Invalid,

View file

@ -1,9 +1,8 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration.Hid namespace Ryujinx.Common.Configuration.Hid
{ {
[JsonConverter(typeof(TypedStringEnumConverter<Key>))] [JsonConverter(typeof(JsonStringEnumConverter<Key>))]
public enum Key public enum Key
{ {
Unknown, Unknown,

View file

@ -1,10 +1,9 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration.Hid namespace Ryujinx.Common.Configuration.Hid
{ {
// This enum was duplicated from Ryujinx.HLE.HOS.Services.Hid.PlayerIndex and should be kept identical // This enum was duplicated from Ryujinx.HLE.HOS.Services.Hid.PlayerIndex and should be kept identical
[JsonConverter(typeof(TypedStringEnumConverter<PlayerIndex>))] [JsonConverter(typeof(JsonStringEnumConverter<PlayerIndex>))]
public enum PlayerIndex public enum PlayerIndex
{ {
Player1 = 0, Player1 = 0,

View file

@ -1,9 +1,8 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration namespace Ryujinx.Common.Configuration
{ {
[JsonConverter(typeof(TypedStringEnumConverter<MemoryManagerMode>))] [JsonConverter(typeof(JsonStringEnumConverter<MemoryManagerMode>))]
public enum MemoryManagerMode : byte public enum MemoryManagerMode : byte
{ {
SoftwarePageTable, SoftwarePageTable,

View file

@ -1,9 +1,8 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration namespace Ryujinx.Common.Configuration
{ {
[JsonConverter(typeof(TypedStringEnumConverter<ScalingFilter>))] [JsonConverter(typeof(JsonStringEnumConverter<ScalingFilter>))]
public enum ScalingFilter public enum ScalingFilter
{ {
Bilinear, Bilinear,

View file

@ -1,9 +1,8 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Common.Logging namespace Ryujinx.Common.Logging
{ {
[JsonConverter(typeof(TypedStringEnumConverter<LogClass>))] [JsonConverter(typeof(JsonStringEnumConverter<LogClass>))]
public enum LogClass public enum LogClass
{ {
Application, Application,

View file

@ -1,9 +1,8 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Common.Logging namespace Ryujinx.Common.Logging
{ {
[JsonConverter(typeof(TypedStringEnumConverter<LogLevel>))] [JsonConverter(typeof(JsonStringEnumConverter<LogLevel>))]
public enum LogLevel public enum LogLevel
{ {
Debug, Debug,

View file

@ -7,6 +7,9 @@ namespace Ryujinx.Common.Memory
{ {
private static readonly RecyclableMemoryStreamManager _shared = new(); private static readonly RecyclableMemoryStreamManager _shared = new();
private static readonly ObjectPool<RecyclableMemoryStream> _streamPool =
new(() => new RecyclableMemoryStream(_shared, Guid.NewGuid(), null, 0));
/// <summary> /// <summary>
/// We don't expose the <c>RecyclableMemoryStreamManager</c> directly because version 2.x /// We don't expose the <c>RecyclableMemoryStreamManager</c> directly because version 2.x
/// returns them as <c>MemoryStream</c>. This Shared class is here to a) offer only the GetStream() versions we use /// returns them as <c>MemoryStream</c>. This Shared class is here to a) offer only the GetStream() versions we use
@ -19,7 +22,12 @@ namespace Ryujinx.Common.Memory
/// </summary> /// </summary>
/// <returns>A <c>RecyclableMemoryStream</c></returns> /// <returns>A <c>RecyclableMemoryStream</c></returns>
public static RecyclableMemoryStream GetStream() public static RecyclableMemoryStream GetStream()
=> new(_shared); {
RecyclableMemoryStream stream = _streamPool.Allocate();
stream.SetLength(0);
return stream;
}
/// <summary> /// <summary>
/// Retrieve a new <c>MemoryStream</c> object with the contents copied from the provided /// Retrieve a new <c>MemoryStream</c> object with the contents copied from the provided
@ -55,7 +63,8 @@ namespace Ryujinx.Common.Memory
RecyclableMemoryStream stream = null; RecyclableMemoryStream stream = null;
try try
{ {
stream = new RecyclableMemoryStream(_shared, id, tag, buffer.Length); stream = _streamPool.Allocate();
stream.SetLength(0);
stream.Write(buffer); stream.Write(buffer);
stream.Position = 0; stream.Position = 0;
return stream; return stream;
@ -83,7 +92,8 @@ namespace Ryujinx.Common.Memory
RecyclableMemoryStream stream = null; RecyclableMemoryStream stream = null;
try try
{ {
stream = new RecyclableMemoryStream(_shared, id, tag, count); stream = _streamPool.Allocate();
stream.SetLength(0);
stream.Write(buffer, offset, count); stream.Write(buffer, offset, count);
stream.Position = 0; stream.Position = 0;
return stream; return stream;
@ -94,6 +104,11 @@ namespace Ryujinx.Common.Memory
throw; throw;
} }
} }
public static void ReleaseStream(RecyclableMemoryStream stream)
{
_streamPool.Release(stream);
}
} }
} }
} }

View file

@ -127,7 +127,7 @@ namespace Ryujinx.Common
public static string[] GetAllAvailableResources(string path, string ext = "") public static string[] GetAllAvailableResources(string path, string ext = "")
{ {
return ResolveManifestPath(path).Item1.GetManifestResourceNames() return ResolveManifestPath(path).Item1.GetManifestResourceNames()
.Where(r => r.EndsWith(ext)) .Where(r => r.StartsWith(path.Replace('/', '.')) && r.EndsWith(ext))
.ToArray(); .ToArray();
} }

View file

@ -20,5 +20,21 @@ namespace Ryujinx.Common.Utilities
Debug.Assert(res != -1); Debug.Assert(res != -1);
} }
} }
// "dumpable" attribute of the calling process
private const int PR_SET_DUMPABLE = 4;
[DllImport("libc", SetLastError = true)]
private static extern int prctl(int option, int arg2);
public static void SetCoreDumpable(bool dumpable)
{
if (OperatingSystem.IsLinux())
{
int dumpableInt = dumpable ? 1 : 0;
int result = prctl(PR_SET_DUMPABLE, dumpableInt);
Debug.Assert(result == 0);
}
}
} }
} }

View file

@ -1,37 +0,0 @@
#nullable enable
using Ryujinx.Common.Logging;
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Utilities
{
/// <summary>
/// Specifies that value of <see cref="TEnum"/> will be serialized as string in JSONs
/// </summary>
/// <remarks>
/// Trimming friendly alternative to <see cref="JsonStringEnumConverter"/>.
/// Get rid of this converter if dotnet supports similar functionality out of the box.
/// </remarks>
/// <typeparam name="TEnum">Type of enum to serialize</typeparam>
public sealed class TypedStringEnumConverter<TEnum> : JsonConverter<TEnum> where TEnum : struct, Enum
{
public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
string? enumValue = reader.GetString();
if (Enum.TryParse(enumValue, out TEnum value))
{
return value;
}
Logger.Warning?.Print(LogClass.Configuration, $"Failed to parse enum value \"{enumValue}\" for {typeof(TEnum)}, using default \"{default(TEnum)}\"");
return default;
}
public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
}

View file

@ -82,7 +82,7 @@ namespace Ryujinx.Graphics.GAL
void SetRasterizerDiscard(bool discard); void SetRasterizerDiscard(bool discard);
void SetRenderTargetColorMasks(ReadOnlySpan<uint> componentMask); void SetRenderTargetColorMasks(ReadOnlySpan<uint> componentMask);
void SetRenderTargets(ITexture[] colors, ITexture depthStencil); void SetRenderTargets(Span<ITexture> colors, ITexture depthStencil);
void SetScissors(ReadOnlySpan<Rectangle<int>> regions); void SetScissors(ReadOnlySpan<Rectangle<int>> regions);

View file

@ -1,5 +1,6 @@
using Ryujinx.Graphics.GAL.Multithreading.Model; using Ryujinx.Graphics.GAL.Multithreading.Model;
using Ryujinx.Graphics.GAL.Multithreading.Resources; using Ryujinx.Graphics.GAL.Multithreading.Resources;
using System;
using System.Buffers; using System.Buffers;
namespace Ryujinx.Graphics.GAL.Multithreading.Commands namespace Ryujinx.Graphics.GAL.Multithreading.Commands
@ -8,11 +9,13 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands
{ {
public static readonly ArrayPool<ITexture> ArrayPool = ArrayPool<ITexture>.Create(512, 50); public static readonly ArrayPool<ITexture> ArrayPool = ArrayPool<ITexture>.Create(512, 50);
public readonly CommandType CommandType => CommandType.SetRenderTargets; public readonly CommandType CommandType => CommandType.SetRenderTargets;
private int _colorsCount;
private TableRef<ITexture[]> _colors; private TableRef<ITexture[]> _colors;
private TableRef<ITexture> _depthStencil; private TableRef<ITexture> _depthStencil;
public void Set(TableRef<ITexture[]> colors, TableRef<ITexture> depthStencil) public void Set(int colorsCount, TableRef<ITexture[]> colors, TableRef<ITexture> depthStencil)
{ {
_colorsCount = colorsCount;
_colors = colors; _colors = colors;
_depthStencil = depthStencil; _depthStencil = depthStencil;
} }
@ -20,16 +23,15 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands
public static void Run(ref SetRenderTargetsCommand command, ThreadedRenderer threaded, IRenderer renderer) public static void Run(ref SetRenderTargetsCommand command, ThreadedRenderer threaded, IRenderer renderer)
{ {
ITexture[] colors = command._colors.Get(threaded); ITexture[] colors = command._colors.Get(threaded);
ITexture[] colorsCopy = ArrayPool.Rent(colors.Length); Span<ITexture> colorsSpan = colors.AsSpan(0, command._colorsCount);
for (int i = 0; i < colors.Length; i++) for (int i = 0; i < colorsSpan.Length; i++)
{ {
colorsCopy[i] = ((ThreadedTexture)colors[i])?.Base; colorsSpan[i] = ((ThreadedTexture)colorsSpan[i])?.Base;
} }
renderer.Pipeline.SetRenderTargets(colorsCopy, command._depthStencil.GetAs<ThreadedTexture>(threaded)?.Base); renderer.Pipeline.SetRenderTargets(colorsSpan, command._depthStencil.GetAs<ThreadedTexture>(threaded)?.Base);
ArrayPool.Return(colorsCopy);
ArrayPool.Return(colors); ArrayPool.Return(colors);
} }
} }

View file

@ -267,12 +267,12 @@ namespace Ryujinx.Graphics.GAL.Multithreading
_renderer.QueueCommand(); _renderer.QueueCommand();
} }
public unsafe void SetRenderTargets(ITexture[] colors, ITexture depthStencil) public unsafe void SetRenderTargets(Span<ITexture> colors, ITexture depthStencil)
{ {
ITexture[] colorsCopy = SetRenderTargetsCommand.ArrayPool.Rent(colors.Length); ITexture[] colorsCopy = SetRenderTargetsCommand.ArrayPool.Rent(colors.Length);
colors.CopyTo(colorsCopy, 0); colors.CopyTo(colorsCopy.AsSpan());
_renderer.New<SetRenderTargetsCommand>()->Set(Ref(colorsCopy), Ref(depthStencil)); _renderer.New<SetRenderTargetsCommand>()->Set(colors.Length, Ref(colorsCopy), Ref(depthStencil));
_renderer.QueueCommand(); _renderer.QueueCommand();
} }

View file

@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
/// </summary> /// </summary>
public class ThreadedRenderer : IRenderer public class ThreadedRenderer : IRenderer
{ {
private const int SpanPoolBytes = 4 * 1024 * 1024; private const int SpanPoolBytes = 8 * 1024 * 1024;
private const int MaxRefsPerCommand = 2; private const int MaxRefsPerCommand = 2;
private const int QueueCount = 10000; private const int QueueCount = 10000;

View file

@ -404,9 +404,12 @@ namespace Ryujinx.Graphics.Gpu
if (force || _pendingSync || (syncPoint && SyncpointActions.Count > 0)) if (force || _pendingSync || (syncPoint && SyncpointActions.Count > 0))
{ {
foreach (ISyncActionHandler action in SyncActions) for (int i = 0; i < SyncActions.Count; i++)
{ {
action.SyncPreAction(syncPoint); if (SyncActions[i].SyncPreAction(syncPoint))
{
SyncActions.RemoveAt(i--);
}
} }
foreach (ISyncActionHandler action in SyncpointActions) foreach (ISyncActionHandler action in SyncpointActions)

View file

@ -411,7 +411,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// flushes often enough, which is determined by the flush balance. /// flushes often enough, which is determined by the flush balance.
/// </summary> /// </summary>
/// <inheritdoc/> /// <inheritdoc/>
public void SyncPreAction(bool syncpoint) public bool SyncPreAction(bool syncpoint)
{ {
if (syncpoint || NextSyncCopies()) if (syncpoint || NextSyncCopies())
{ {
@ -421,6 +421,8 @@ namespace Ryujinx.Graphics.Gpu.Image
_registeredBufferSync = _modifiedSync; _registeredBufferSync = _modifiedSync;
} }
} }
return true;
} }
/// <summary> /// <summary>

View file

@ -15,7 +15,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <summary> /// <summary>
/// Buffer, used to store vertex and index data, uniform and storage buffers, and others. /// Buffer, used to store vertex and index data, uniform and storage buffers, and others.
/// </summary> /// </summary>
class Buffer : INonOverlappingRange, ISyncActionHandler, IDisposable class Buffer : INonOverlappingRange<Buffer>, ISyncActionHandler, IDisposable
{ {
private const ulong GranularBufferThreshold = 4096; private const ulong GranularBufferThreshold = 4096;
@ -41,6 +41,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// End address of the buffer in guest memory. /// End address of the buffer in guest memory.
/// </summary> /// </summary>
public ulong EndAddress => Address + Size; public ulong EndAddress => Address + Size;
public Buffer Next { get; set; }
public Buffer Previous { get; set; }
/// <summary> /// <summary>
/// Increments when the buffer is (partially) unmapped or disposed. /// Increments when the buffer is (partially) unmapped or disposed.
@ -87,6 +90,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
private readonly bool _useGranular; private readonly bool _useGranular;
private bool _syncActionRegistered; private bool _syncActionRegistered;
private bool _bufferInherited;
private int _referenceCount = 1; private int _referenceCount = 1;
@ -113,7 +117,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
ulong size, ulong size,
BufferStage stage, BufferStage stage,
bool sparseCompatible, bool sparseCompatible,
RangeItem<Buffer>[] baseBuffers) Buffer[] baseBuffers)
{ {
_context = context; _context = context;
_physicalMemory = physicalMemory; _physicalMemory = physicalMemory;
@ -134,15 +138,15 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (baseBuffers.Length != 0) if (baseBuffers.Length != 0)
{ {
baseHandles = new List<IRegionHandle>(); baseHandles = new List<IRegionHandle>();
foreach (RangeItem<Buffer> item in baseBuffers) foreach (Buffer item in baseBuffers)
{ {
if (item.Value._useGranular) if (item._useGranular)
{ {
baseHandles.AddRange(item.Value._memoryTrackingGranular.Handles); baseHandles.AddRange(item._memoryTrackingGranular.Handles);
} }
else else
{ {
baseHandles.Add(item.Value._memoryTracking); baseHandles.Add(item._memoryTracking);
} }
} }
} }
@ -247,14 +251,14 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// Checks if a given range overlaps with the buffer. /// Checks if a given range overlaps with the buffer.
/// </summary> /// </summary>
/// <param name="address">Start address of the range</param> /// <param name="address">Start address of the range</param>
/// <param name="size">Size in bytes of the range</param> /// <param name="endAddress">End address of the range</param>
/// <returns>True if the range overlaps, false otherwise</returns> /// <returns>True if the range overlaps, false otherwise</returns>
public bool OverlapsWith(ulong address, ulong size) public bool OverlapsWith(ulong address, ulong endAddress)
{ {
return Address < address + size && address < EndAddress; return Address < endAddress && address < EndAddress;
} }
public INonOverlappingRange Split(ulong splitAddress) public INonOverlappingRange<Buffer> Split(ulong splitAddress)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@ -389,11 +393,16 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// This will copy any buffer ranges designated for pre-flushing. /// This will copy any buffer ranges designated for pre-flushing.
/// </summary> /// </summary>
/// <param name="syncpoint">True if the action is a guest syncpoint</param> /// <param name="syncpoint">True if the action is a guest syncpoint</param>
public void SyncPreAction(bool syncpoint) public bool SyncPreAction(bool syncpoint)
{ {
if (_bufferInherited)
{
return true;
}
if (_referenceCount == 0) if (_referenceCount == 0)
{ {
return; return false;
} }
if (BackingState.ShouldChangeBacking()) if (BackingState.ShouldChangeBacking())
@ -410,6 +419,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
_modifiedRanges?.GetRangesAtSync(Address, Size, _context.SyncNumber, _syncPreRangeAction); _modifiedRanges?.GetRangesAtSync(Address, Size, _context.SyncNumber, _syncPreRangeAction);
} }
} }
return false;
} }
void SyncPreRangeAction(ulong address, ulong size) void SyncPreRangeAction(ulong address, ulong size)
@ -426,10 +437,13 @@ namespace Ryujinx.Graphics.Gpu.Memory
{ {
_syncActionRegistered = false; _syncActionRegistered = false;
if (_bufferInherited)
{
return true;
}
if (_useGranular) if (_useGranular)
{ {
_modifiedRanges?.GetRanges(Address, Size, _syncRangeAction); _modifiedRanges?.GetRanges(Address, Size, _syncRangeAction);
} }
else else
@ -453,6 +467,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="from">The buffer to inherit from</param> /// <param name="from">The buffer to inherit from</param>
public void InheritModifiedRanges(Buffer from) public void InheritModifiedRanges(Buffer from)
{ {
from._bufferInherited = true;
if (from._modifiedRanges is { HasRanges: true }) if (from._modifiedRanges is { HasRanges: true })
{ {
if (from._syncActionRegistered && !_syncActionRegistered) if (from._syncActionRegistered && !_syncActionRegistered)

View file

@ -56,7 +56,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="parent">Parent buffer</param> /// <param name="parent">Parent buffer</param>
/// <param name="stage">Initial buffer stage</param> /// <param name="stage">Initial buffer stage</param>
/// <param name="baseBuffers">Buffers to inherit state from</param> /// <param name="baseBuffers">Buffers to inherit state from</param>
public BufferBackingState(GpuContext context, Buffer parent, BufferStage stage, RangeItem<Buffer>[] baseBuffers) public BufferBackingState(GpuContext context, Buffer parent, BufferStage stage, Buffer[] baseBuffers)
{ {
_size = (int)parent.Size; _size = (int)parent.Size;
_systemMemoryType = context.Capabilities.MemoryType; _systemMemoryType = context.Capabilities.MemoryType;
@ -102,9 +102,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (baseBuffers.Length != 0) if (baseBuffers.Length != 0)
{ {
foreach (RangeItem<Buffer> item in baseBuffers) foreach (Buffer item in baseBuffers)
{ {
CombineState(item.Value.BackingState); CombineState(item.BackingState);
} }
} }
} }

View file

@ -79,16 +79,13 @@ namespace Ryujinx.Graphics.Gpu.Memory
for (int index = 0; index < range.Count; index++) for (int index = 0; index < range.Count; index++)
{ {
MemoryRange subRange = range.GetSubRange(index); MemoryRange subRange = range.GetSubRange(index);
_buffers.Lock.EnterReadLock(); ReadOnlySpan<Buffer> overlaps = _buffers.FindOverlapsAsSpan(subRange.Address, subRange.Size);
Span<RangeItem<Buffer>> overlaps = _buffers.FindOverlapsAsSpan(subRange.Address, subRange.Size);
for (int i = 0; i < overlaps.Length; i++) for (int i = 0; i < overlaps.Length; i++)
{ {
overlaps[i].Value.Unmapped(subRange.Address, subRange.Size); overlaps[i].Unmapped(subRange.Address, subRange.Size);
} }
_buffers.Lock.ExitReadLock();
} }
} }
@ -328,7 +325,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
ulong alignedEndAddress = (endAddress + alignmentMask) & ~alignmentMask; ulong alignedEndAddress = (endAddress + alignmentMask) & ~alignmentMask;
ulong alignedSize = alignedEndAddress - alignedAddress; ulong alignedSize = alignedEndAddress - alignedAddress;
Buffer buffer = _buffers.FindOverlap(alignedAddress, alignedSize).Value; Buffer buffer = _buffers.FindOverlap(alignedAddress, alignedSize);
BufferRange bufferRange = buffer.GetRange(alignedAddress, alignedSize, false); BufferRange bufferRange = buffer.GetRange(alignedAddress, alignedSize, false);
alignedSubRanges[i] = new MemoryRange(alignedAddress, alignedSize); alignedSubRanges[i] = new MemoryRange(alignedAddress, alignedSize);
@ -395,7 +392,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (subRange.Address != MemoryManager.PteUnmapped) if (subRange.Address != MemoryManager.PteUnmapped)
{ {
Buffer buffer = _buffers.FindOverlap(subRange.Address, subRange.Size).Value; Buffer buffer = _buffers.FindOverlap(subRange.Address, subRange.Size);
virtualBuffer.AddPhysicalDependency(buffer, subRange.Address, dstOffset, subRange.Size); virtualBuffer.AddPhysicalDependency(buffer, subRange.Address, dstOffset, subRange.Size);
physicalBuffers.Add(buffer); physicalBuffers.Add(buffer);
@ -487,10 +484,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="stage">The type of usage that created the buffer</param> /// <param name="stage">The type of usage that created the buffer</param>
private void CreateBufferAligned(ulong address, ulong size, BufferStage stage) private void CreateBufferAligned(ulong address, ulong size, BufferStage stage)
{ {
Buffer newBuffer = null; ReadOnlySpan<Buffer> overlaps = _buffers.FindOverlapsAsSpan(address, size);
_buffers.Lock.EnterWriteLock();
Span<RangeItem<Buffer>> overlaps = _buffers.FindOverlapsAsSpan(address, size);
if (overlaps.Length != 0) if (overlaps.Length != 0)
{ {
@ -521,7 +515,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
{ {
// Try to grow the buffer by 1.5x of its current size. // Try to grow the buffer by 1.5x of its current size.
// This improves performance in the cases where the buffer is resized often by small amounts. // This improves performance in the cases where the buffer is resized often by small amounts.
ulong existingSize = overlaps[0].Value.Size; ulong existingSize = overlaps[0].Size;
ulong growthSize = (existingSize + Math.Min(existingSize >> 1, MaxDynamicGrowthSize)) & ~BufferAlignmentMask; ulong growthSize = (existingSize + Math.Min(existingSize >> 1, MaxDynamicGrowthSize)) & ~BufferAlignmentMask;
size = Math.Max(size, growthSize); size = Math.Max(size, growthSize);
@ -535,39 +529,22 @@ namespace Ryujinx.Graphics.Gpu.Memory
for (int i = 0; i < overlaps.Length; i++) for (int i = 0; i < overlaps.Length; i++)
{ {
anySparseCompatible |= overlaps[i].Value.SparseCompatible; anySparseCompatible |= overlaps[i].SparseCompatible;
} }
RangeItem<Buffer>[] overlapsArray = overlaps.ToArray(); Buffer[] overlapsArray = overlaps.ToArray();
_buffers.RemoveRange(overlaps[0], overlaps[^1]); _buffers.RemoveRange(overlaps[0], overlaps[^1]);
_buffers.Lock.ExitWriteLock();
ulong newSize = endAddress - address; ulong newSize = endAddress - address;
newBuffer = CreateBufferAligned(address, newSize, stage, anySparseCompatible, overlapsArray); _buffers.Add(CreateBufferAligned(address, newSize, stage, anySparseCompatible, overlapsArray));
}
else
{
_buffers.Lock.ExitWriteLock();
} }
} }
else else
{ {
_buffers.Lock.ExitWriteLock();
// No overlap, just create a new buffer. // No overlap, just create a new buffer.
newBuffer = new(_context, _physicalMemory, address, size, stage, sparseCompatible: false, []); _buffers.Add(new(_context, _physicalMemory, address, size, stage, sparseCompatible: false, []));
}
if (newBuffer is not null)
{
_buffers.Lock.EnterWriteLock();
_buffers.Add(newBuffer);
_buffers.Lock.ExitWriteLock();
} }
} }
@ -583,10 +560,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
private void CreateBufferAligned(ulong address, ulong size, BufferStage stage, ulong alignment) private void CreateBufferAligned(ulong address, ulong size, BufferStage stage, ulong alignment)
{ {
bool sparseAligned = alignment >= SparseBufferAlignmentSize; bool sparseAligned = alignment >= SparseBufferAlignmentSize;
Buffer newBuffer = null;
_buffers.Lock.EnterWriteLock(); ReadOnlySpan<Buffer> overlaps = _buffers.FindOverlapsAsSpan(address, size);
Span<RangeItem<Buffer>> overlaps = _buffers.FindOverlapsAsSpan(address, size);
if (overlaps.Length != 0) if (overlaps.Length != 0)
{ {
@ -598,7 +573,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (overlaps[0].Address > address || if (overlaps[0].Address > address ||
overlaps[0].EndAddress < endAddress || overlaps[0].EndAddress < endAddress ||
(overlaps[0].Address & (alignment - 1)) != 0 || (overlaps[0].Address & (alignment - 1)) != 0 ||
(!overlaps[0].Value.SparseCompatible && sparseAligned)) (!overlaps[0].SparseCompatible && sparseAligned))
{ {
// We need to make sure the new buffer is properly aligned. // We need to make sure the new buffer is properly aligned.
// However, after the range is aligned, it is possible that it // However, after the range is aligned, it is possible that it
@ -622,35 +597,18 @@ namespace Ryujinx.Graphics.Gpu.Memory
ulong newSize = endAddress - address; ulong newSize = endAddress - address;
RangeItem<Buffer>[] overlapsArray = overlaps.ToArray(); Buffer[] overlapsArray = overlaps.ToArray();
_buffers.RemoveRange(overlaps[0], overlaps[^1]); _buffers.RemoveRange(overlaps[0], overlaps[^1]);
_buffers.Lock.ExitWriteLock(); _buffers.Add(CreateBufferAligned(address, newSize, stage, sparseAligned, overlapsArray));
newBuffer = CreateBufferAligned(address, newSize, stage, sparseAligned, overlapsArray);
}
else
{
_buffers.Lock.ExitWriteLock();
} }
} }
else else
{ {
_buffers.Lock.ExitWriteLock();
// No overlap, just create a new buffer. // No overlap, just create a new buffer.
newBuffer = new(_context, _physicalMemory, address, size, stage, sparseAligned, []); _buffers.Add(new(_context, _physicalMemory, address, size, stage, sparseAligned, []));
} }
if (newBuffer is not null)
{
_buffers.Lock.EnterWriteLock();
_buffers.Add(newBuffer);
_buffers.Lock.ExitWriteLock();
}
} }
/// <summary> /// <summary>
@ -663,13 +621,13 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="stage">The type of usage that created the buffer</param> /// <param name="stage">The type of usage that created the buffer</param>
/// <param name="sparseCompatible">Indicates if the buffer can be used in a sparse buffer mapping</param> /// <param name="sparseCompatible">Indicates if the buffer can be used in a sparse buffer mapping</param>
/// <param name="overlaps">Buffers overlapping the range</param> /// <param name="overlaps">Buffers overlapping the range</param>
private Buffer CreateBufferAligned(ulong address, ulong size, BufferStage stage, bool sparseCompatible, RangeItem<Buffer>[] overlaps) private Buffer CreateBufferAligned(ulong address, ulong size, BufferStage stage, bool sparseCompatible, Buffer[] overlaps)
{ {
Buffer newBuffer = new(_context, _physicalMemory, address, size, stage, sparseCompatible, overlaps); Buffer newBuffer = new(_context, _physicalMemory, address, size, stage, sparseCompatible, overlaps);
for (int index = 0; index < overlaps.Length; index++) for (int index = 0; index < overlaps.Length; index++)
{ {
Buffer buffer = overlaps[index].Value; Buffer buffer = overlaps[index];
int dstOffset = (int)(buffer.Address - newBuffer.Address); int dstOffset = (int)(buffer.Address - newBuffer.Address);
@ -897,7 +855,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
{ {
MemoryRange subRange = range.GetSubRange(i); MemoryRange subRange = range.GetSubRange(i);
Buffer subBuffer = _buffers.FindOverlap(subRange.Address, subRange.Size).Value; Buffer subBuffer = _buffers.FindOverlap(subRange.Address, subRange.Size);
subBuffer.SynchronizeMemory(subRange.Address, subRange.Size); subBuffer.SynchronizeMemory(subRange.Address, subRange.Size);
@ -945,7 +903,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (size != 0) if (size != 0)
{ {
buffer = _buffers.FindOverlap(address, size).Value; buffer = _buffers.FindOverlap(address, size);
buffer.CopyFromDependantVirtualBuffers(); buffer.CopyFromDependantVirtualBuffers();
buffer.SynchronizeMemory(address, size); buffer.SynchronizeMemory(address, size);
@ -957,7 +915,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
} }
else else
{ {
buffer = _buffers.FindOverlapFast(address, 1).Value; buffer = _buffers.FindOverlapFast(address, 1);
} }
return buffer; return buffer;
@ -995,7 +953,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
{ {
if (size != 0) if (size != 0)
{ {
Buffer buffer = _buffers.FindOverlap(address, size).Value; Buffer buffer = _buffers.FindOverlap(address, size);
if (copyBackVirtual) if (copyBackVirtual)
{ {

View file

@ -8,7 +8,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <summary> /// <summary>
/// A range within a buffer that has been modified by the GPU. /// A range within a buffer that has been modified by the GPU.
/// </summary> /// </summary>
class BufferModifiedRange : INonOverlappingRange class BufferModifiedRange : INonOverlappingRange<BufferModifiedRange>
{ {
/// <summary> /// <summary>
/// Start address of the range in guest memory. /// Start address of the range in guest memory.
@ -24,6 +24,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// End address of the range in guest memory. /// End address of the range in guest memory.
/// </summary> /// </summary>
public ulong EndAddress => Address + Size; public ulong EndAddress => Address + Size;
public BufferModifiedRange Next { get; set; }
public BufferModifiedRange Previous { get; set; }
/// <summary> /// <summary>
/// The GPU sync number at the time of the last modification. /// The GPU sync number at the time of the last modification.
@ -54,14 +57,14 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// Checks if a given range overlaps with the modified range. /// Checks if a given range overlaps with the modified range.
/// </summary> /// </summary>
/// <param name="address">Start address of the range</param> /// <param name="address">Start address of the range</param>
/// <param name="size">Size in bytes of the range</param> /// <param name="endAddress">End address of the range</param>
/// <returns>True if the range overlaps, false otherwise</returns> /// <returns>True if the range overlaps, false otherwise</returns>
public bool OverlapsWith(ulong address, ulong size) public bool OverlapsWith(ulong address, ulong endAddress)
{ {
return Address < address + size && address < EndAddress; return Address < endAddress && address < EndAddress;
} }
public INonOverlappingRange Split(ulong splitAddress) public INonOverlappingRange<BufferModifiedRange> Split(ulong splitAddress)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@ -119,11 +122,11 @@ namespace Ryujinx.Graphics.Gpu.Memory
// Slices a given region using the modified regions in the list. Calls the action for the new slices. // Slices a given region using the modified regions in the list. Calls the action for the new slices.
Lock.EnterReadLock(); Lock.EnterReadLock();
Span<RangeItem<BufferModifiedRange>> overlaps = FindOverlapsAsSpan(address, size); ReadOnlySpan<BufferModifiedRange> overlaps = FindOverlapsAsSpan(address, size);
for (int i = 0; i < overlaps.Length; i++) for (int i = 0; i < overlaps.Length; i++)
{ {
BufferModifiedRange overlap = overlaps[i].Value; BufferModifiedRange overlap = overlaps[i];
if (overlap.Address > address) if (overlap.Address > address)
{ {
@ -157,7 +160,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
ulong syncNumber = _context.SyncNumber; ulong syncNumber = _context.SyncNumber;
// We may overlap with some existing modified regions. They must be cut into by the new entry. // We may overlap with some existing modified regions. They must be cut into by the new entry.
Lock.EnterWriteLock(); Lock.EnterWriteLock();
(RangeItem<BufferModifiedRange> first, RangeItem<BufferModifiedRange> last) = FindOverlapsAsNodes(address, size); (BufferModifiedRange first, BufferModifiedRange last) = FindOverlapsAsNodes(address, size);
if (first is null) if (first is null)
{ {
@ -170,34 +173,39 @@ namespace Ryujinx.Graphics.Gpu.Memory
{ {
if (first.Address == address && first.EndAddress == endAddress) if (first.Address == address && first.EndAddress == endAddress)
{ {
first.Value.SyncNumber = syncNumber; first.SyncNumber = syncNumber;
first.Value.Parent = this; first.Parent = this;
Lock.ExitWriteLock(); Lock.ExitWriteLock();
return; return;
} }
if (first.Address < address) if (first.Address < address)
{ {
first.Value.Size = address - first.Address;
Update(first);
if (first.EndAddress > endAddress) if (first.EndAddress > endAddress)
{ {
Add(new BufferModifiedRange(endAddress, first.EndAddress - endAddress, Add(new BufferModifiedRange(endAddress, first.EndAddress - endAddress,
first.Value.SyncNumber, first.Value.Parent)); first.SyncNumber, first.Parent));
} }
first.Size = address - first.Address;
} }
else else
{ {
if (first.EndAddress > endAddress) if (first.EndAddress > endAddress)
{ {
first.Value.Size = first.EndAddress - endAddress; first.Size = first.EndAddress - endAddress;
first.Value.Address = endAddress; first.Address = endAddress;
Update(first);
} }
else else
{ {
Remove(first.Value); first.Address = address;
first.Size = size;
first.SyncNumber = syncNumber;
first.Parent = this;
Lock.ExitWriteLock();
return;
} }
} }
@ -207,38 +215,39 @@ namespace Ryujinx.Graphics.Gpu.Memory
return; return;
} }
BufferModifiedRange buffPre = null;
BufferModifiedRange buffPost = null;
bool extendsPost = false;
bool extendsPre = false;
if (first.Address < address) if (first.Address < address)
{ {
buffPre = new BufferModifiedRange(first.Address, address - first.Address, first.Size = address - first.Address;
first.Value.SyncNumber, first.Value.Parent); first = first.Next;
extendsPre = true;
} }
if (last.EndAddress > endAddress) if (last.EndAddress > endAddress)
{ {
buffPost = new BufferModifiedRange(endAddress, last.EndAddress - endAddress, last.Size = last.EndAddress - endAddress;
last.Value.SyncNumber, last.Value.Parent); last.Address = endAddress;
extendsPost = true; last = last.Previous;
} }
RemoveRange(first, last); if (first.Address < last.Address)
if (extendsPre)
{ {
Add(buffPre); RemoveRange(first.Next, last);
first.Address = address;
first.Size = size;
first.SyncNumber = syncNumber;
first.Parent = this;
} }
else if (first.Address == last.Address)
if (extendsPost)
{ {
Add(buffPost); first.Address = address;
first.Size = size;
first.SyncNumber = syncNumber;
first.Parent = this;
} }
else
Add(new BufferModifiedRange(address, size, syncNumber, this)); {
Add(new BufferModifiedRange(address, size, syncNumber, this));
}
Lock.ExitWriteLock(); Lock.ExitWriteLock();
} }
@ -252,11 +261,11 @@ namespace Ryujinx.Graphics.Gpu.Memory
public void GetRangesAtSync(ulong address, ulong size, ulong syncNumber, Action<ulong, ulong> rangeAction) public void GetRangesAtSync(ulong address, ulong size, ulong syncNumber, Action<ulong, ulong> rangeAction)
{ {
Lock.EnterReadLock(); Lock.EnterReadLock();
Span<RangeItem<BufferModifiedRange>> overlaps = FindOverlapsAsSpan(address, size); ReadOnlySpan<BufferModifiedRange> overlaps = FindOverlapsAsSpan(address, size);
for (int i = 0; i < overlaps.Length; i++) for (int i = 0; i < overlaps.Length; i++)
{ {
BufferModifiedRange overlap = overlaps[i].Value; BufferModifiedRange overlap = overlaps[i];
if (overlap.SyncNumber == syncNumber) if (overlap.SyncNumber == syncNumber)
{ {
@ -277,18 +286,18 @@ namespace Ryujinx.Graphics.Gpu.Memory
{ {
// We use the non-span method here because keeping the lock will cause a deadlock. // We use the non-span method here because keeping the lock will cause a deadlock.
Lock.EnterReadLock(); Lock.EnterReadLock();
RangeItem<BufferModifiedRange>[] overlaps = FindOverlapsAsArray(address, size, out int length); BufferModifiedRange[] overlaps = FindOverlapsAsArray(address, size, out int length);
Lock.ExitReadLock(); Lock.ExitReadLock();
if (length != 0) if (length != 0)
{ {
for (int i = 0; i < length; i++) for (int i = 0; i < length; i++)
{ {
BufferModifiedRange overlap = overlaps[i].Value; BufferModifiedRange overlap = overlaps[i];
rangeAction(overlap.Address, overlap.Size); rangeAction(overlap.Address, overlap.Size);
} }
ArrayPool<RangeItem<BufferModifiedRange>>.Shared.Return(overlaps); ArrayPool<BufferModifiedRange>.Shared.Return(overlaps);
} }
} }
@ -301,7 +310,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
public bool HasRange(ulong address, ulong size) public bool HasRange(ulong address, ulong size)
{ {
Lock.EnterReadLock(); Lock.EnterReadLock();
RangeItem<BufferModifiedRange> first = FindOverlapFast(address, size); BufferModifiedRange first = FindOverlapFast(address, size);
bool result = first is not null; bool result = first is not null;
Lock.ExitReadLock(); Lock.ExitReadLock();
return result; return result;
@ -336,7 +345,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="address">The start address of the flush range</param> /// <param name="address">The start address of the flush range</param>
/// <param name="endAddress">The end address of the flush range</param> /// <param name="endAddress">The end address of the flush range</param>
private void RemoveRangesAndFlush( private void RemoveRangesAndFlush(
RangeItem<BufferModifiedRange>[] overlaps, BufferModifiedRange[] overlaps,
int rangeCount, int rangeCount,
long highestDiff, long highestDiff,
ulong currentSync, ulong currentSync,
@ -349,7 +358,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
for (int i = 0; i < rangeCount; i++) for (int i = 0; i < rangeCount; i++)
{ {
BufferModifiedRange overlap = overlaps[i].Value; BufferModifiedRange overlap = overlaps[i];
long diff = (long)(overlap.SyncNumber - currentSync); long diff = (long)(overlap.SyncNumber - currentSync);
@ -358,7 +367,14 @@ namespace Ryujinx.Graphics.Gpu.Memory
ulong clampAddress = Math.Max(address, overlap.Address); ulong clampAddress = Math.Max(address, overlap.Address);
ulong clampEnd = Math.Min(endAddress, overlap.EndAddress); ulong clampEnd = Math.Min(endAddress, overlap.EndAddress);
ClearPart(overlap, clampAddress, clampEnd); if (i == 0 || i == rangeCount - 1)
{
ClearPart(overlap, clampAddress, clampEnd);
}
else
{
Remove(overlap);
}
RangeActionWithMigration(clampAddress, clampEnd - clampAddress, waitSync, _flushAction); RangeActionWithMigration(clampAddress, clampEnd - clampAddress, waitSync, _flushAction);
} }
@ -398,7 +414,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
Lock.EnterWriteLock(); Lock.EnterWriteLock();
// We use the non-span method here because the array is partially modified by the code, which would invalidate a span. // We use the non-span method here because the array is partially modified by the code, which would invalidate a span.
RangeItem<BufferModifiedRange>[] overlaps = FindOverlapsAsArray(address, size, out int rangeCount); BufferModifiedRange[] overlaps = FindOverlapsAsArray(address, size, out int rangeCount);
if (rangeCount == 0) if (rangeCount == 0)
{ {
@ -414,7 +430,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
for (int i = 0; i < rangeCount; i++) for (int i = 0; i < rangeCount; i++)
{ {
BufferModifiedRange overlap = overlaps![i].Value; BufferModifiedRange overlap = overlaps![i];
long diff = (long)(overlap.SyncNumber - currentSync); long diff = (long)(overlap.SyncNumber - currentSync);
@ -436,7 +452,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
RemoveRangesAndFlush(overlaps, rangeCount, highestDiff, currentSync, address, endAddress); RemoveRangesAndFlush(overlaps, rangeCount, highestDiff, currentSync, address, endAddress);
ArrayPool<RangeItem<BufferModifiedRange>>.Shared.Return(overlaps!); ArrayPool<BufferModifiedRange>.Shared.Return(overlaps!);
Lock.ExitWriteLock(); Lock.ExitWriteLock();
} }
@ -452,7 +468,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
public void InheritRanges(BufferModifiedRangeList ranges, Action<ulong, ulong> registerRangeAction) public void InheritRanges(BufferModifiedRangeList ranges, Action<ulong, ulong> registerRangeAction)
{ {
ranges.Lock.EnterReadLock(); ranges.Lock.EnterReadLock();
BufferModifiedRange[] inheritRanges = ranges.ToArray(); int rangesCount = ranges.Count;
BufferModifiedRange[] inheritRanges = ArrayPool<BufferModifiedRange>.Shared.Rent(ranges.Count);
ranges.Items.AsSpan(0, ranges.Count).CopyTo(inheritRanges);
ranges.Lock.ExitReadLock(); ranges.Lock.ExitReadLock();
// Copy over the migration from the previous range list // Copy over the migration from the previous range list
@ -478,22 +496,26 @@ namespace Ryujinx.Graphics.Gpu.Memory
ranges._migrationTarget = this; ranges._migrationTarget = this;
Lock.EnterWriteLock(); Lock.EnterWriteLock();
foreach (BufferModifiedRange range in inheritRanges) for (int i = 0; i < rangesCount; i++)
{ {
BufferModifiedRange range = inheritRanges[i];
Add(range); Add(range);
} }
Lock.ExitWriteLock(); Lock.ExitWriteLock();
ulong currentSync = _context.SyncNumber; ulong currentSync = _context.SyncNumber;
foreach (BufferModifiedRange range in inheritRanges) for (int i = 0; i < rangesCount; i++)
{ {
BufferModifiedRange range = inheritRanges[i];
if (range.SyncNumber != currentSync) if (range.SyncNumber != currentSync)
{ {
registerRangeAction(range.Address, range.Size); registerRangeAction(range.Address, range.Size);
} }
} }
ArrayPool<BufferModifiedRange>.Shared.Return(inheritRanges);
} }
/// <summary> /// <summary>
@ -534,18 +556,25 @@ namespace Ryujinx.Graphics.Gpu.Memory
private void ClearPart(BufferModifiedRange overlap, ulong address, ulong endAddress) private void ClearPart(BufferModifiedRange overlap, ulong address, ulong endAddress)
{ {
Remove(overlap);
// If the overlap extends outside of the clear range, make sure those parts still exist. // If the overlap extends outside of the clear range, make sure those parts still exist.
if (overlap.Address < address) if (overlap.Address < address)
{ {
Add(new BufferModifiedRange(overlap.Address, address - overlap.Address, overlap.SyncNumber, overlap.Parent)); if (overlap.EndAddress > endAddress)
{
Add(new BufferModifiedRange(endAddress, overlap.EndAddress - endAddress, overlap.SyncNumber, overlap.Parent));
}
overlap.Size = address - overlap.Address;
} }
else if (overlap.EndAddress > endAddress)
if (overlap.EndAddress > endAddress)
{ {
Add(new BufferModifiedRange(endAddress, overlap.EndAddress - endAddress, overlap.SyncNumber, overlap.Parent)); overlap.Size = overlap.EndAddress - endAddress;
overlap.Address = endAddress;
}
else
{
Remove(overlap);
} }
} }
@ -558,7 +587,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
{ {
ulong endAddress = address + size; ulong endAddress = address + size;
Lock.EnterWriteLock(); Lock.EnterWriteLock();
(RangeItem<BufferModifiedRange> first, RangeItem<BufferModifiedRange> last) = FindOverlapsAsNodes(address, size); (BufferModifiedRange first, BufferModifiedRange last) = FindOverlapsAsNodes(address, size);
if (first is null) if (first is null)
{ {
@ -570,26 +599,24 @@ namespace Ryujinx.Graphics.Gpu.Memory
{ {
if (first.Address < address) if (first.Address < address)
{ {
first.Value.Size = address - first.Address; first.Size = address - first.Address;
Update(first);
if (first.EndAddress > endAddress) if (first.EndAddress > endAddress)
{ {
Add(new BufferModifiedRange(endAddress, first.EndAddress - endAddress, Add(new BufferModifiedRange(endAddress, first.EndAddress - endAddress,
first.Value.SyncNumber, first.Value.Parent)); first.SyncNumber, first.Parent));
} }
} }
else else
{ {
if (first.EndAddress > endAddress) if (first.EndAddress > endAddress)
{ {
first.Value.Size = first.EndAddress - endAddress; first.Size = first.EndAddress - endAddress;
first.Value.Address = endAddress; first.Address = endAddress;
Update(first);
} }
else else
{ {
Remove(first.Value); Remove(first);
} }
} }
@ -605,14 +632,14 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (first.Address < address) if (first.Address < address)
{ {
buffPre = new BufferModifiedRange(first.Address, address - first.Address, buffPre = new BufferModifiedRange(first.Address, address - first.Address,
first.Value.SyncNumber, first.Value.Parent); first.SyncNumber, first.Parent);
extendsPre = true; extendsPre = true;
} }
if (last.EndAddress > endAddress) if (last.EndAddress > endAddress)
{ {
buffPost = new BufferModifiedRange(endAddress, last.EndAddress - endAddress, buffPost = new BufferModifiedRange(endAddress, last.EndAddress - endAddress,
last.Value.SyncNumber, last.Value.Parent); last.SyncNumber, last.Parent);
extendsPost = true; extendsPost = true;
} }

View file

@ -15,7 +15,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <summary> /// <summary>
/// Represents a GPU virtual memory range. /// Represents a GPU virtual memory range.
/// </summary> /// </summary>
private class VirtualRange : INonOverlappingRange private class VirtualRange : INonOverlappingRange<VirtualRange>
{ {
/// <summary> /// <summary>
/// GPU virtual address where the range starts. /// GPU virtual address where the range starts.
@ -32,6 +32,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// </summary> /// </summary>
public ulong EndAddress => Address + Size; public ulong EndAddress => Address + Size;
public VirtualRange Next { get; set; }
public VirtualRange Previous { get; set; }
/// <summary> /// <summary>
/// Physical regions where the GPU virtual region is mapped. /// Physical regions where the GPU virtual region is mapped.
/// </summary> /// </summary>
@ -54,14 +57,14 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// Checks if a given range overlaps with the buffer. /// Checks if a given range overlaps with the buffer.
/// </summary> /// </summary>
/// <param name="address">Start address of the range</param> /// <param name="address">Start address of the range</param>
/// <param name="size">Size in bytes of the range</param> /// <param name="endAddress">End address of the range</param>
/// <returns>True if the range overlaps, false otherwise</returns> /// <returns>True if the range overlaps, false otherwise</returns>
public bool OverlapsWith(ulong address, ulong size) public bool OverlapsWith(ulong address, ulong endAddress)
{ {
return Address < address + size && address < EndAddress; return Address < endAddress && address < EndAddress;
} }
public INonOverlappingRange Split(ulong splitAddress) public INonOverlappingRange<VirtualRange> Split(ulong splitAddress)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@ -122,7 +125,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
ulong originalVa = gpuVa; ulong originalVa = gpuVa;
_virtualRanges.Lock.EnterWriteLock(); _virtualRanges.Lock.EnterWriteLock();
(RangeItem<VirtualRange> first, RangeItem<VirtualRange> last) = _virtualRanges.FindOverlapsAsNodes(gpuVa, size); (VirtualRange first, VirtualRange last) = _virtualRanges.FindOverlapsAsNodes(gpuVa, size);
if (first is not null) if (first is not null)
{ {
@ -147,8 +150,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
} }
else else
{ {
found = first.Value.Range.Count == 1 || IsSparseAligned(first.Value.Range); found = first.Range.Count == 1 || IsSparseAligned(first.Range);
range = first.Value.Range.Slice(gpuVa - first.Address, size); range = first.Range.Slice(gpuVa - first.Address, size);
} }
} }
else else

View file

@ -17,6 +17,6 @@ namespace Ryujinx.Graphics.Gpu.Synchronization
/// Action to be performed immediately before sync is created. /// Action to be performed immediately before sync is created.
/// </summary> /// </summary>
/// <param name="syncpoint">True if the action is a guest syncpoint</param> /// <param name="syncpoint">True if the action is a guest syncpoint</param>
void SyncPreAction(bool syncpoint) { } bool SyncPreAction(bool syncpoint) { return true; }
} }
} }

View file

@ -1166,7 +1166,7 @@ namespace Ryujinx.Graphics.OpenGL
} }
} }
public void SetRenderTargets(ITexture[] colors, ITexture depthStencil) public void SetRenderTargets(Span<ITexture> colors, ITexture depthStencil)
{ {
EnsureFramebuffer(); EnsureFramebuffer();

View file

@ -71,17 +71,31 @@ namespace Ryujinx.Graphics.Vulkan
HasDepthStencil = isDepthStencil; HasDepthStencil = isDepthStencil;
} }
public FramebufferParams(Device device, ITexture[] colors, ITexture depthStencil) public FramebufferParams(Device device, ReadOnlySpan<ITexture> colors, ITexture depthStencil)
{ {
_device = device; _device = device;
int colorsCount = colors.Count(IsValidTextureView); int colorsCount = 0;
_colorsCanonical = new TextureView[colors.Length];
for (int i = 0; i < colors.Length; i++)
{
ITexture color = colors[i];
if (color is TextureView { Valid: true } view)
{
colorsCount++;
_colorsCanonical[i] = view;
}
else
{
_colorsCanonical[i] = null;
}
}
int count = colorsCount + (IsValidTextureView(depthStencil) ? 1 : 0); int count = colorsCount + (IsValidTextureView(depthStencil) ? 1 : 0);
_attachments = new Auto<DisposableImageView>[count]; _attachments = new Auto<DisposableImageView>[count];
_colors = new TextureView[colorsCount]; _colors = new TextureView[colorsCount];
_colorsCanonical = colors.Select(color => color is TextureView view && view.Valid ? view : null).ToArray();
AttachmentSamples = new uint[count]; AttachmentSamples = new uint[count];
AttachmentFormats = new VkFormat[count]; AttachmentFormats = new VkFormat[count];
@ -165,9 +179,17 @@ namespace Ryujinx.Graphics.Vulkan
_totalCount = colors.Length; _totalCount = colors.Length;
} }
public FramebufferParams Update(ITexture[] colors, ITexture depthStencil) public FramebufferParams Update(ReadOnlySpan<ITexture> colors, ITexture depthStencil)
{ {
int colorsCount = colors.Count(IsValidTextureView); int colorsCount = 0;
foreach (ITexture color in colors)
{
if (IsValidTextureView(color))
{
colorsCount++;
}
}
int count = colorsCount + (IsValidTextureView(depthStencil) ? 1 : 0); int count = colorsCount + (IsValidTextureView(depthStencil) ? 1 : 0);

View file

@ -1,7 +1,7 @@
using Ryujinx.Common;
using Ryujinx.Common.Memory; using Ryujinx.Common.Memory;
using Silk.NET.Vulkan; using Silk.NET.Vulkan;
using System; using System;
using System.Buffers;
namespace Ryujinx.Graphics.Vulkan namespace Ryujinx.Graphics.Vulkan
{ {
@ -10,6 +10,8 @@ namespace Ryujinx.Graphics.Vulkan
/// </summary> /// </summary>
class MultiFenceHolder class MultiFenceHolder
{ {
public static readonly ObjectPool<FenceHolder[]> FencePool = new(() => new FenceHolder[CommandBufferPool.MaxCommandBuffers]);
private const int BufferUsageTrackingGranularity = 4096; private const int BufferUsageTrackingGranularity = 4096;
public FenceHolder[] Fences { get; } public FenceHolder[] Fences { get; }
@ -20,7 +22,7 @@ namespace Ryujinx.Graphics.Vulkan
/// </summary> /// </summary>
public MultiFenceHolder() public MultiFenceHolder()
{ {
Fences = ArrayPool<FenceHolder>.Shared.Rent(CommandBufferPool.MaxCommandBuffers); Fences = FencePool.Allocate();
} }
/// <summary> /// <summary>
@ -29,7 +31,7 @@ namespace Ryujinx.Graphics.Vulkan
/// <param name="size">Size of the buffer</param> /// <param name="size">Size of the buffer</param>
public MultiFenceHolder(int size) public MultiFenceHolder(int size)
{ {
Fences = ArrayPool<FenceHolder>.Shared.Rent(CommandBufferPool.MaxCommandBuffers); Fences = FencePool.Allocate();
_bufferUsageBitmap = new BufferUsageBitmap(size, BufferUsageTrackingGranularity); _bufferUsageBitmap = new BufferUsageBitmap(size, BufferUsageTrackingGranularity);
} }

View file

@ -1035,7 +1035,7 @@ namespace Ryujinx.Graphics.Vulkan
} }
} }
private void SetRenderTargetsInternal(ITexture[] colors, ITexture depthStencil, bool filterWriteMasked) private void SetRenderTargetsInternal(Span<ITexture> colors, ITexture depthStencil, bool filterWriteMasked)
{ {
CreateFramebuffer(colors, depthStencil, filterWriteMasked); CreateFramebuffer(colors, depthStencil, filterWriteMasked);
CreateRenderPass(); CreateRenderPass();
@ -1043,7 +1043,7 @@ namespace Ryujinx.Graphics.Vulkan
SignalAttachmentChange(); SignalAttachmentChange();
} }
public void SetRenderTargets(ITexture[] colors, ITexture depthStencil) public void SetRenderTargets(Span<ITexture> colors, ITexture depthStencil)
{ {
_framebufferUsingColorWriteMask = false; _framebufferUsingColorWriteMask = false;
SetRenderTargetsInternal(colors, depthStencil, Gd.IsTBDR); SetRenderTargetsInternal(colors, depthStencil, Gd.IsTBDR);
@ -1389,7 +1389,7 @@ namespace Ryujinx.Graphics.Vulkan
_currentPipelineHandle = 0; _currentPipelineHandle = 0;
} }
private void CreateFramebuffer(ITexture[] colors, ITexture depthStencil, bool filterWriteMasked) private void CreateFramebuffer(Span<ITexture> colors, ITexture depthStencil, bool filterWriteMasked)
{ {
if (filterWriteMasked) if (filterWriteMasked)
{ {
@ -1399,7 +1399,7 @@ namespace Ryujinx.Graphics.Vulkan
// Just try to remove duplicate attachments. // Just try to remove duplicate attachments.
// Save a copy of the array to rebind when mask changes. // Save a copy of the array to rebind when mask changes.
void MaskOut() void MaskOut(ReadOnlySpan<ITexture> colors)
{ {
if (!_framebufferUsingColorWriteMask) if (!_framebufferUsingColorWriteMask)
{ {
@ -1436,12 +1436,12 @@ namespace Ryujinx.Graphics.Vulkan
if (vkBlend.ColorWriteMask == 0) if (vkBlend.ColorWriteMask == 0)
{ {
colors[i] = null; colors[i] = null;
MaskOut(); MaskOut(colors);
} }
else if (vkBlend2.ColorWriteMask == 0) else if (vkBlend2.ColorWriteMask == 0)
{ {
colors[j] = null; colors[j] = null;
MaskOut(); MaskOut(colors);
} }
} }
} }

View file

@ -1,6 +1,6 @@
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Silk.NET.Vulkan; using Silk.NET.Vulkan;
using System.Buffers; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@ -193,7 +193,8 @@ namespace Ryujinx.Graphics.Vulkan
{ {
_firstHandle = first.ID + 1; _firstHandle = first.ID + 1;
_handles.RemoveAt(0); _handles.RemoveAt(0);
ArrayPool<FenceHolder>.Shared.Return(first.Waitable.Fences); Array.Clear(first.Waitable.Fences);
MultiFenceHolder.FencePool.Release(first.Waitable.Fences);
first.Waitable = null; first.Waitable = null;
} }
} }

View file

@ -1,43 +1,91 @@
using Gommon; using Gommon;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Kernel.Memory;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Text;
namespace Ryujinx.HLE.Debugger namespace Ryujinx.HLE.Debugger
{ {
public partial class Debugger public partial class Debugger
{ {
private sealed record RcmdEntry(string[] Names, Func<Debugger, string, string> Handler, string[] HelpLines);
// Atmosphere/libraries/libmesosphere/source/kern_k_memory_block_manager.cpp
private static readonly string[] _memoryStateNames =
{
"----- Free -----",
"Io ",
"Static ",
"Code ",
"CodeData ",
"Normal ",
"Shared ",
"Alias ",
"AliasCode ",
"AliasCodeData ",
"Ipc ",
"Stack ",
"ThreadLocal ",
"Transfered ",
"SharedTransfered",
"SharedCode ",
"Inaccessible ",
"NonSecureIpc ",
"NonDeviceIpc ",
"Kernel ",
"GeneratedCode ",
"CodeOut ",
"Coverage ",
};
static Debugger() static Debugger()
{ {
_rcmdDelegates.Add(["help"], _rcmdDelegates.Add(new RcmdEntry(
_ => _rcmdDelegates.Keys ["help"],
.Where(x => !x[0].Equals("help")) (dbgr, _) => _rcmdDelegates
.Select(x => x.JoinToString('\n')) .Where(entry => entry.HelpLines.Length > 0)
.JoinToString('\n') + '\n' .SelectMany(entry => entry.HelpLines)
); .JoinToString('\n') + '\n',
_rcmdDelegates.Add(["get info"], dbgr => dbgr.GetProcessInfo()); Array.Empty<string>()));
_rcmdDelegates.Add(["backtrace", "bt"], dbgr => dbgr.GetStackTrace());
_rcmdDelegates.Add(["registers", "reg"], dbgr => dbgr.GetRegisters()); _rcmdDelegates.Add(new RcmdEntry(["get info"], (dbgr, _) => dbgr.GetProcessInfo(), ["get info"]));
_rcmdDelegates.Add(["minidump"], dbgr => dbgr.GetMinidump()); _rcmdDelegates.Add(new RcmdEntry(["backtrace", "bt"], (dbgr, _) => dbgr.GetStackTrace(), ["backtrace", "bt"]));
_rcmdDelegates.Add(new RcmdEntry(["registers", "reg"], (dbgr, _) => dbgr.GetRegisters(), ["registers", "reg"]));
_rcmdDelegates.Add(new RcmdEntry(["minidump"], (dbgr, _) => dbgr.GetMinidump(), ["minidump"]));
_rcmdDelegates.Add(new RcmdEntry(["get mappings"], (dbgr, args) => dbgr.GetMemoryMappings(args), ["get mappings", "get mappings {address}"]));
_rcmdDelegates.Add(new RcmdEntry(["get mapping"], (dbgr, args) => dbgr.GetMemoryMapping(args), ["get mapping {address}"]));
} }
private static readonly Dictionary<string[], Func<Debugger, string>> _rcmdDelegates = new(); private static readonly List<RcmdEntry> _rcmdDelegates = [];
public static Func<Debugger, string> FindRcmdDelegate(string command) public static string CallRcmdDelegate(Debugger debugger, string command)
{ {
Func<Debugger, string> searchResult = _ => $"Unknown command: {command}\n"; string originalCommand = command ?? string.Empty;
string trimmedCommand = originalCommand.Trim();
foreach ((string[] names, Func<Debugger, string> dlg) in _rcmdDelegates) foreach (RcmdEntry entry in _rcmdDelegates)
{ {
if (names.ContainsIgnoreCase(command.Trim())) foreach (string name in entry.Names)
{ {
searchResult = dlg; if (trimmedCommand.Equals(name, StringComparison.OrdinalIgnoreCase))
break; {
return entry.Handler(debugger, string.Empty);
}
if (trimmedCommand.Length > name.Length &&
trimmedCommand.StartsWith(name, StringComparison.OrdinalIgnoreCase) &&
char.IsWhiteSpace(trimmedCommand[name.Length]))
{
string arguments = trimmedCommand[name.Length..].TrimStart();
return entry.Handler(debugger, arguments);
}
} }
} }
return searchResult; return $"Unknown command: {originalCommand}\n";
} }
public string GetStackTrace() public string GetStackTrace()
@ -86,5 +134,181 @@ namespace Ryujinx.HLE.Debugger
return $"Error getting process info: {e.Message}\n"; return $"Error getting process info: {e.Message}\n";
} }
} }
public string GetMemoryMappings(string arguments)
{
if (Process?.MemoryManager is not { } memoryManager)
{
return "No application process found\n";
}
string trimmedArgs = arguments?.Trim() ?? string.Empty;
ulong startAddress = 0;
if (!string.IsNullOrEmpty(trimmedArgs))
{
if (!TryParseAddressArgument(trimmedArgs, out startAddress))
{
return $"Invalid address: {trimmedArgs}\n";
}
}
ulong requestedAddress = startAddress;
ulong currentAddress = Math.Max(requestedAddress, memoryManager.AddrSpaceStart);
StringBuilder sb = new();
sb.AppendLine($"Mappings (starting from 0x{requestedAddress:x10}):");
if (currentAddress >= memoryManager.AddrSpaceEnd)
{
return sb.ToString();
}
while (currentAddress < memoryManager.AddrSpaceEnd)
{
KMemoryInfo info = memoryManager.QueryMemory(currentAddress);
try
{
if (info.Size == 0 || info.Address >= memoryManager.AddrSpaceEnd)
{
break;
}
sb.AppendLine(FormatMapping(info, indent: true));
if (info.Address > ulong.MaxValue - info.Size)
{
break;
}
ulong nextAddress = info.Address + info.Size;
if (nextAddress <= currentAddress)
{
break;
}
currentAddress = nextAddress;
}
finally
{
KMemoryInfo.Pool.Release(info);
}
}
return sb.ToString();
}
public string GetMemoryMapping(string arguments)
{
if (Process?.MemoryManager is not { } memoryManager)
{
return "No application process found\n";
}
string trimmedArgs = arguments?.Trim() ?? string.Empty;
if (string.IsNullOrEmpty(trimmedArgs))
{
return "Missing address argument for `get mapping`\n";
}
if (!TryParseAddressArgument(trimmedArgs, out ulong address))
{
return $"Invalid address: {trimmedArgs}\n";
}
KMemoryInfo info = memoryManager.QueryMemory(address);
try
{
return FormatMapping(info, indent: false) + '\n';
}
finally
{
KMemoryInfo.Pool.Release(info);
}
}
private static string FormatMapping(KMemoryInfo info, bool indent)
{
ulong endAddress;
if (info.Size == 0)
{
endAddress = info.Address;
}
else if (info.Address > ulong.MaxValue - (info.Size - 1))
{
endAddress = ulong.MaxValue;
}
else
{
endAddress = info.Address + info.Size - 1;
}
string prefix = indent ? " " : string.Empty;
return $"{prefix}0x{info.Address:x10} - 0x{endAddress:x10} {GetPermissionString(info)} {GetMemoryStateName(info.State)} {GetAttributeFlags(info)} [{info.IpcRefCount}, {info.DeviceRefCount}]";
}
private static string GetPermissionString(KMemoryInfo info)
{
if ((info.State & MemoryState.UserMask) == MemoryState.Unmapped)
{
return " ";
}
return info.Permission switch
{
KMemoryPermission.ReadAndExecute => "r-x",
KMemoryPermission.Read => "r--",
KMemoryPermission.ReadAndWrite => "rw-",
_ => "---"
};
}
private static string GetMemoryStateName(MemoryState state)
{
int stateIndex = (int)(state & MemoryState.UserMask);
if ((uint)stateIndex < _memoryStateNames.Length)
{
return _memoryStateNames[stateIndex];
}
return "Unknown ";
}
private static bool TryParseAddressArgument(string text, out ulong value)
{
value = 0;
if (string.IsNullOrWhiteSpace(text))
{
return false;
}
string trimmed = text.Trim();
if (trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
trimmed = trimmed[2..];
}
if (trimmed.Length == 0)
{
return false;
}
return ulong.TryParse(trimmed, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out value);
}
private static string GetAttributeFlags(KMemoryInfo info)
{
char locked = info.Attribute.HasFlag(MemoryAttribute.Borrowed) ? 'L' : '-';
char ipc = info.Attribute.HasFlag(MemoryAttribute.IpcMapped) ? 'I' : '-';
char device = info.Attribute.HasFlag(MemoryAttribute.DeviceMapped) ? 'D' : '-';
char uncached = info.Attribute.HasFlag(MemoryAttribute.Uncached) ? 'U' : '-';
return $"{locked}{ipc}{device}{uncached}";
}
} }
} }

View file

@ -404,9 +404,8 @@ namespace Ryujinx.HLE.Debugger.Gdb
string command = Helpers.FromHex(hexCommand); string command = Helpers.FromHex(hexCommand);
Logger.Debug?.Print(LogClass.GdbStub, $"Received Rcmd: {command}"); Logger.Debug?.Print(LogClass.GdbStub, $"Received Rcmd: {command}");
Func<Debugger, string> rcmd = Debugger.FindRcmdDelegate(command); string response = Debugger.CallRcmdDelegate(Debugger, command);
Processor.ReplyHex(response);
Processor.ReplyHex(rcmd(Debugger));
} }
catch (Exception e) catch (Exception e)
{ {

View file

@ -483,10 +483,29 @@ namespace Ryujinx.HLE.FileSystem
{ {
if (Directory.Exists(keysSource)) if (Directory.Exists(keysSource))
{ {
foreach (string filePath in Directory.EnumerateFiles(keysSource, "*.keys")) string[] keyPaths = Directory.EnumerateFiles(keysSource, "*.keys").ToArray();
if (keyPaths.Length is 0)
throw new FileNotFoundException($"Directory '{keysSource}' contained no '.keys' files.");
foreach (string filePath in keyPaths)
{ {
VerifyKeysFile(filePath); try
File.Copy(filePath, Path.Combine(installDirectory, Path.GetFileName(filePath)), true); {
VerifyKeysFile(filePath);
}
catch (Exception e)
{
Logger.Error?.Print(LogClass.Application, e.Message);
continue;
}
string destPath = Path.Combine(installDirectory, Path.GetFileName(filePath));
if (File.Exists(destPath))
File.Delete(destPath);
File.Copy(filePath, destPath, true);
} }
return; return;
@ -501,13 +520,25 @@ namespace Ryujinx.HLE.FileSystem
using FileStream file = File.OpenRead(keysSource); using FileStream file = File.OpenRead(keysSource);
if (info.Extension is ".keys") if (info.Extension is not ".keys")
throw new InvalidFirmwarePackageException("Input file extension is not .keys");
try
{ {
VerifyKeysFile(keysSource); VerifyKeysFile(keysSource);
File.Copy(keysSource, Path.Combine(installDirectory, info.Name), true); }
} catch
else {
throw new InvalidFirmwarePackageException("Input file is not a valid key package"); throw new InvalidFirmwarePackageException("Input file is not a valid key package");
}
string dest = Path.Combine(installDirectory, info.Name);
if (File.Exists(dest))
File.Delete(dest);
// overwrite: true seems to not work on its own? https://github.com/Ryubing/Issues/issues/189
File.Copy(keysSource, dest, true);
} }
private void FinishInstallation(string temporaryDirectory, string registeredDirectory) private void FinishInstallation(string temporaryDirectory, string registeredDirectory)
@ -985,8 +1016,8 @@ namespace Ryujinx.HLE.FileSystem
public static void VerifyKeysFile(string filePath) public static void VerifyKeysFile(string filePath)
{ {
// Verify the keys file format refers to https://github.com/Thealexbarney/LibHac/blob/master/KEYS.md // Verify the keys file format refers to https://github.com/Thealexbarney/LibHac/blob/master/KEYS.md
string genericPattern = @"^[a-z0-9_]+ = [a-z0-9]+$"; string genericPattern = "^[a-z0-9_]+ = [a-z0-9]+$";
string titlePattern = @"^[a-z0-9]{32} = [a-z0-9]{32}$"; string titlePattern = "^[a-z0-9]{32} = [a-z0-9]{32}$";
if (File.Exists(filePath)) if (File.Exists(filePath))
{ {
@ -994,24 +1025,13 @@ namespace Ryujinx.HLE.FileSystem
string fileName = Path.GetFileName(filePath); string fileName = Path.GetFileName(filePath);
string[] lines = File.ReadAllLines(filePath); string[] lines = File.ReadAllLines(filePath);
bool verified; bool verified = fileName switch
switch (fileName)
{ {
case "prod.keys": "prod.keys" or "console.keys" or "dev.keys" => VerifyKeys(lines, genericPattern),
verified = VerifyKeys(lines, genericPattern); "title.keys" => VerifyKeys(lines, titlePattern),
break; _ => throw new FormatException(
case "title.keys": $"Keys file name \"{fileName}\" not supported. Only \"prod.keys\", \"title.keys\", \"console.keys\", \"dev.keys\" are supported.")
verified = VerifyKeys(lines, titlePattern); };
break;
case "console.keys":
verified = VerifyKeys(lines, genericPattern);
break;
case "dev.keys":
verified = VerifyKeys(lines, genericPattern);
break;
default:
throw new FormatException($"Keys file name \"{fileName}\" not supported. Only \"prod.keys\", \"title.keys\", \"console.keys\", \"dev.keys\" are supported.");
}
if (!verified) if (!verified)
{ {

View file

@ -2,6 +2,7 @@ using Microsoft.IO;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Memory; using Ryujinx.Common.Memory;
using System; using System;
using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
@ -37,7 +38,7 @@ namespace Ryujinx.HLE.HOS.Ipc
public IpcMessage(ReadOnlySpan<byte> data, long cmdPtr) public IpcMessage(ReadOnlySpan<byte> data, long cmdPtr)
{ {
using RecyclableMemoryStream ms = MemoryStreamManager.Shared.GetStream(data); RecyclableMemoryStream ms = MemoryStreamManager.Shared.GetStream(data);
BinaryReader reader = new(ms); BinaryReader reader = new(ms);
@ -123,6 +124,8 @@ namespace Ryujinx.HLE.HOS.Ipc
} }
ObjectIds = []; ObjectIds = [];
MemoryStreamManager.Shared.ReleaseStream(ms);
} }
public RecyclableMemoryStream GetStream(long cmdPtr, ulong recvListAddr) public RecyclableMemoryStream GetStream(long cmdPtr, ulong recvListAddr)

View file

@ -20,6 +20,15 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
_exchangeBufferDescriptors = new List<KBufferDescriptor>(MaxInternalBuffersCount); _exchangeBufferDescriptors = new List<KBufferDescriptor>(MaxInternalBuffersCount);
} }
public KBufferDescriptorTable Clear()
{
_sendBufferDescriptors.Clear();
_receiveBufferDescriptors.Clear();
_exchangeBufferDescriptors.Clear();
return this;
}
public Result AddSendBuffer(ulong src, ulong dst, ulong size, MemoryState state) public Result AddSendBuffer(ulong src, ulong dst, ulong size, MemoryState state)
{ {
return Add(_sendBufferDescriptors, src, dst, size, state); return Add(_sendBufferDescriptors, src, dst, size, state);

View file

@ -1,3 +1,4 @@
using Ryujinx.Common;
using Ryujinx.HLE.HOS.Kernel.Common; using Ryujinx.HLE.HOS.Kernel.Common;
using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.HLE.HOS.Kernel.Process;
using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.HOS.Kernel.Threading;
@ -32,7 +33,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
{ {
KThread currentThread = KernelStatic.GetCurrentThread(); KThread currentThread = KernelStatic.GetCurrentThread();
KSessionRequest request = new(currentThread, customCmdBuffAddr, customCmdBuffSize); KSessionRequest request = _parent.ServerSession.RequestPool.Allocate().Set(currentThread, customCmdBuffAddr, customCmdBuffSize);
KernelContext.CriticalSection.Enter(); KernelContext.CriticalSection.Enter();
@ -55,7 +56,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
{ {
KThread currentThread = KernelStatic.GetCurrentThread(); KThread currentThread = KernelStatic.GetCurrentThread();
KSessionRequest request = new(currentThread, customCmdBuffAddr, customCmdBuffSize, asyncEvent); KSessionRequest request = _parent.ServerSession.RequestPool.Allocate().Set(currentThread, customCmdBuffAddr, customCmdBuffSize, asyncEvent);
KernelContext.CriticalSection.Enter(); KernelContext.CriticalSection.Enter();

View file

@ -10,6 +10,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
{ {
class KServerSession : KSynchronizationObject class KServerSession : KSynchronizationObject
{ {
public readonly ObjectPool<KSessionRequest> RequestPool = new(() => new KSessionRequest());
private static readonly MemoryState[] _ipcMemoryStates = private static readonly MemoryState[] _ipcMemoryStates =
[ [
MemoryState.IpcBuffer3, MemoryState.IpcBuffer3,
@ -274,6 +276,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
KernelContext.CriticalSection.Leave(); KernelContext.CriticalSection.Leave();
WakeClientThread(request, clientResult); WakeClientThread(request, clientResult);
RequestPool.Release(request);
} }
if (clientHeader.ReceiveListType < 2 && if (clientHeader.ReceiveListType < 2 &&
@ -627,6 +631,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
CloseAllHandles(clientMsg, serverHeader, clientProcess); CloseAllHandles(clientMsg, serverHeader, clientProcess);
FinishRequest(request, clientResult); FinishRequest(request, clientResult);
RequestPool.Release(request);
} }
if (clientHeader.ReceiveListType < 2 && if (clientHeader.ReceiveListType < 2 &&
@ -865,6 +871,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
// Unmap buffers from server. // Unmap buffers from server.
FinishRequest(request, clientResult); FinishRequest(request, clientResult);
RequestPool.Release(request);
return serverResult; return serverResult;
} }
@ -1098,6 +1106,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
foreach (KSessionRequest request in IterateWithRemovalOfAllRequests()) foreach (KSessionRequest request in IterateWithRemovalOfAllRequests())
{ {
FinishRequest(request, KernelResult.PortRemoteClosed); FinishRequest(request, KernelResult.PortRemoteClosed);
RequestPool.Release(request);
} }
} }
@ -1117,6 +1127,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
{ {
SendResultToAsyncRequestClient(request, KernelResult.PortRemoteClosed); SendResultToAsyncRequestClient(request, KernelResult.PortRemoteClosed);
} }
RequestPool.Release(request);
} }
WakeServerThreads(KernelResult.PortRemoteClosed); WakeServerThreads(KernelResult.PortRemoteClosed);

View file

@ -5,18 +5,18 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
{ {
class KSessionRequest class KSessionRequest
{ {
public KBufferDescriptorTable BufferDescriptorTable { get; } public KBufferDescriptorTable BufferDescriptorTable { get; private set; }
public KThread ClientThread { get; } public KThread ClientThread { get; private set; }
public KProcess ServerProcess { get; set; } public KProcess ServerProcess { get; set; }
public KWritableEvent AsyncEvent { get; } public KWritableEvent AsyncEvent { get; private set; }
public ulong CustomCmdBuffAddr { get; } public ulong CustomCmdBuffAddr { get; private set; }
public ulong CustomCmdBuffSize { get; } public ulong CustomCmdBuffSize { get; private set; }
public KSessionRequest( public KSessionRequest Set(
KThread clientThread, KThread clientThread,
ulong customCmdBuffAddr, ulong customCmdBuffAddr,
ulong customCmdBuffSize, ulong customCmdBuffSize,
@ -27,7 +27,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
CustomCmdBuffSize = customCmdBuffSize; CustomCmdBuffSize = customCmdBuffSize;
AsyncEvent = asyncEvent; AsyncEvent = asyncEvent;
BufferDescriptorTable = new KBufferDescriptorTable(); BufferDescriptorTable = BufferDescriptorTable?.Clear() ?? new KBufferDescriptorTable();
return this;
} }
} }
} }

View file

@ -1,10 +1,8 @@
using Ryujinx.Common;
using Ryujinx.HLE.HOS.Kernel.Common; using Ryujinx.HLE.HOS.Kernel.Common;
using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.HLE.HOS.Kernel.Process;
using Ryujinx.Horizon.Common; using Ryujinx.Horizon.Common;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
namespace Ryujinx.HLE.HOS.Kernel.Threading namespace Ryujinx.HLE.HOS.Kernel.Threading
@ -12,12 +10,12 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
class KAddressArbiter class KAddressArbiter
{ {
private const int HasListenersMask = 0x40000000; private const int HasListenersMask = 0x40000000;
private static readonly ObjectPool<KThread[]> _threadArrayPool = new(() => []);
private readonly KernelContext _context; private readonly KernelContext _context;
private readonly List<KThread> _condVarThreads; private readonly Dictionary<ulong, List<KThread>> _condVarThreads;
private readonly List<KThread> _arbiterThreads; private readonly Dictionary<ulong, List<KThread>> _arbiterThreads;
private readonly ByDynamicPriority _byDynamicPriority;
public KAddressArbiter(KernelContext context) public KAddressArbiter(KernelContext context)
{ {
@ -25,6 +23,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
_condVarThreads = []; _condVarThreads = [];
_arbiterThreads = []; _arbiterThreads = [];
_byDynamicPriority = new ByDynamicPriority();
} }
public Result ArbitrateLock(int ownerHandle, ulong mutexAddress, int requesterHandle) public Result ArbitrateLock(int ownerHandle, ulong mutexAddress, int requesterHandle)
@ -140,9 +139,23 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
currentThread.MutexAddress = mutexAddress; currentThread.MutexAddress = mutexAddress;
currentThread.ThreadHandleForUserMutex = threadHandle; currentThread.ThreadHandleForUserMutex = threadHandle;
currentThread.CondVarAddress = condVarAddress;
_condVarThreads.Add(currentThread); if (_condVarThreads.TryGetValue(condVarAddress, out List<KThread> threads))
{
int i = 0;
if (threads.Count > 0)
{
i = threads.BinarySearch(currentThread, _byDynamicPriority);
if (i < 0) i = ~i;
}
threads.Insert(i, currentThread);
}
else
{
_condVarThreads.Add(condVarAddress, [currentThread]);
}
if (timeout != 0) if (timeout != 0)
{ {
@ -165,7 +178,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
currentThread.MutexOwner?.RemoveMutexWaiter(currentThread); currentThread.MutexOwner?.RemoveMutexWaiter(currentThread);
_condVarThreads.Remove(currentThread); _condVarThreads[condVarAddress].Remove(currentThread);
_context.CriticalSection.Leave(); _context.CriticalSection.Leave();
@ -200,13 +213,14 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
{ {
_context.CriticalSection.Enter(); _context.CriticalSection.Enter();
static bool SignalProcessWideKeyPredicate(KThread thread, ulong address) int validThreads = 0;
_condVarThreads.TryGetValue(address, out List<KThread> threads);
if (threads is not null && threads.Count > 0)
{ {
return thread.CondVarAddress == address; validThreads = WakeThreads(threads, count, TryAcquireMutex);
} }
int validThreads = WakeThreads(_condVarThreads, count, TryAcquireMutex, SignalProcessWideKeyPredicate, address);
if (validThreads == 0) if (validThreads == 0)
{ {
KernelTransfer.KernelToUser(address, 0); KernelTransfer.KernelToUser(address, 0);
@ -315,9 +329,24 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
currentThread.MutexAddress = address; currentThread.MutexAddress = address;
currentThread.WaitingInArbitration = true; currentThread.WaitingInArbitration = true;
if (_arbiterThreads.TryGetValue(address, out List<KThread> threads))
{
int i = 0;
_arbiterThreads.Add(currentThread); if (threads.Count > 0)
{
i = threads.BinarySearch(currentThread, _byDynamicPriority);
if (i < 0) i = ~i;
}
threads.Insert(i, currentThread);
}
else
{
_arbiterThreads.Add(address, [currentThread]);
}
currentThread.Reschedule(ThreadSchedState.Paused); currentThread.Reschedule(ThreadSchedState.Paused);
if (timeout > 0) if (timeout > 0)
@ -336,7 +365,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
if (currentThread.WaitingInArbitration) if (currentThread.WaitingInArbitration)
{ {
_arbiterThreads.Remove(currentThread); _arbiterThreads[address].Remove(currentThread);
currentThread.WaitingInArbitration = false; currentThread.WaitingInArbitration = false;
} }
@ -392,9 +421,24 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
currentThread.MutexAddress = address; currentThread.MutexAddress = address;
currentThread.WaitingInArbitration = true; currentThread.WaitingInArbitration = true;
if (_arbiterThreads.TryGetValue(address, out List<KThread> threads))
{
int i = 0;
_arbiterThreads.Add(currentThread); if (threads.Count > 0)
{
i = threads.BinarySearch(currentThread, _byDynamicPriority);
if (i < 0) i = ~i;
}
threads.Insert(i, currentThread);
}
else
{
_arbiterThreads.Add(address, [currentThread]);
}
currentThread.Reschedule(ThreadSchedState.Paused); currentThread.Reschedule(ThreadSchedState.Paused);
if (timeout > 0) if (timeout > 0)
@ -413,7 +457,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
if (currentThread.WaitingInArbitration) if (currentThread.WaitingInArbitration)
{ {
_arbiterThreads.Remove(currentThread); _arbiterThreads[address].Remove(currentThread);
currentThread.WaitingInArbitration = false; currentThread.WaitingInArbitration = false;
} }
@ -486,15 +530,12 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
// or equal to the Count of threads to be signaled, or Count is zero // or equal to the Count of threads to be signaled, or Count is zero
// or negative. It is incremented if there are no threads waiting. // or negative. It is incremented if there are no threads waiting.
int waitingCount = 0; int waitingCount = 0;
foreach (KThread thread in _arbiterThreads) if (_arbiterThreads.TryGetValue(address, out List<KThread> threads))
{ {
if (thread.MutexAddress == address && waitingCount = threads.Count;
++waitingCount >= count)
{
break;
}
} }
if (waitingCount > 0) if (waitingCount > 0)
{ {
@ -561,55 +602,38 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
thread.WaitingInArbitration = false; thread.WaitingInArbitration = false;
} }
static bool ArbiterThreadPredecate(KThread thread, ulong address) _arbiterThreads.TryGetValue(address, out List<KThread> threads);
{
return thread.MutexAddress == address;
}
WakeThreads(_arbiterThreads, count, RemoveArbiterThread, ArbiterThreadPredecate, address); if (threads is not null && threads.Count > 0)
{
WakeThreads(threads, count, RemoveArbiterThread);
}
} }
private static int WakeThreads( private static int WakeThreads(
List<KThread> threads, List<KThread> threads,
int count, int count,
Action<KThread> removeCallback, Action<KThread> removeCallback)
Func<KThread, ulong, bool> predicate,
ulong address = 0)
{ {
KThread[] candidates = _threadArrayPool.Allocate(); int validCount = count > 0 ? Math.Min(count, threads.Count) : threads.Count;
if (candidates.Length < threads.Count)
{
Array.Resize(ref candidates, threads.Count);
}
int validCount = 0;
for (int i = 0; i < threads.Count; i++)
{
if (predicate(threads[i], address))
{
candidates[validCount++] = threads[i];
}
}
Span<KThread> candidatesSpan = candidates.AsSpan(..validCount);
candidatesSpan.Sort((x, y) => (x.DynamicPriority.CompareTo(y.DynamicPriority)));
if (count > 0) for (int i = 0; i < validCount; i++)
{
candidatesSpan = candidatesSpan[..Math.Min(count, candidatesSpan.Length)];
}
foreach (KThread thread in candidatesSpan)
{ {
KThread thread = threads[i];
removeCallback(thread); removeCallback(thread);
threads.Remove(thread);
} }
_threadArrayPool.Release(candidates); threads.RemoveRange(0, validCount);
return validCount; return validCount;
} }
private class ByDynamicPriority : IComparer<KThread>
{
public int Compare(KThread x, KThread y)
{
return x!.DynamicPriority.CompareTo(y!.DynamicPriority);
}
}
} }
} }

View file

@ -61,8 +61,6 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
public KSynchronizationObject SignaledObj { get; set; } public KSynchronizationObject SignaledObj { get; set; }
public ulong CondVarAddress { get; set; }
private ulong _entrypoint; private ulong _entrypoint;
private ThreadStart _customThreadStart; private ThreadStart _customThreadStart;
private bool _forcedUnschedulable; private bool _forcedUnschedulable;

View file

@ -1,9 +1,8 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.HLE.HOS.Services.Account.Acc namespace Ryujinx.HLE.HOS.Services.Account.Acc
{ {
[JsonConverter(typeof(TypedStringEnumConverter<AccountState>))] [JsonConverter(typeof(JsonStringEnumConverter<AccountState>))]
public enum AccountState public enum AccountState
{ {
Closed, Closed,

View file

@ -416,7 +416,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
return ResultCode.InvalidParameters; return ResultCode.InvalidParameters;
} }
Logger.Stub?.PrintStub(LogClass.ServiceAm, new { albumReportOption }); context.Device.UIHandler.TakeScreenshot();
return ResultCode.Success; return ResultCode.Success;
} }

View file

@ -23,6 +23,9 @@ namespace Ryujinx.HLE.HOS.Services
private int _selfId; private int _selfId;
private bool _isDomain; private bool _isDomain;
// cache array so we don't recreate it all the time
private object[] _parameters = [null];
public IpcService(ServerBase server = null, bool registerTipc = false) public IpcService(ServerBase server = null, bool registerTipc = false)
{ {
Stopwatch sw = Stopwatch.StartNew(); Stopwatch sw = Stopwatch.StartNew();
@ -146,7 +149,9 @@ namespace Ryujinx.HLE.HOS.Services
{ {
Logger.Trace?.Print(LogClass.KernelIpc, $"{service.GetType().Name}: {processRequest.Name}"); Logger.Trace?.Print(LogClass.KernelIpc, $"{service.GetType().Name}: {processRequest.Name}");
result = (ResultCode)processRequest.Invoke(service, [context]); _parameters[0] = context;
result = (ResultCode)processRequest.Invoke(service, _parameters);
} }
else else
{ {
@ -196,7 +201,9 @@ namespace Ryujinx.HLE.HOS.Services
{ {
Logger.Debug?.Print(LogClass.KernelIpc, $"{GetType().Name}: {processRequest.Name}"); Logger.Debug?.Print(LogClass.KernelIpc, $"{GetType().Name}: {processRequest.Name}");
result = (ResultCode)processRequest.Invoke(this, [context]); _parameters[0] = context;
result = (ResultCode)processRequest.Invoke(this, _parameters);
} }
else else
{ {

View file

@ -59,6 +59,10 @@ namespace Ryujinx.HLE.HOS.Services.Nv
// TODO: This should call set:sys::GetDebugModeFlag // TODO: This should call set:sys::GetDebugModeFlag
private readonly bool _debugModeEnabled = false; private readonly bool _debugModeEnabled = false;
private byte[] _ioctl2Buffer = [];
private byte[] _ioctlArgumentBuffer = [];
private byte[] _ioctl3Buffer = [];
public INvDrvServices(ServiceCtx context) : base(context.Device.System.NvDrvServer) public INvDrvServices(ServiceCtx context) : base(context.Device.System.NvDrvServer)
{ {
@ -128,27 +132,38 @@ namespace Ryujinx.HLE.HOS.Services.Nv
if (!context.Memory.TryReadUnsafe(inputDataPosition, (int)inputDataSize, out arguments)) if (!context.Memory.TryReadUnsafe(inputDataPosition, (int)inputDataSize, out arguments))
{ {
arguments = new byte[inputDataSize]; if (_ioctlArgumentBuffer.Length < (int)inputDataSize)
{
Array.Resize(ref _ioctlArgumentBuffer, (int)inputDataSize);
}
arguments = _ioctlArgumentBuffer.AsSpan(0, (int)inputDataSize);
context.Memory.Read(inputDataPosition, arguments); context.Memory.Read(inputDataPosition, arguments);
} }
else
{
arguments = arguments.ToArray();
}
} }
else if (isWrite) else if (isWrite)
{ {
byte[] outputData = new byte[outputDataSize]; if (_ioctlArgumentBuffer.Length < (int)outputDataSize)
{
arguments = new Span<byte>(outputData); Array.Resize(ref _ioctlArgumentBuffer, (int)outputDataSize);
}
arguments = _ioctlArgumentBuffer.AsSpan(0, (int)outputDataSize);
} }
else else
{ {
byte[] temp = new byte[inputDataSize]; if (!context.Memory.TryReadUnsafe(inputDataPosition, (int)inputDataSize, out arguments))
{
if (_ioctlArgumentBuffer.Length < (int)inputDataSize)
{
Array.Resize(ref _ioctlArgumentBuffer, (int)inputDataSize);
}
arguments = _ioctlArgumentBuffer.AsSpan(0, (int)inputDataSize);
context.Memory.Read(inputDataPosition, temp); context.Memory.Read(inputDataPosition, arguments);
}
arguments = new Span<byte>(temp);
} }
return NvResult.Success; return NvResult.Success;
@ -270,7 +285,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv
if ((ioctlCommand.DirectionValue & NvIoctl.Direction.Write) != 0) if ((ioctlCommand.DirectionValue & NvIoctl.Direction.Write) != 0)
{ {
context.Memory.Write(context.Request.GetBufferType0x22(0).Position, arguments.ToArray()); context.Memory.Write(context.Request.GetBufferType0x22(0).Position, arguments);
} }
} }
} }
@ -474,13 +489,15 @@ namespace Ryujinx.HLE.HOS.Services.Nv
errorCode = GetIoctlArgument(context, ioctlCommand, out Span<byte> arguments); errorCode = GetIoctlArgument(context, ioctlCommand, out Span<byte> arguments);
byte[] inlineInBuffer = null;
if (!context.Memory.TryReadUnsafe(inlineInBufferPosition, (int)inlineInBufferSize, out Span<byte> inlineInBufferSpan)) if (!context.Memory.TryReadUnsafe(inlineInBufferPosition, (int)inlineInBufferSize, out Span<byte> inlineInBufferSpan))
{ {
inlineInBuffer = _byteArrayPool.Rent((int)inlineInBufferSize); if (_ioctl2Buffer.Length < (int)inlineInBufferSize)
inlineInBufferSpan = inlineInBuffer; {
context.Memory.Read(inlineInBufferPosition, inlineInBufferSpan[..(int)inlineInBufferSize]); Array.Resize(ref _ioctl2Buffer, (int)inlineInBufferSize);
}
inlineInBufferSpan = _ioctl2Buffer.AsSpan(0, (int)inlineInBufferSize);
context.Memory.Read(inlineInBufferPosition, inlineInBufferSpan);
} }
if (errorCode == NvResult.Success) if (errorCode == NvResult.Success)
@ -489,7 +506,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv
if (errorCode == NvResult.Success) if (errorCode == NvResult.Success)
{ {
NvInternalResult internalResult = deviceFile.Ioctl2(ioctlCommand, arguments, inlineInBufferSpan[..(int)inlineInBufferSize]); NvInternalResult internalResult = deviceFile.Ioctl2(ioctlCommand, arguments, inlineInBufferSpan);
if (internalResult == NvInternalResult.NotImplemented) if (internalResult == NvInternalResult.NotImplemented)
{ {
@ -500,15 +517,10 @@ namespace Ryujinx.HLE.HOS.Services.Nv
if ((ioctlCommand.DirectionValue & NvIoctl.Direction.Write) != 0) if ((ioctlCommand.DirectionValue & NvIoctl.Direction.Write) != 0)
{ {
context.Memory.Write(context.Request.GetBufferType0x22(0).Position, arguments.ToArray()); context.Memory.Write(context.Request.GetBufferType0x22(0).Position, arguments);
} }
} }
} }
if (inlineInBuffer is not null)
{
_byteArrayPool.Return(inlineInBuffer);
}
} }
context.ResponseData.Write((uint)errorCode); context.ResponseData.Write((uint)errorCode);
@ -531,13 +543,15 @@ namespace Ryujinx.HLE.HOS.Services.Nv
errorCode = GetIoctlArgument(context, ioctlCommand, out Span<byte> arguments); errorCode = GetIoctlArgument(context, ioctlCommand, out Span<byte> arguments);
byte[] inlineOutBuffer = null;
if (!context.Memory.TryReadUnsafe(inlineOutBufferPosition, (int)inlineOutBufferSize, out Span<byte> inlineOutBufferSpan)) if (!context.Memory.TryReadUnsafe(inlineOutBufferPosition, (int)inlineOutBufferSize, out Span<byte> inlineOutBufferSpan))
{ {
inlineOutBuffer = _byteArrayPool.Rent((int)inlineOutBufferSize); if (_ioctl3Buffer.Length < (int)inlineOutBufferSize)
inlineOutBufferSpan = inlineOutBuffer; {
context.Memory.Read(inlineOutBufferPosition, inlineOutBufferSpan[..(int)inlineOutBufferSize]); Array.Resize(ref _ioctl3Buffer, (int)inlineOutBufferSize);
}
inlineOutBufferSpan = _ioctl3Buffer.AsSpan(0, (int)inlineOutBufferSize);
context.Memory.Read(inlineOutBufferPosition, inlineOutBufferSpan);
} }
if (errorCode == NvResult.Success) if (errorCode == NvResult.Success)
@ -546,7 +560,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv
if (errorCode == NvResult.Success) if (errorCode == NvResult.Success)
{ {
NvInternalResult internalResult = deviceFile.Ioctl3(ioctlCommand, arguments, inlineOutBufferSpan[..(int)inlineOutBufferSize]); NvInternalResult internalResult = deviceFile.Ioctl3(ioctlCommand, arguments, inlineOutBufferSpan);
if (internalResult == NvInternalResult.NotImplemented) if (internalResult == NvInternalResult.NotImplemented)
{ {
@ -557,16 +571,11 @@ namespace Ryujinx.HLE.HOS.Services.Nv
if ((ioctlCommand.DirectionValue & NvIoctl.Direction.Write) != 0) if ((ioctlCommand.DirectionValue & NvIoctl.Direction.Write) != 0)
{ {
context.Memory.Write(context.Request.GetBufferType0x22(0).Position, arguments.ToArray()); context.Memory.Write(context.Request.GetBufferType0x22(0).Position, arguments);
context.Memory.Write(inlineOutBufferPosition, inlineOutBufferSpan[..(int)inlineOutBufferSize].ToArray()); context.Memory.Write(inlineOutBufferPosition, inlineOutBufferSpan);
} }
} }
} }
if (inlineOutBuffer is not null)
{
_byteArrayPool.Return(inlineOutBuffer);
}
} }
context.ResponseData.Write((uint)errorCode); context.ResponseData.Write((uint)errorCode);

View file

@ -454,8 +454,9 @@ namespace Ryujinx.HLE.HOS.Services
response.RawData = _responseDataStream.ToArray(); response.RawData = _responseDataStream.ToArray();
using RecyclableMemoryStream responseStream = response.GetStreamTipc(); RecyclableMemoryStream responseStream = response.GetStreamTipc();
_selfProcess.CpuMemory.Write(_selfThread.TlsAddress, responseStream.GetReadOnlySequence()); _selfProcess.CpuMemory.Write(_selfThread.TlsAddress, responseStream.GetReadOnlySequence());
MemoryStreamManager.Shared.ReleaseStream(responseStream);
} }
else else
{ {
@ -464,8 +465,9 @@ namespace Ryujinx.HLE.HOS.Services
if (!isTipcCommunication) if (!isTipcCommunication)
{ {
using RecyclableMemoryStream responseStream = response.GetStream((long)_selfThread.TlsAddress, recvListAddr | ((ulong)PointerBufferSize << 48)); RecyclableMemoryStream responseStream = response.GetStream((long)_selfThread.TlsAddress, recvListAddr | ((ulong)PointerBufferSize << 48));
_selfProcess.CpuMemory.Write(_selfThread.TlsAddress, responseStream.GetReadOnlySequence()); _selfProcess.CpuMemory.Write(_selfThread.TlsAddress, responseStream.GetReadOnlySequence());
MemoryStreamManager.Shared.ReleaseStream(responseStream);
} }
return shouldReply; return shouldReply;

View file

@ -68,5 +68,10 @@ namespace Ryujinx.HLE.UI
/// Displays the player select dialog and returns the selected profile. /// Displays the player select dialog and returns the selected profile.
/// </summary> /// </summary>
UserProfile ShowPlayerSelectDialog(); UserProfile ShowPlayerSelectDialog();
/// <summary>
/// Takes a screenshot from the current renderer and saves it in the screenshots folder.
/// </summary>
void TakeScreenshot();
} }
} }

View file

@ -19,6 +19,9 @@ namespace Ryujinx.Horizon.Sdk.OsTypes.Impl
private int _waitingThreadHandle; private int _waitingThreadHandle;
private MultiWaitHolderBase _signaledHolder; private MultiWaitHolderBase _signaledHolder;
ObjectPool<int[]> _objectHandlePool = new(() => new int[64]);
ObjectPool<MultiWaitHolderBase[]> _objectPool = new(() => new MultiWaitHolderBase[64]);
public long CurrentTime { get; private set; } public long CurrentTime { get; private set; }
@ -76,11 +79,15 @@ namespace Ryujinx.Horizon.Sdk.OsTypes.Impl
private MultiWaitHolderBase WaitAnyHandleImpl(bool infinite, long timeout) private MultiWaitHolderBase WaitAnyHandleImpl(bool infinite, long timeout)
{ {
Span<int> objectHandles = new int[64]; int[] objectHandles = _objectHandlePool.Allocate();
Span<int> objectHandlesSpan = objectHandles;
objectHandlesSpan.Clear();
Span<MultiWaitHolderBase> objects = new MultiWaitHolderBase[64]; MultiWaitHolderBase[] objects = _objectPool.Allocate();
Span<MultiWaitHolderBase> objectsSpan = objects;
objectsSpan.Clear();
int count = FillObjectsArray(objectHandles, objects); int count = FillObjectsArray(objectHandlesSpan, objectsSpan);
long endTime = infinite ? long.MaxValue : PerformanceCounter.ElapsedMilliseconds * 1000000; long endTime = infinite ? long.MaxValue : PerformanceCounter.ElapsedMilliseconds * 1000000;
@ -98,7 +105,7 @@ namespace Ryujinx.Horizon.Sdk.OsTypes.Impl
} }
else else
{ {
index = WaitSynchronization(objectHandles[..count], minTimeout); index = WaitSynchronization(objectHandlesSpan[..count], minTimeout);
DebugUtil.Assert(index != WaitInvalid); DebugUtil.Assert(index != WaitInvalid);
} }
@ -116,12 +123,18 @@ namespace Ryujinx.Horizon.Sdk.OsTypes.Impl
{ {
_signaledHolder = minTimeoutObject; _signaledHolder = minTimeoutObject;
_objectHandlePool.Release(objectHandles);
_objectPool.Release(objects);
return _signaledHolder; return _signaledHolder;
} }
} }
} }
else else
{ {
_objectHandlePool.Release(objectHandles);
_objectPool.Release(objects);
return null; return null;
} }
@ -131,6 +144,9 @@ namespace Ryujinx.Horizon.Sdk.OsTypes.Impl
{ {
if (_signaledHolder != null) if (_signaledHolder != null)
{ {
_objectHandlePool.Release(objectHandles);
_objectPool.Release(objects);
return _signaledHolder; return _signaledHolder;
} }
} }
@ -139,8 +155,11 @@ namespace Ryujinx.Horizon.Sdk.OsTypes.Impl
default: default:
lock (_lock) lock (_lock)
{ {
_signaledHolder = objects[index]; _signaledHolder = objectsSpan[index];
_objectHandlePool.Release(objectHandles);
_objectPool.Release(objects);
return _signaledHolder; return _signaledHolder;
} }
} }

View file

@ -532,8 +532,6 @@ namespace Ryujinx.Input.HLE
hidKeyboard.Modifier |= value << entry.Target; hidKeyboard.Modifier |= value << entry.Target;
} }
ArrayPool<bool>.Shared.Return(keyboardState.KeysState);
return hidKeyboard; return hidKeyboard;

View file

@ -20,7 +20,6 @@ namespace Ryujinx.Input.HLE
{ {
public class NpadManager : IDisposable public class NpadManager : IDisposable
{ {
private static readonly ObjectPool<List<SixAxisInput>> _hleMotionStatesPool = new (() => new List<SixAxisInput>(NpadDevices.MaxControllers));
private readonly CemuHookClient _cemuHookClient; private readonly CemuHookClient _cemuHookClient;
private readonly Lock _lock = new(); private readonly Lock _lock = new();
@ -40,6 +39,9 @@ namespace Ryujinx.Input.HLE
private bool _enableKeyboard; private bool _enableKeyboard;
private bool _enableMouse; private bool _enableMouse;
private Switch _device; private Switch _device;
private readonly List<GamepadInput> _hleInputStates = [];
private readonly List<SixAxisInput> _hleMotionStates = new(NpadDevices.MaxControllers);
public NpadManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver, IGamepadDriver mouseDriver) public NpadManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver, IGamepadDriver mouseDriver)
{ {
@ -217,8 +219,8 @@ namespace Ryujinx.Input.HLE
{ {
lock (_lock) lock (_lock)
{ {
List<GamepadInput> hleInputStates = []; _hleInputStates.Clear();
List<SixAxisInput> hleMotionStates = _hleMotionStatesPool.Allocate(); _hleMotionStates.Clear();
KeyboardInput? hleKeyboardInput = null; KeyboardInput? hleKeyboardInput = null;
@ -260,14 +262,14 @@ namespace Ryujinx.Input.HLE
inputState.PlayerId = playerIndex; inputState.PlayerId = playerIndex;
motionState.Item1.PlayerId = playerIndex; motionState.Item1.PlayerId = playerIndex;
hleInputStates.Add(inputState); _hleInputStates.Add(inputState);
hleMotionStates.Add(motionState.Item1); _hleMotionStates.Add(motionState.Item1);
if (isJoyconPair && !motionState.Item2.Equals(default)) if (isJoyconPair && !motionState.Item2.Equals(default))
{ {
motionState.Item2.PlayerId = playerIndex; motionState.Item2.PlayerId = playerIndex;
hleMotionStates.Add(motionState.Item2); _hleMotionStates.Add(motionState.Item2);
} }
} }
@ -276,8 +278,8 @@ namespace Ryujinx.Input.HLE
hleKeyboardInput = NpadController.GetHLEKeyboardInput(_keyboardDriver); hleKeyboardInput = NpadController.GetHLEKeyboardInput(_keyboardDriver);
} }
_device.Hid.Npads.Update(hleInputStates); _device.Hid.Npads.Update(_hleInputStates);
_device.Hid.Npads.UpdateSixAxis(hleMotionStates); _device.Hid.Npads.UpdateSixAxis(_hleMotionStates);
if (hleKeyboardInput.HasValue) if (hleKeyboardInput.HasValue)
{ {
@ -328,10 +330,7 @@ namespace Ryujinx.Input.HLE
_device.Hid.Mouse.Update(0, 0); _device.Hid.Mouse.Update(0, 0);
} }
_device.TamperMachine.UpdateInput(hleInputStates); _device.TamperMachine.UpdateInput(_hleInputStates);
hleMotionStates.Clear();
_hleMotionStatesPool.Release(hleMotionStates);
} }
} }

View file

@ -8,6 +8,8 @@ namespace Ryujinx.Input
/// </summary> /// </summary>
public interface IKeyboard : IGamepad public interface IKeyboard : IGamepad
{ {
private static bool[] _keyState;
/// <summary> /// <summary>
/// Check if a given key is pressed on the keyboard. /// Check if a given key is pressed on the keyboard.
/// </summary> /// </summary>
@ -29,15 +31,17 @@ namespace Ryujinx.Input
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
static KeyboardStateSnapshot GetStateSnapshot(IKeyboard keyboard) static KeyboardStateSnapshot GetStateSnapshot(IKeyboard keyboard)
{ {
if (_keyState is null)
{
_keyState = new bool[(int)Key.Count];
}
bool[] keysState = ArrayPool<bool>.Shared.Rent((int)Key.Count);
for (Key key = 0; key < Key.Count; key++) for (Key key = 0; key < Key.Count; key++)
{ {
keysState[(int)key] = keyboard.IsPressed(key); _keyState[(int)key] = keyboard.IsPressed(key);
} }
return new KeyboardStateSnapshot(keysState); return new KeyboardStateSnapshot(_keyState);
} }
} }
} }

View file

@ -3,7 +3,7 @@ namespace Ryujinx.Memory.Range
/// <summary> /// <summary>
/// Range of memory that can be split in two. /// Range of memory that can be split in two.
/// </summary> /// </summary>
public interface INonOverlappingRange : IRange public interface INonOverlappingRange<T> : IRangeListRange<T> where T : class, IRangeListRange<T>
{ {
/// <summary> /// <summary>
/// Split this region into two, around the specified address. /// Split this region into two, around the specified address.
@ -11,6 +11,6 @@ namespace Ryujinx.Memory.Range
/// </summary> /// </summary>
/// <param name="splitAddress">Address to split the region around</param> /// <param name="splitAddress">Address to split the region around</param>
/// <returns>The second part of the split region, with start address at the given split.</returns> /// <returns>The second part of the split region, with start address at the given split.</returns>
public INonOverlappingRange Split(ulong splitAddress); public INonOverlappingRange<T> Split(ulong splitAddress);
} }
} }

View file

@ -24,8 +24,8 @@ namespace Ryujinx.Memory.Range
/// Check if this range overlaps with another. /// Check if this range overlaps with another.
/// </summary> /// </summary>
/// <param name="address">Base address</param> /// <param name="address">Base address</param>
/// <param name="size">Size of the range</param> /// <param name="endAddress">EndAddress of the range</param>
/// <returns>True if overlapping, false otherwise</returns> /// <returns>True if overlapping, false otherwise</returns>
bool OverlapsWith(ulong address, ulong size); bool OverlapsWith(ulong address, ulong endAddress);
} }
} }

View file

@ -11,7 +11,7 @@ namespace Ryujinx.Memory.Range
/// A range list that assumes ranges are non-overlapping, with list items that can be split in two to avoid overlaps. /// A range list that assumes ranges are non-overlapping, with list items that can be split in two to avoid overlaps.
/// </summary> /// </summary>
/// <typeparam name="T">Type of the range.</typeparam> /// <typeparam name="T">Type of the range.</typeparam>
public unsafe class NonOverlappingRangeList<T> : RangeListBase<T> where T : class, INonOverlappingRange public class NonOverlappingRangeList<T> : RangeListBase<T> where T : class, INonOverlappingRange<T>
{ {
public readonly ReaderWriterLockSlim Lock = new(); public readonly ReaderWriterLockSlim Lock = new();
@ -32,83 +32,18 @@ namespace Ryujinx.Memory.Range
/// <param name="item">The item to be added</param> /// <param name="item">The item to be added</param>
public override void Add(T item) public override void Add(T item)
{ {
Debug.Assert(item.Address != item.EndAddress);
int index = BinarySearch(item.Address); int index = BinarySearch(item.Address);
if (index < 0) if (index < 0)
{ {
index = ~index; index = ~index;
} }
RangeItem<T> rangeItem = _rangeItemPool.Allocate().Set(item);
Insert(index, rangeItem);
}
/// <summary>
/// Updates an item's end address on the list. Address must be the same.
/// </summary>
/// <param name="item">The item to be updated</param>
/// <returns>True if the item was located and updated, false otherwise</returns>
protected override bool Update(T item)
{
int index = BinarySearch(item.Address);
if (index >= 0 && Items[index].Value.Equals(item))
{
RangeItem<T> rangeItem = new(item) { Previous = Items[index].Previous, Next = Items[index].Next };
if (index > 0)
{
Items[index - 1].Next = rangeItem;
}
if (index < Count - 1)
{
Items[index + 1].Previous = rangeItem;
}
Items[index] = rangeItem;
return true;
}
return false;
}
/// <summary>
/// Updates an item's end address on the list. Address must be the same.
/// </summary>
/// <param name="item">The RangeItem to be updated</param>
/// <returns>True if the item was located and updated, false otherwise</returns>
protected override bool Update(RangeItem<T> item)
{
int index = BinarySearch(item.Address);
RangeItem<T> rangeItem = new(item.Value) { Previous = item.Previous, Next = item.Next };
if (index > 0)
{
Items[index - 1].Next = rangeItem;
}
if (index < Count - 1)
{
Items[index + 1].Previous = rangeItem;
}
Items[index] = rangeItem;
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Insert(int index, RangeItem<T> item)
{
Debug.Assert(item.Address != item.EndAddress);
if (Count + 1 > Items.Length) if (Count + 1 > Items.Length)
{ {
Array.Resize(ref Items, Items.Length + BackingGrowthSize); Array.Resize(ref Items, (int)(Items.Length * 1.5));
} }
if (index >= Count) if (index >= Count)
@ -145,8 +80,6 @@ namespace Ryujinx.Memory.Range
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void RemoveAt(int index) private void RemoveAt(int index)
{ {
_rangeItemPool.Release(Items[index]);
if (index < Count - 1) if (index < Count - 1)
{ {
Items[index + 1].Previous = index > 0 ? Items[index - 1] : null; Items[index + 1].Previous = index > 0 ? Items[index - 1] : null;
@ -173,7 +106,7 @@ namespace Ryujinx.Memory.Range
{ {
int index = BinarySearch(item.Address); int index = BinarySearch(item.Address);
if (index >= 0 && Items[index].Value.Equals(item)) if (index >= 0 && Items[index] == item)
{ {
RemoveAt(index); RemoveAt(index);
@ -188,7 +121,7 @@ namespace Ryujinx.Memory.Range
/// </summary> /// </summary>
/// <param name="startItem">The first item in the range of items to be removed</param> /// <param name="startItem">The first item in the range of items to be removed</param>
/// <param name="endItem">The last item in the range of items to be removed</param> /// <param name="endItem">The last item in the range of items to be removed</param>
public override void RemoveRange(RangeItem<T> startItem, RangeItem<T> endItem) public override void RemoveRange(T startItem, T endItem)
{ {
if (startItem is null) if (startItem is null)
{ {
@ -197,7 +130,7 @@ namespace Ryujinx.Memory.Range
if (startItem == endItem) if (startItem == endItem)
{ {
Remove(startItem.Value); Remove(startItem);
return; return;
} }
@ -229,42 +162,45 @@ namespace Ryujinx.Memory.Range
/// <param name="size">Size of the range</param> /// <param name="size">Size of the range</param>
public void RemoveRange(ulong address, ulong size) public void RemoveRange(ulong address, ulong size)
{ {
int startIndex = BinarySearchLeftEdge(address, address + size); (int startIndex, int endIndex) = BinarySearchEdges(address, address + size);
if (startIndex < 0) if (startIndex < 0)
{ {
return; return;
} }
int endIndex = startIndex; if (startIndex == endIndex - 1)
while (Items[endIndex] is not null && Items[endIndex].Address < address + size)
{ {
if (endIndex == Count - 1) RemoveAt(startIndex);
{ return;
break;
}
endIndex++;
} }
if (endIndex < Count - 1) RemoveRangeInternal(startIndex, endIndex);
}
/// <summary>
/// Removes a range of items from the item list
/// </summary>
/// <param name="index">Start index of the range</param>
/// <param name="endIndex">End index of the range (exclusive)</param>
private void RemoveRangeInternal(int index, int endIndex)
{
if (endIndex < Count)
{ {
Items[endIndex + 1].Previous = startIndex > 0 ? Items[startIndex - 1] : null; Items[endIndex].Previous = index > 0 ? Items[index - 1] : null;
} }
if (startIndex > 0) if (index > 0)
{ {
Items[startIndex - 1].Next = endIndex < Count - 1 ? Items[endIndex + 1] : null; Items[index - 1].Next = endIndex < Count ? Items[endIndex] : null;
} }
if (endIndex < Count)
if (endIndex < Count - 1)
{ {
Array.Copy(Items, endIndex + 1, Items, startIndex, Count - endIndex - 1); Array.Copy(Items, endIndex, Items, index, Count - endIndex);
} }
Count -= endIndex - startIndex + 1; Count -= endIndex - index;
} }
/// <summary> /// <summary>
@ -296,8 +232,8 @@ namespace Ryujinx.Memory.Range
// So we need to return both the split 0-1 and 1-2 ranges. // So we need to return both the split 0-1 and 1-2 ranges.
Lock.EnterWriteLock(); Lock.EnterWriteLock();
(RangeItem<T> first, RangeItem<T> last) = FindOverlapsAsNodes(address, size); (T first, T last) = FindOverlapsAsNodes(address, size);
list = new List<T>(); list = [];
if (first is null) if (first is null)
{ {
@ -311,42 +247,41 @@ namespace Ryujinx.Memory.Range
ulong lastAddress = address; ulong lastAddress = address;
ulong endAddress = address + size; ulong endAddress = address + size;
RangeItem<T> current = first; T current = first;
while (last is not null && current is not null && current.Address < endAddress) while (last is not null && current is not null && current.Address < endAddress)
{ {
T region = current.Value; if (first == last && current.Address == address && current.Size == size)
if (first == last && region.Address == address && region.Size == size)
{ {
// Exact match, no splitting required. // Exact match, no splitting required.
list.Add(region); list.Add(current);
Lock.ExitWriteLock(); Lock.ExitWriteLock();
return; return;
} }
if (lastAddress < region.Address) if (lastAddress < current.Address)
{ {
// There is a gap between this region and the last. We need to fill it. // There is a gap between this region and the last. We need to fill it.
T fillRegion = factory(lastAddress, region.Address - lastAddress); T fillRegion = factory(lastAddress, current.Address - lastAddress);
list.Add(fillRegion); list.Add(fillRegion);
Add(fillRegion); Add(fillRegion);
} }
if (region.Address < address) if (current.Address < address)
{ {
// Split the region around our base address and take the high half. // Split the region around our base address and take the high half.
region = Split(region, address); current = Split(current, address);
} }
if (region.EndAddress > address + size) if (current.EndAddress > address + size)
{ {
// Split the region around our end address and take the low half. // Split the region around our end address and take the low half.
Split(region, address + size); Split(current, address + size);
} }
list.Add(region); list.Add(current);
lastAddress = region.EndAddress; lastAddress = current.EndAddress;
current = current.Next; current = current.Next;
} }
@ -374,7 +309,6 @@ namespace Ryujinx.Memory.Range
private T Split(T region, ulong splitAddress) private T Split(T region, ulong splitAddress)
{ {
T newRegion = (T)region.Split(splitAddress); T newRegion = (T)region.Split(splitAddress);
Update(region);
Add(newRegion); Add(newRegion);
return newRegion; return newRegion;
} }
@ -386,16 +320,11 @@ namespace Ryujinx.Memory.Range
/// <param name="size">Size in bytes of the range</param> /// <param name="size">Size in bytes of the range</param>
/// <returns>The leftmost overlapping item, or null if none is found</returns> /// <returns>The leftmost overlapping item, or null if none is found</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public override RangeItem<T> FindOverlap(ulong address, ulong size) public override T FindOverlap(ulong address, ulong size)
{ {
int index = BinarySearchLeftEdge(address, address + size); int index = BinarySearchLeftEdge(address, address + size);
if (index < 0) return index < 0 ? null : Items[index];
{
return null;
}
return Items[index];
} }
/// <summary> /// <summary>
@ -405,16 +334,11 @@ namespace Ryujinx.Memory.Range
/// <param name="size">Size in bytes of the range</param> /// <param name="size">Size in bytes of the range</param>
/// <returns>The overlapping item, or null if none is found</returns> /// <returns>The overlapping item, or null if none is found</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public override RangeItem<T> FindOverlapFast(ulong address, ulong size) public override T FindOverlapFast(ulong address, ulong size)
{ {
int index = BinarySearch(address, address + size); int index = BinarySearch(address, address + size);
if (index < 0) return index < 0 ? null : Items[index];
{
return null;
}
return Items[index];
} }
/// <summary> /// <summary>
@ -424,23 +348,18 @@ namespace Ryujinx.Memory.Range
/// <param name="size">Size in bytes of the range</param> /// <param name="size">Size in bytes of the range</param>
/// <returns>The first and last overlapping items, or null if none are found</returns> /// <returns>The first and last overlapping items, or null if none are found</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public (RangeItem<T>, RangeItem<T>) FindOverlapsAsNodes(ulong address, ulong size) public (T, T) FindOverlapsAsNodes(ulong address, ulong size)
{ {
(int index, int endIndex) = BinarySearchEdges(address, address + size); (int index, int endIndex) = BinarySearchEdges(address, address + size);
if (index < 0) return index < 0 ? (null, null) : (Items[index], Items[endIndex - 1]);
{
return (null, null);
}
return (Items[index], Items[endIndex - 1]);
} }
public RangeItem<T>[] FindOverlapsAsArray(ulong address, ulong size, out int length) public T[] FindOverlapsAsArray(ulong address, ulong size, out int length)
{ {
(int index, int endIndex) = BinarySearchEdges(address, address + size); (int index, int endIndex) = BinarySearchEdges(address, address + size);
RangeItem<T>[] result; T[] result;
if (index < 0) if (index < 0)
{ {
@ -449,29 +368,20 @@ namespace Ryujinx.Memory.Range
} }
else else
{ {
result = ArrayPool<RangeItem<T>>.Shared.Rent(endIndex - index); result = ArrayPool<T>.Shared.Rent(endIndex - index);
length = endIndex - index; length = endIndex - index;
Array.Copy(Items, index, result, 0, endIndex - index); Items.AsSpan(index, endIndex - index).CopyTo(result);
} }
return result; return result;
} }
public Span<RangeItem<T>> FindOverlapsAsSpan(ulong address, ulong size) public ReadOnlySpan<T> FindOverlapsAsSpan(ulong address, ulong size)
{ {
(int index, int endIndex) = BinarySearchEdges(address, address + size); (int index, int endIndex) = BinarySearchEdges(address, address + size);
Span<RangeItem<T>> result; ReadOnlySpan<T> result = index < 0 ? [] : Items.AsSpan(index, endIndex - index);
if (index < 0)
{
result = [];
}
else
{
result = Items.AsSpan().Slice(index, endIndex - index);
}
return result; return result;
} }
@ -480,7 +390,7 @@ namespace Ryujinx.Memory.Range
{ {
for (int i = 0; i < Count; i++) for (int i = 0; i < Count; i++)
{ {
yield return Items[i].Value; yield return Items[i];
} }
} }
} }

View file

@ -14,14 +14,14 @@ namespace Ryujinx.Memory.Range
/// startIndex is inclusive. /// startIndex is inclusive.
/// endIndex is exclusive. /// endIndex is exclusive.
/// </remarks> /// </remarks>
public readonly struct OverlapResult<T> where T : IRange public readonly struct OverlapResult<T> where T : class, IRangeListRange<T>
{ {
public readonly int StartIndex = -1; public readonly int StartIndex = -1;
public readonly int EndIndex = -1; public readonly int EndIndex = -1;
public readonly RangeItem<T> QuickResult; public readonly T QuickResult;
public int Count => EndIndex - StartIndex; public int Count => EndIndex - StartIndex;
public OverlapResult(int startIndex, int endIndex, RangeItem<T> quickResult = null) public OverlapResult(int startIndex, int endIndex, T quickResult = null)
{ {
this.StartIndex = startIndex; this.StartIndex = startIndex;
this.EndIndex = endIndex; this.EndIndex = endIndex;
@ -33,7 +33,7 @@ namespace Ryujinx.Memory.Range
/// Sorted list of ranges that supports binary search. /// Sorted list of ranges that supports binary search.
/// </summary> /// </summary>
/// <typeparam name="T">Type of the range.</typeparam> /// <typeparam name="T">Type of the range.</typeparam>
public class RangeList<T> : RangeListBase<T> where T : IRange public class RangeList<T> : RangeListBase<T> where T : class, IRangeListRange<T>
{ {
public readonly ReaderWriterLockSlim Lock = new(); public readonly ReaderWriterLockSlim Lock = new();
@ -61,104 +61,6 @@ namespace Ryujinx.Memory.Range
index = ~index; index = ~index;
} }
Insert(index, new RangeItem<T>(item));
}
/// <summary>
/// Updates an item's end address on the list. Address must be the same.
/// </summary>
/// <param name="item">The item to be updated</param>
/// <returns>True if the item was located and updated, false otherwise</returns>
protected override bool Update(T item)
{
int index = BinarySearch(item.Address);
if (index >= 0)
{
while (index < Count)
{
if (Items[index].Value.Equals(item))
{
RangeItem<T> rangeItem = new(item) { Previous = Items[index].Previous, Next = Items[index].Next };
if (index > 0)
{
Items[index - 1].Next = rangeItem;
}
if (index < Count - 1)
{
Items[index + 1].Previous = rangeItem;
}
Items[index] = rangeItem;
return true;
}
if (Items[index].Address > item.Address)
{
break;
}
index++;
}
}
return false;
}
/// <summary>
/// Updates an item's end address on the list. Address must be the same.
/// </summary>
/// <param name="item">The RangeItem to be updated</param>
/// <returns>True if the item was located and updated, false otherwise</returns>
protected override bool Update(RangeItem<T> item)
{
int index = BinarySearch(item.Address);
if (index >= 0)
{
while (index < Count)
{
if (Items[index].Equals(item))
{
RangeItem<T> rangeItem = new(item.Value) { Previous = item.Previous, Next = item.Next };
if (index > 0)
{
Items[index - 1].Next = rangeItem;
}
if (index < Count - 1)
{
Items[index + 1].Previous = rangeItem;
}
Items[index] = rangeItem;
return true;
}
if (Items[index].Address > item.Address)
{
break;
}
index++;
}
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Insert(int index, RangeItem<T> item)
{
Debug.Assert(item.Address != item.EndAddress);
Debug.Assert(item.Address % 32 == 0);
if (Count + 1 > Items.Length) if (Count + 1 > Items.Length)
{ {
Array.Resize(ref Items, Items.Length + BackingGrowthSize); Array.Resize(ref Items, Items.Length + BackingGrowthSize);
@ -220,7 +122,7 @@ namespace Ryujinx.Memory.Range
/// <param name="startItem">The first item in the range of items to be removed</param> /// <param name="startItem">The first item in the range of items to be removed</param>
/// <param name="endItem">The last item in the range of items to be removed</param> /// <param name="endItem">The last item in the range of items to be removed</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void RemoveRange(RangeItem<T> startItem, RangeItem<T> endItem) public override void RemoveRange(T startItem, T endItem)
{ {
if (startItem is null) if (startItem is null)
{ {
@ -229,30 +131,29 @@ namespace Ryujinx.Memory.Range
if (startItem == endItem) if (startItem == endItem)
{ {
Remove(startItem.Value); Remove(startItem);
return; return;
} }
int startIndex = BinarySearch(startItem.Address); (int index, int endIndex) = BinarySearchEdges(startItem.Address, endItem.EndAddress);
int endIndex = BinarySearch(endItem.Address);
if (endIndex < Count - 1) if (endIndex < Count)
{ {
Items[endIndex + 1].Previous = startIndex > 0 ? Items[startIndex - 1] : null; Items[endIndex].Previous = index > 0 ? Items[index - 1] : null;
} }
if (startIndex > 0) if (index > 0)
{ {
Items[startIndex - 1].Next = endIndex < Count - 1 ? Items[endIndex + 1] : null; Items[index - 1].Next = endIndex < Count ? Items[endIndex] : null;
} }
if (endIndex < Count - 1) if (endIndex < Count)
{ {
Array.Copy(Items, endIndex + 1, Items, startIndex, Count - endIndex - 1); Array.Copy(Items, endIndex, Items, index, Count - endIndex);
} }
Count -= endIndex - startIndex + 1; Count -= endIndex - index;
} }
/// <summary> /// <summary>
@ -268,7 +169,7 @@ namespace Ryujinx.Memory.Range
{ {
while (index < Count) while (index < Count)
{ {
if (Items[index].Value.Equals(item)) if (Items[index] == item)
{ {
RemoveAt(index); RemoveAt(index);
@ -298,7 +199,7 @@ namespace Ryujinx.Memory.Range
/// <param name="size">Size in bytes of the range</param> /// <param name="size">Size in bytes of the range</param>
/// <returns>The overlapping item, or the default value for the type if none found</returns> /// <returns>The overlapping item, or the default value for the type if none found</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public override RangeItem<T> FindOverlap(ulong address, ulong size) public override T FindOverlap(ulong address, ulong size)
{ {
int index = BinarySearchLeftEdge(address, address + size); int index = BinarySearchLeftEdge(address, address + size);
@ -321,7 +222,7 @@ namespace Ryujinx.Memory.Range
/// <param name="size">Size in bytes of the range</param> /// <param name="size">Size in bytes of the range</param>
/// <returns>The overlapping item, or the default value for the type if none found</returns> /// <returns>The overlapping item, or the default value for the type if none found</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public override RangeItem<T> FindOverlapFast(ulong address, ulong size) public override T FindOverlapFast(ulong address, ulong size)
{ {
int index = BinarySearch(address, address + size); int index = BinarySearch(address, address + size);
@ -340,7 +241,7 @@ namespace Ryujinx.Memory.Range
/// <param name="size">Size in bytes of the range</param> /// <param name="size">Size in bytes of the range</param>
/// <param name="output">Output array where matches will be written. It is automatically resized to fit the results</param> /// <param name="output">Output array where matches will be written. It is automatically resized to fit the results</param>
/// <returns>Range information of overlapping items found</returns> /// <returns>Range information of overlapping items found</returns>
private OverlapResult<T> FindOverlaps(ulong address, ulong size, ref RangeItem<T>[] output) private OverlapResult<T> FindOverlaps(ulong address, ulong size, ref T[] output)
{ {
int outputCount = 0; int outputCount = 0;
@ -353,7 +254,7 @@ namespace Ryujinx.Memory.Range
for (int i = startIndex; i < Count; i++) for (int i = startIndex; i < Count; i++)
{ {
ref RangeItem<T> item = ref Items[i]; T item = Items[i];
if (item.Address >= endAddress) if (item.Address >= endAddress)
{ {
@ -398,7 +299,7 @@ namespace Ryujinx.Memory.Range
{ {
for (int i = 0; i < Count; i++) for (int i = 0; i < Count; i++)
{ {
yield return Items[i].Value; yield return Items[i];
} }
} }
} }

View file

@ -1,56 +1,22 @@
using Ryujinx.Common; using Ryujinx.Common;
using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace Ryujinx.Memory.Range namespace Ryujinx.Memory.Range
{ {
public class RangeItem<TValue> where TValue : IRange public interface IRangeListRange<TValue> : IRange where TValue : class, IRangeListRange<TValue>
{ {
public RangeItem<TValue> Next; public TValue Next { get; set; }
public RangeItem<TValue> Previous; public TValue Previous { get; set; }
public ulong Address;
public ulong EndAddress;
public TValue Value;
public RangeItem()
{
}
public RangeItem(TValue value)
{
Address = value.Address;
EndAddress = value.Address + value.Size;
Value = value;
}
public RangeItem<TValue> Set(TValue value)
{
Next = null;
Previous = null;
Address = value.Address;
EndAddress = value.Address + value.Size;
Value = value;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool OverlapsWith(ulong address, ulong endAddress)
{
return Address < endAddress && address < EndAddress;
}
} }
public unsafe abstract class RangeListBase<T> : IEnumerable<T> where T : IRange public unsafe abstract class RangeListBase<T> : IEnumerable<T> where T : class, IRangeListRange<T>
{ {
protected static readonly ObjectPool<RangeItem<T>> _rangeItemPool = new(() => new RangeItem<T>());
private const int BackingInitialSize = 1024; private const int BackingInitialSize = 1024;
protected RangeItem<T>[] Items; protected T[] Items;
protected readonly int BackingGrowthSize; protected readonly int BackingGrowthSize;
public int Count { get; protected set; } public int Count { get; protected set; }
@ -62,32 +28,18 @@ namespace Ryujinx.Memory.Range
protected RangeListBase(int backingInitialSize = BackingInitialSize) protected RangeListBase(int backingInitialSize = BackingInitialSize)
{ {
BackingGrowthSize = backingInitialSize; BackingGrowthSize = backingInitialSize;
Items = new RangeItem<T>[backingInitialSize]; Items = new T[backingInitialSize];
} }
public abstract void Add(T item); public abstract void Add(T item);
/// <summary>
/// Updates an item's end address on the list. Address must be the same.
/// </summary>
/// <param name="item">The item to be updated</param>
/// <returns>True if the item was located and updated, false otherwise</returns>
protected abstract bool Update(T item);
/// <summary>
/// Updates an item's end address on the list. Address must be the same.
/// </summary>
/// <param name="item">The RangeItem to be updated</param>
/// <returns>True if the item was located and updated, false otherwise</returns>
protected abstract bool Update(RangeItem<T> item);
public abstract bool Remove(T item); public abstract bool Remove(T item);
public abstract void RemoveRange(RangeItem<T> startItem, RangeItem<T> endItem); public abstract void RemoveRange(T startItem, T endItem);
public abstract RangeItem<T> FindOverlap(ulong address, ulong size); public abstract T FindOverlap(ulong address, ulong size);
public abstract RangeItem<T> FindOverlapFast(ulong address, ulong size); public abstract T FindOverlapFast(ulong address, ulong size);
/// <summary> /// <summary>
/// Performs binary search on the internal list of items. /// Performs binary search on the internal list of items.
@ -106,7 +58,7 @@ namespace Ryujinx.Memory.Range
int middle = left + (range >> 1); int middle = left + (range >> 1);
ref RangeItem<T> item = ref Items[middle]; T item = Items[middle];
if (item.Address == address) if (item.Address == address)
{ {
@ -144,7 +96,7 @@ namespace Ryujinx.Memory.Range
int middle = left + (range >> 1); int middle = left + (range >> 1);
ref RangeItem<T> item = ref Items[middle]; T item = Items[middle];
if (item.OverlapsWith(address, endAddress)) if (item.OverlapsWith(address, endAddress))
{ {
@ -185,7 +137,7 @@ namespace Ryujinx.Memory.Range
int middle = left + (range >> 1); int middle = left + (range >> 1);
ref RangeItem<T> item = ref Items[middle]; T item = Items[middle];
bool match = item.OverlapsWith(address, endAddress); bool match = item.OverlapsWith(address, endAddress);
@ -237,7 +189,7 @@ namespace Ryujinx.Memory.Range
int middle = right - (range >> 1); int middle = right - (range >> 1);
ref RangeItem<T> item = ref Items[middle]; T item = Items[middle];
bool match = item.OverlapsWith(address, endAddress); bool match = item.OverlapsWith(address, endAddress);
@ -282,7 +234,7 @@ namespace Ryujinx.Memory.Range
if (Count == 1) if (Count == 1)
{ {
ref RangeItem<T> item = ref Items[0]; T item = Items[0];
if (item.OverlapsWith(address, endAddress)) if (item.OverlapsWith(address, endAddress))
{ {
@ -312,7 +264,7 @@ namespace Ryujinx.Memory.Range
int middle = left + (range >> 1); int middle = left + (range >> 1);
ref RangeItem<T> item = ref Items[middle]; T item = Items[middle];
bool match = item.OverlapsWith(address, endAddress); bool match = item.OverlapsWith(address, endAddress);
@ -369,7 +321,7 @@ namespace Ryujinx.Memory.Range
int middle = right - (range >> 1); int middle = right - (range >> 1);
ref RangeItem<T> item = ref Items[middle]; T item = Items[middle];
bool match = item.OverlapsWith(address, endAddress); bool match = item.OverlapsWith(address, endAddress);

View file

@ -5,7 +5,7 @@ namespace Ryujinx.Memory.Tracking
/// <summary> /// <summary>
/// A region of memory. /// A region of memory.
/// </summary> /// </summary>
abstract class AbstractRegion : INonOverlappingRange abstract class AbstractRegion<T> : INonOverlappingRange<T> where T : class, INonOverlappingRange<T>
{ {
/// <summary> /// <summary>
/// Base address. /// Base address.
@ -21,6 +21,9 @@ namespace Ryujinx.Memory.Tracking
/// End address. /// End address.
/// </summary> /// </summary>
public ulong EndAddress => Address + Size; public ulong EndAddress => Address + Size;
public T Next { get; set; }
public T Previous { get; set; }
/// <summary> /// <summary>
/// Create a new region. /// Create a new region.
@ -37,11 +40,11 @@ namespace Ryujinx.Memory.Tracking
/// Check if this range overlaps with another. /// Check if this range overlaps with another.
/// </summary> /// </summary>
/// <param name="address">Base address</param> /// <param name="address">Base address</param>
/// <param name="size">Size of the range</param> /// <param name="endAddress">End address</param>
/// <returns>True if overlapping, false otherwise</returns> /// <returns>True if overlapping, false otherwise</returns>
public bool OverlapsWith(ulong address, ulong size) public bool OverlapsWith(ulong address, ulong endAddress)
{ {
return Address < address + size && address < EndAddress; return Address < endAddress && address < EndAddress;
} }
/// <summary> /// <summary>
@ -68,6 +71,6 @@ namespace Ryujinx.Memory.Tracking
/// </summary> /// </summary>
/// <param name="splitAddress">Address to split the region around</param> /// <param name="splitAddress">Address to split the region around</param>
/// <returns>The second part of the split region, with start address at the given split.</returns> /// <returns>The second part of the split region, with start address at the given split.</returns>
public abstract INonOverlappingRange Split(ulong splitAddress); public abstract INonOverlappingRange<T> Split(ulong splitAddress);
} }
} }

View file

@ -81,10 +81,10 @@ namespace Ryujinx.Memory.Tracking
{ {
NonOverlappingRangeList<VirtualRegion> regions = type == 0 ? _virtualRegions : _guestVirtualRegions; NonOverlappingRangeList<VirtualRegion> regions = type == 0 ? _virtualRegions : _guestVirtualRegions;
regions.Lock.EnterReadLock(); regions.Lock.EnterReadLock();
Span<RangeItem<VirtualRegion>> overlaps = regions.FindOverlapsAsSpan(va, size); ReadOnlySpan<VirtualRegion> overlaps = regions.FindOverlapsAsSpan(va, size);
for (int i = 0; i < overlaps.Length; i++) for (int i = 0; i < overlaps.Length; i++)
{ {
VirtualRegion region = overlaps[i].Value; VirtualRegion region = overlaps[i];
// If the region has been fully remapped, signal that it has been mapped again. // If the region has been fully remapped, signal that it has been mapped again.
bool remapped = _memoryManager.IsRangeMapped(region.Address, region.Size); bool remapped = _memoryManager.IsRangeMapped(region.Address, region.Size);
@ -117,11 +117,11 @@ namespace Ryujinx.Memory.Tracking
{ {
NonOverlappingRangeList<VirtualRegion> regions = type == 0 ? _virtualRegions : _guestVirtualRegions; NonOverlappingRangeList<VirtualRegion> regions = type == 0 ? _virtualRegions : _guestVirtualRegions;
regions.Lock.EnterReadLock(); regions.Lock.EnterReadLock();
Span<RangeItem<VirtualRegion>> overlaps = regions.FindOverlapsAsSpan(va, size); ReadOnlySpan<VirtualRegion> overlaps = regions.FindOverlapsAsSpan(va, size);
for (int i = 0; i < overlaps.Length; i++) for (int i = 0; i < overlaps.Length; i++)
{ {
overlaps[i].Value.SignalMappingChanged(false); overlaps[i].SignalMappingChanged(false);
} }
regions.Lock.ExitReadLock(); regions.Lock.ExitReadLock();
} }
@ -301,7 +301,7 @@ namespace Ryujinx.Memory.Tracking
// We use the non-span method here because keeping the lock will cause a deadlock. // We use the non-span method here because keeping the lock will cause a deadlock.
regions.Lock.EnterReadLock(); regions.Lock.EnterReadLock();
RangeItem<VirtualRegion>[] overlaps = regions.FindOverlapsAsArray(address, size, out int length); VirtualRegion[] overlaps = regions.FindOverlapsAsArray(address, size, out int length);
regions.Lock.ExitReadLock(); regions.Lock.ExitReadLock();
if (length == 0 && !precise) if (length == 0 && !precise)
@ -327,7 +327,7 @@ namespace Ryujinx.Memory.Tracking
for (int i = 0; i < length; i++) for (int i = 0; i < length; i++)
{ {
VirtualRegion region = overlaps[i].Value; VirtualRegion region = overlaps[i];
if (precise) if (precise)
{ {
@ -341,7 +341,7 @@ namespace Ryujinx.Memory.Tracking
if (length != 0) if (length != 0)
{ {
ArrayPool<RangeItem<VirtualRegion>>.Shared.Return(overlaps); ArrayPool<VirtualRegion>.Shared.Return(overlaps);
} }
} }
} }

View file

@ -6,7 +6,7 @@ namespace Ryujinx.Memory.Tracking
/// <summary> /// <summary>
/// A region of virtual memory. /// A region of virtual memory.
/// </summary> /// </summary>
class VirtualRegion : AbstractRegion class VirtualRegion : AbstractRegion<VirtualRegion>
{ {
public List<RegionHandle> Handles = []; public List<RegionHandle> Handles = [];
@ -137,7 +137,7 @@ namespace Ryujinx.Memory.Tracking
} }
} }
public override INonOverlappingRange Split(ulong splitAddress) public override INonOverlappingRange<VirtualRegion> Split(ulong splitAddress)
{ {
VirtualRegion newRegion = new(_tracking, splitAddress, EndAddress - splitAddress, Guest, _lastPermission); VirtualRegion newRegion = new(_tracking, splitAddress, EndAddress - splitAddress, Guest, _lastPermission);
Size = splitAddress - Address; Size = splitAddress - Address;

View file

@ -1,5 +1,7 @@
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@ -10,23 +12,34 @@ namespace Ryujinx.UI.LocaleGenerator
{ {
public void Initialize(IncrementalGeneratorInitializationContext context) public void Initialize(IncrementalGeneratorInitializationContext context)
{ {
IncrementalValuesProvider<AdditionalText> localeFile = context.AdditionalTextsProvider.Where(static x => x.Path.EndsWith("locales.json")); IncrementalValuesProvider<AdditionalText> localeFiles = context.AdditionalTextsProvider.Where(static x => Path.GetDirectoryName(x.Path)?.Replace('\\', '/').EndsWith("assets/Locales") ?? false);
IncrementalValuesProvider<string> contents = localeFile.Select((text, cancellationToken) => text.GetText(cancellationToken)!.ToString()); IncrementalValueProvider<ImmutableArray<(string, string)>> collectedContents = localeFiles.Select((text, cancellationToken) => (text.GetText(cancellationToken)!.ToString(), Path.GetFileName(text.Path))).Collect();
context.RegisterSourceOutput(contents, (spc, content) => context.RegisterSourceOutput(collectedContents, (spc, contents) =>
{ {
IEnumerable<string> lines = content.Split('\n').Where(x => x.Trim().StartsWith("\"ID\":")).Select(x => x.Split(':')[1].Trim().Replace("\"", string.Empty).Replace(",", string.Empty));
StringBuilder enumSourceBuilder = new(); StringBuilder enumSourceBuilder = new();
enumSourceBuilder.AppendLine("namespace Ryujinx.Ava.Common.Locale;"); enumSourceBuilder.AppendLine("namespace Ryujinx.Ava.Common.Locale;");
enumSourceBuilder.AppendLine("public enum LocaleKeys"); enumSourceBuilder.AppendLine("public enum LocaleKeys");
enumSourceBuilder.AppendLine("{"); enumSourceBuilder.AppendLine("{");
foreach (string? line in lines)
foreach ((string, string) content in contents)
{ {
enumSourceBuilder.AppendLine($" {line},"); IEnumerable<string> lines = content.Item1.Split('\n').Where(x => x.Trim().StartsWith("\"ID\":")).Select(x => x.Split(':')[1].Trim().Replace("\"", string.Empty).Replace(",", string.Empty));
foreach (string? line in lines)
{
if (content.Item2 == "Root.json")
{
enumSourceBuilder.AppendLine($" {line},");
}
else
{
enumSourceBuilder.AppendLine($" {content.Item2.Split('.')[0]}_{line},");
}
}
} }
enumSourceBuilder.AppendLine("}"); enumSourceBuilder.AppendLine("}");
spc.AddSource("LocaleKeys", enumSourceBuilder.ToString()); spc.AddSource("LocaleKeys", enumSourceBuilder.ToString());

View file

@ -8,6 +8,8 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Common.Locale namespace Ryujinx.Ava.Common.Locale
@ -158,52 +160,86 @@ namespace Ryujinx.Ava.Common.Locale
LocaleChanged?.Invoke(); LocaleChanged?.Invoke();
} }
private static LocalesJson? _localeData; private static LocalesData? _localeData;
private static Dictionary<LocaleKeys, string> LoadJsonLanguage(string languageCode) private static Dictionary<LocaleKeys, string> LoadJsonLanguage(string languageCode)
{ {
Dictionary<LocaleKeys, string> localeStrings = new(); Dictionary<LocaleKeys, string> localeStrings = new();
_localeData ??= EmbeddedResources.ReadAllText("Ryujinx/Assets/Locale.json") if (_localeData is null)
.Into(it => JsonHelper.Deserialize(it, LocalesJsonContext.Default.LocalesJson));
foreach (LocalesEntry locale in _localeData.Value.Locales)
{ {
if (locale.Translations.Count < _localeData.Value.Languages.Count) Dictionary<string, LocalesJson> locales = [];
foreach (string uri in EmbeddedResources.GetAllAvailableResources("Ryujinx/Assets/Locales", ".json"))
{ {
throw new Exception( string path = uri[..^".json".Length];
$"Locale key {{{locale.ID}}} is missing languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!"); path = path.Replace('.', '/');
path = path.Append(".json");
locales.TryAdd(Path.GetFileName(path), EmbeddedResources.ReadAllText(path)
.Into(it => JsonHelper.Deserialize(it, LocalesJsonContext.Default.LocalesJson)));
} }
if (locale.Translations.Count > _localeData.Value.Languages.Count) _localeData = new LocalesData
{ {
throw new Exception( Languages = EmbeddedResources.ReadAllText("Ryujinx/Assets/Languages.json")
$"Locale key {{{locale.ID}}} has too many languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!"); .Into(it => JsonHelper.Deserialize(it, LanguagesJsonContext.Default.LanguagesJson)).Languages.Keys.ToList(),
} LocalesFiles = locales
};
if (!Enum.TryParse<LocaleKeys>(locale.ID, out LocaleKeys localeKey)) }
continue;
string str = locale.Translations.TryGetValue(languageCode, out string val) && !string.IsNullOrEmpty(val) foreach (LocalesJson file in _localeData.Value.LocalesFiles.Values)
? val {
: locale.Translations[DefaultLanguageCode]; foreach (LocalesEntry locale in file.Locales)
if (string.IsNullOrEmpty(str))
{ {
throw new Exception( if (locale.Translations.Count < _localeData.Value.Languages.Count)
$"Locale key '{locale.ID}' has no valid translations for desired language {languageCode}! {DefaultLanguageCode} is an empty string or null"); {
} throw new Exception(
$"Locale key {{{locale.ID}}} is missing languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
}
localeStrings[localeKey] = str; if (locale.Translations.Count > _localeData.Value.Languages.Count)
{
throw new Exception(
$"Locale key {{{locale.ID}}} has too many languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
}
if (!Enum.TryParse<LocaleKeys>(locale.ID, out LocaleKeys localeKey))
continue;
string str = locale.Translations.TryGetValue(languageCode, out string val) && !string.IsNullOrEmpty(val)
? val
: locale.Translations[DefaultLanguageCode];
if (string.IsNullOrEmpty(str))
{
throw new Exception(
$"Locale key '{locale.ID}' has no valid translations for desired language {languageCode}! {DefaultLanguageCode} is an empty string or null");
}
localeStrings[localeKey] = str;
}
} }
return localeStrings; return localeStrings;
} }
} }
public struct LocalesJson public struct LocalesData
{ {
public List<string> Languages { get; set; } public List<string> Languages { get; set; }
public Dictionary<string, LocalesJson> LocalesFiles { get; set; }
}
public struct LanguagesJson
{
public Dictionary<string, string> Languages { get; set; }
}
public struct LocalesJson
{
public List<LocalesEntry> Locales { get; set; } public List<LocalesEntry> Locales { get; set; }
} }
@ -216,4 +252,8 @@ namespace Ryujinx.Ava.Common.Locale
[JsonSourceGenerationOptions(WriteIndented = true)] [JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(LocalesJson))] [JsonSerializable(typeof(LocalesJson))]
internal partial class LocalesJsonContext : JsonSerializerContext; internal partial class LocalesJsonContext : JsonSerializerContext;
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(LanguagesJson))]
internal partial class LanguagesJsonContext : JsonSerializerContext;
} }

View file

@ -580,5 +580,10 @@ namespace Ryujinx.Headless
{ {
return AccountSaveDataManager.GetLastUsedUser(); return AccountSaveDataManager.GetLastUsedUser();
} }
public void TakeScreenshot()
{
throw new NotImplementedException();
}
} }
} }

View file

@ -17,6 +17,7 @@ using Ryujinx.Common.Configuration;
using Ryujinx.Common.GraphicsDriver; using Ryujinx.Common.GraphicsDriver;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.SystemInterop; using Ryujinx.Common.SystemInterop;
using Ryujinx.Common.Utilities;
using Ryujinx.Graphics.Vulkan.MoltenVK; using Ryujinx.Graphics.Vulkan.MoltenVK;
using Ryujinx.Headless; using Ryujinx.Headless;
using Ryujinx.SDL3.Common; using Ryujinx.SDL3.Common;
@ -46,7 +47,7 @@ namespace Ryujinx.Ava
public static int Main(string[] args) public static int Main(string[] args)
{ {
Version = ReleaseInformation.Version; Version = ReleaseInformation.Version;
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())
{ {
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041)) if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041))
@ -55,8 +56,11 @@ namespace Ryujinx.Ava
return 0; return 0;
} }
if (Environment.CurrentDirectory.StartsWithIgnoreCase("C:\\Program Files") || var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
Environment.CurrentDirectory.StartsWithIgnoreCase("C:\\Program Files (x86)")) var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
if (Environment.CurrentDirectory.StartsWithIgnoreCase(programFiles) ||
Environment.CurrentDirectory.StartsWithIgnoreCase(programFilesX86))
{ {
_ = Win32NativeInterop.MessageBoxA(nint.Zero, "Ryujinx is not intended to be run from the Program Files folder. Please move it out and relaunch.", $"Ryujinx {Version}", MbIconwarning); _ = Win32NativeInterop.MessageBoxA(nint.Zero, "Ryujinx is not intended to be run from the Program Files folder. Please move it out and relaunch.", $"Ryujinx {Version}", MbIconwarning);
return 0; return 0;
@ -73,11 +77,23 @@ namespace Ryujinx.Ava
} }
} }
bool noGuiArg = ConsumeCommandLineArgument(ref args, "--no-gui") || ConsumeCommandLineArgument(ref args, "nogui");
bool coreDumpArg = ConsumeCommandLineArgument(ref args, "--core-dumps");
// TODO: Ryujinx causes core dumps on Linux when it exits "uncleanly", eg. through an unhandled exception.
// This is undesirable and causes very odd behavior during development (the process stops responding,
// the .NET debugger freezes or suddenly detaches, /tmp/ gets filled etc.), unless explicitly requested by the user.
// This needs to be investigated, but calling prctl() is better than modifying system-wide settings or leaving this be.
if (!coreDumpArg)
{
OsUtils.SetCoreDumpable(false);
}
PreviewerDetached = true; PreviewerDetached = true;
if (args.Length > 0 && args[0] is "--no-gui" or "nogui") if (noGuiArg)
{ {
HeadlessRyujinx.Entrypoint(args[1..]); HeadlessRyujinx.Entrypoint(args);
return 0; return 0;
} }
@ -112,6 +128,14 @@ namespace Ryujinx.Ava
: [Win32RenderingMode.Software] : [Win32RenderingMode.Software]
}); });
private static bool ConsumeCommandLineArgument(ref string[] args, string targetArgument)
{
List<string> argList = [.. args];
bool found = argList.Remove(targetArgument);
args = argList.ToArray();
return found;
}
private static void Initialize(string[] args) private static void Initialize(string[] args)
{ {
// Ensure Discord presence timestamp begins at the absolute start of when Ryujinx is launched // Ensure Discord presence timestamp begins at the absolute start of when Ryujinx is launched
@ -177,7 +201,6 @@ namespace Ryujinx.Ava
} }
} }
public static string GetDirGameUserConfig(string gameId, bool changeFolderForGame = false) public static string GetDirGameUserConfig(string gameId, bool changeFolderForGame = false)
{ {
if (string.IsNullOrEmpty(gameId)) if (string.IsNullOrEmpty(gameId))

View file

@ -134,7 +134,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="Assets\locales.json" /> <None Remove="Assets\**\*.json" />
<None Remove="Assets\Styles\Styles.xaml" /> <None Remove="Assets\Styles\Styles.xaml" />
<None Remove="Assets\Styles\Themes.xaml" /> <None Remove="Assets\Styles\Themes.xaml" />
<None Remove="Assets\Icons\Controller_JoyConLeft.svg" /> <None Remove="Assets\Icons\Controller_JoyConLeft.svg" />
@ -156,8 +156,8 @@
<EmbeddedResource Include="..\..\docs\compatibility.csv" LogicalName="RyujinxGameCompatibilityList"> <EmbeddedResource Include="..\..\docs\compatibility.csv" LogicalName="RyujinxGameCompatibilityList">
<Link>Assets\RyujinxGameCompatibility.csv</Link> <Link>Assets\RyujinxGameCompatibility.csv</Link>
</EmbeddedResource> </EmbeddedResource>
<EmbeddedResource Include="..\..\assets\locales.json"> <EmbeddedResource Include="..\..\assets\**\*.json">
<Link>Assets\Locale.json</Link> <LinkBase>Assets</LinkBase>
</EmbeddedResource> </EmbeddedResource>
<EmbeddedResource Include="Assets\Styles\Styles.xaml" /> <EmbeddedResource Include="Assets\Styles\Styles.xaml" />
<EmbeddedResource Include="Assets\Icons\Controller_JoyConLeft.svg" /> <EmbeddedResource Include="Assets\Icons\Controller_JoyConLeft.svg" />
@ -178,6 +178,6 @@
<EmbeddedResource Include="Assets\UIImages\Logo_Ryujinx_AntiAlias.png" /> <EmbeddedResource Include="Assets\UIImages\Logo_Ryujinx_AntiAlias.png" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AdditionalFiles Include="..\..\assets\locales.json" /> <AdditionalFiles Include="..\..\assets\Locales\*.json" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -1,9 +1,8 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Systems.Configuration namespace Ryujinx.Ava.Systems.Configuration
{ {
[JsonConverter(typeof(TypedStringEnumConverter<AudioBackend>))] [JsonConverter(typeof(JsonStringEnumConverter<AudioBackend>))]
public enum AudioBackend public enum AudioBackend
{ {
Dummy, Dummy,

View file

@ -1,10 +1,9 @@
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.HOS.SystemState;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Systems.Configuration.System namespace Ryujinx.Ava.Systems.Configuration.System
{ {
[JsonConverter(typeof(TypedStringEnumConverter<Language>))] [JsonConverter(typeof(JsonStringEnumConverter<Language>))]
public enum Language public enum Language
{ {
Japanese, Japanese,

View file

@ -1,10 +1,9 @@
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.HOS.SystemState;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Systems.Configuration.System namespace Ryujinx.Ava.Systems.Configuration.System
{ {
[JsonConverter(typeof(TypedStringEnumConverter<Region>))] [JsonConverter(typeof(JsonStringEnumConverter<Region>))]
public enum Region public enum Region
{ {
Japan, Japan,

View file

@ -1,9 +1,8 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Systems.Configuration.UI namespace Ryujinx.Ava.Systems.Configuration.UI
{ {
[JsonConverter(typeof(TypedStringEnumConverter<FocusLostType>))] [JsonConverter(typeof(JsonStringEnumConverter<FocusLostType>))]
public enum FocusLostType public enum FocusLostType
{ {
DoNothing, DoNothing,

View file

@ -1,9 +1,8 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Systems.Configuration.UI namespace Ryujinx.Ava.Systems.Configuration.UI
{ {
[JsonConverter(typeof(TypedStringEnumConverter<UpdaterType>))] [JsonConverter(typeof(JsonStringEnumConverter<UpdaterType>))]
public enum UpdaterType public enum UpdaterType
{ {
Off, Off,

View file

@ -327,5 +327,10 @@ namespace Ryujinx.Ava.UI.Applet
return profile; return profile;
} }
public void TakeScreenshot()
{
_parent.ViewModel.AppHost.ScreenshotRequested = true;
}
} }
} }

View file

@ -56,6 +56,8 @@ namespace Ryujinx.Ava
if (OperatingSystem.IsMacOS()) if (OperatingSystem.IsMacOS())
{ {
// Switches macOS key held behavior to repeat the input key instead of showing the character accents menu (like doing on an iOS keyboard would).
// https://macos-defaults.com/keyboard/applepressandholdenabled.html
Process.Start("/usr/bin/defaults", "write org.ryujinx.Ryujinx ApplePressAndHoldEnabled -bool false"); Process.Start("/usr/bin/defaults", "write org.ryujinx.Ryujinx ApplePressAndHoldEnabled -bool false");
} }
} }

View file

@ -1333,7 +1333,10 @@ namespace Ryujinx.Ava.UI.ViewModels
} }
} }
public void TakeScreenshot() => AppHost.ScreenshotRequested = true; public void TakeScreenshot()
{
AppHost.ScreenshotRequested = true;
}
public void HideUi() => ShowMenuAndStatusBar = false; public void HideUi() => ShowMenuAndStatusBar = false;

View file

@ -85,29 +85,16 @@ namespace Ryujinx.Ava.UI.Views.Main
private static IEnumerable<MenuItem> GenerateLanguageMenuItems() private static IEnumerable<MenuItem> GenerateLanguageMenuItems()
{ {
const string LocalePath = "Ryujinx/Assets/Locale.json"; const string LanguagesPath = "Ryujinx/Assets/Languages.json";
string languageJson = EmbeddedResources.ReadAllText(LocalePath); string languageJson = EmbeddedResources.ReadAllText(LanguagesPath);
string currentLanguageCode = LocaleManager.Instance.CurrentLanguageCode; string currentLanguageCode = LocaleManager.Instance.CurrentLanguageCode;
LocalesJson locales = JsonHelper.Deserialize(languageJson, LocalesJsonContext.Default.LocalesJson); LanguagesJson languages = JsonHelper.Deserialize(languageJson, LanguagesJsonContext.Default.LanguagesJson);
foreach (string language in locales.Languages) foreach ((string code, string language) in languages.Languages)
{ {
int index = locales.Locales.FindIndex(x => x.ID == "Language"); string languageName = string.IsNullOrEmpty(language) ? code : language;
string languageName;
if (index == -1)
{
languageName = language;
}
else
{
string tr = locales.Locales[index].Translations[language];
languageName = string.IsNullOrEmpty(tr)
? language
: tr;
}
MenuItem menuItem = new() MenuItem menuItem = new()
{ {