diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index ba372c6bc..18e00b6d2 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -1,4 +1,4 @@ -name: Canary release job +name: Canary CI on: workflow_dispatch: @@ -19,7 +19,6 @@ concurrency: release env: POWERSHELL_TELEMETRY_OPTOUT: 1 DOTNET_CLI_TELEMETRY_OPTOUT: 1 - RYUJINX_BASE_VERSION: "1.3" RYUJINX_TARGET_RELEASE_CHANNEL_NAME: "canary" RELEASE: 1 @@ -30,8 +29,8 @@ jobs: 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: win-x64, os: ubuntu-latest, zip_os_name: win_x64 } + #- { name: win-arm64, os: ubuntu-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: @@ -44,11 +43,25 @@ jobs: - name: Overwrite csc problem matcher 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 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 "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 @@ -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 - name: Packing Windows builds - if: matrix.platform.os == 'windows-latest' + if: contains(matrix.platform.name, 'win') run: | pushd publish rm libarmeilleure-jitsupport.dylib 7z a ../release_output/ryujinx-canary-${{ 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/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" + + 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 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' + if: contains(matrix.platform.name, 'linux') run: | pushd publish 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 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 - name: Build AppImage (Linux) - if: matrix.platform.os == 'ubuntu-latest' + if: contains(matrix.platform.name, 'linux') run: | BUILD_VERSION="${{ steps.version_info.outputs.build_version }}" PLATFORM_NAME="${{ matrix.platform.name }}" @@ -139,8 +139,8 @@ jobs: pushd publish_appimage mv Ryujinx.AppImage ../release_output/ryujinx-canary-$BUILD_VERSION-$ARCH_NAME.AppImage 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 macos_release: @@ -159,10 +159,10 @@ jobs: chmod +x llvm.sh sudo ./llvm.sh 17 - - name: Install GitLabCli + - name: Install gli run: | 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 mv gli $HOME/.bin/ echo "$HOME/.bin" >> $GITHUB_PATH @@ -183,9 +183,10 @@ jobs: - name: Get version info 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 "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: Configure for release run: | @@ -200,7 +201,7 @@ jobs: - name: Publish macOS Ryujinx run: | ./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish_ava ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 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: name: Create GitLab Release @@ -210,37 +211,41 @@ jobs: - release steps: - uses: actions/checkout@v4 - - - name: Get version info - 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 + + - name: Install gli run: | 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 mv gli $HOME/.bin/ echo "$HOME/.bin" >> $GITHUB_PATH env: 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 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 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 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 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 diff --git a/.github/workflows/debug_release.yml b/.github/workflows/debug_release.yml deleted file mode 100644 index b166adb61..000000000 --- a/.github/workflows/debug_release.yml +++ /dev/null @@ -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" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 79b51d8e9..3c1b6f2de 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,15 +1,18 @@ -name: Release job +name: Stable CI on: 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 env: POWERSHELL_TELEMETRY_OPTOUT: 1 DOTNET_CLI_TELEMETRY_OPTOUT: 1 - RYUJINX_BASE_VERSION: "1.3" RYUJINX_TARGET_RELEASE_CHANNEL_NAME: "release" RELEASE: 1 @@ -20,8 +23,8 @@ jobs: 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: win-x64, os: ubuntu-latest, zip_os_name: win_x64 } + #- { name: win-arm64, os: ubuntu-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: @@ -33,12 +36,30 @@ jobs: - name: Overwrite csc problem matcher 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 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 + 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 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 - name: Packing Windows builds - if: matrix.platform.os == 'windows-latest' + if: contains(matrix.platform.name, 'win') 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" + + 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 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' + if: contains(matrix.platform.name, 'linux') 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" + + 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 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build AppImage (Linux) - if: matrix.platform.os == 'ubuntu-latest' + if: contains(matrix.platform.name, 'linux') run: | BUILD_VERSION="${{ steps.version_info.outputs.build_version }}" PLATFORM_NAME="${{ matrix.platform.name }}" @@ -131,7 +139,7 @@ jobs: mv Ryujinx.AppImage ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage 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 macos_release: @@ -150,10 +158,10 @@ jobs: chmod +x llvm.sh sudo ./llvm.sh 17 - - name: Install GitLabCli + - name: Install gli run: | 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 mv gli $HOME/.bin/ echo "$HOME/.bin" >> $GITHUB_PATH @@ -174,9 +182,14 @@ jobs: - name: Get version info 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 + 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 + shell: bash - name: Configure for release run: | @@ -189,7 +202,8 @@ jobs: - 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" + + 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: name: Create GitLab Release @@ -200,32 +214,45 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Get version info - 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 + - name: Install gli run: | 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 mv gli $HOME/.bin/ echo "$HOME/.bin" >> $GITHUB_PATH env: 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 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 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 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 diff --git a/.gitignore b/.gitignore index 0c46c50c0..6f887e638 100644 --- a/.gitignore +++ b/.gitignore @@ -100,6 +100,7 @@ DocProject/Help/html # Click-Once directory publish/ +RyubingMaintainerTools/ # Publish Web Output *.Publish.xml diff --git a/Ryujinx.sln b/Ryujinx.sln index 24def42a3..b89d5da0a 100644 --- a/Ryujinx.sln +++ b/Ryujinx.sln @@ -21,6 +21,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.GAL", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.OpenGL", "src\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj", "{9558FB96-075D-4219-8FFF-401979DC0B69}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Graphics.RenderDoc", "src\Ryujinx.Graphics.RenderDocApi\Ryujinx.Graphics.RenderDocApi.csproj", "{D58FA894-27D5-4EAA-9042-AD422AD82931}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Texture", "src\Ryujinx.Graphics.Texture\Ryujinx.Graphics.Texture.csproj", "{E1B1AD28-289D-47B7-A106-326972240207}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Shader", "src\Ryujinx.Graphics.Shader\Ryujinx.Graphics.Shader.csproj", "{03B955CD-AD84-4B93-AAA7-BF17923BBAA5}" @@ -555,6 +557,18 @@ Global {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x64.Build.0 = Release|Any CPU {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x86.ActiveCfg = Release|Any CPU {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x86.Build.0 = Release|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x64.ActiveCfg = Debug|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x64.Build.0 = Debug|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x86.ActiveCfg = Debug|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x86.Build.0 = Debug|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|Any CPU.Build.0 = Release|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x64.ActiveCfg = Release|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x64.Build.0 = Release|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x86.ActiveCfg = Release|Any CPU + {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/assets/Languages.json b/assets/Languages.json new file mode 100644 index 000000000..e921b6e30 --- /dev/null +++ b/assets/Languages.json @@ -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": "繁體中文 (台灣)" + } +} \ No newline at end of file diff --git a/assets/Locales.md b/assets/Locales.md new file mode 100644 index 000000000..39b7e4d46 --- /dev/null +++ b/assets/Locales.md @@ -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 `"": ""`, 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 + } + } + ] + } + \ No newline at end of file diff --git a/assets/Locales/RenderDoc.json b/assets/Locales/RenderDoc.json new file mode 100644 index 000000000..132e24067 --- /dev/null +++ b/assets/Locales/RenderDoc.json @@ -0,0 +1,104 @@ +{ + "Locales": [ + { + "ID": "MenuBarActions_StartCapture", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Start RenderDoc Frame Capture", + "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": "" + } + }, + { + "ID": "MenuBarActions_EndCapture", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "End RenderDoc Frame Capture", + "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": "" + } + }, + { + "ID": "MenuBarActions_DiscardCapture", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Discard RenderDoc Frame Capture", + "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": "" + } + }, + { + "ID": "MenuBarActions_DiscardCapture_ToolTip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Ends the currently active RenderDoc Frame Capture, immediately discarding its result.", + "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": "" + } + } + ] +} diff --git a/assets/locales.json b/assets/Locales/Root.json similarity index 99% rename from assets/locales.json rename to assets/Locales/Root.json index 8899bf692..aa8937247 100644 --- a/assets/locales.json +++ b/assets/Locales/Root.json @@ -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": [ - { - "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", "Translations": { diff --git a/src/ARMeilleure/Instructions/InstEmitException.cs b/src/ARMeilleure/Instructions/InstEmitException.cs index d30fb2fbd..a91716c64 100644 --- a/src/ARMeilleure/Instructions/InstEmitException.cs +++ b/src/ARMeilleure/Instructions/InstEmitException.cs @@ -19,7 +19,7 @@ namespace ARMeilleure.Instructions context.LoadFromContext(); - context.Return(Const(op.Address)); + InstEmitFlowHelper.EmitReturn(context, Const(op.Address)); } public static void Svc(ArmEmitterContext context) @@ -49,7 +49,7 @@ namespace ARMeilleure.Instructions context.LoadFromContext(); - context.Return(Const(op.Address)); + InstEmitFlowHelper.EmitReturn(context, Const(op.Address)); } } } diff --git a/src/ARMeilleure/Instructions/InstEmitException32.cs b/src/ARMeilleure/Instructions/InstEmitException32.cs index 57af1522b..e5bad56ef 100644 --- a/src/ARMeilleure/Instructions/InstEmitException32.cs +++ b/src/ARMeilleure/Instructions/InstEmitException32.cs @@ -33,7 +33,7 @@ namespace ARMeilleure.Instructions context.LoadFromContext(); - context.Return(Const(context.CurrOp.Address)); + InstEmitFlowHelper.EmitReturn(context, Const(context.CurrOp.Address)); } } } diff --git a/src/ARMeilleure/Instructions/InstEmitFlow.cs b/src/ARMeilleure/Instructions/InstEmitFlow.cs index a986bf66f..cb214d3d5 100644 --- a/src/ARMeilleure/Instructions/InstEmitFlow.cs +++ b/src/ARMeilleure/Instructions/InstEmitFlow.cs @@ -66,7 +66,7 @@ namespace ARMeilleure.Instructions { OpCodeBReg op = (OpCodeBReg)context.CurrOp; - context.Return(GetIntOrZR(context, op.Rn)); + EmitReturn(context, GetIntOrZR(context, op.Rn)); } public static void Tbnz(ArmEmitterContext context) => EmitTb(context, onNotZero: true); diff --git a/src/ARMeilleure/Instructions/InstEmitFlowHelper.cs b/src/ARMeilleure/Instructions/InstEmitFlowHelper.cs index d0871b29f..74866f982 100644 --- a/src/ARMeilleure/Instructions/InstEmitFlowHelper.cs +++ b/src/ARMeilleure/Instructions/InstEmitFlowHelper.cs @@ -13,6 +13,10 @@ namespace ARMeilleure.Instructions { static class InstEmitFlowHelper { + // How many calls we can have in our call stack before we give up and return to the dispatcher. + // This prevents stack overflows caused by deep recursive calls. + private const int MaxCallDepth = 200; + public static void EmitCondBranch(ArmEmitterContext context, Operand target, Condition cond) { if (cond != Condition.Al) @@ -182,12 +186,7 @@ namespace ARMeilleure.Instructions { if (isReturn || context.IsSingleStep) { - if (target.Type == OperandType.I32) - { - target = context.ZeroExtend32(OperandType.I64, target); - } - - context.Return(target); + EmitReturn(context, target); } else { @@ -195,6 +194,19 @@ namespace ARMeilleure.Instructions } } + public static void EmitReturn(ArmEmitterContext context, Operand target) + { + Operand nativeContext = context.LoadArgument(OperandType.I64, 0); + DecreaseCallDepth(context, nativeContext); + + if (target.Type == OperandType.I32) + { + target = context.ZeroExtend32(OperandType.I64, target); + } + + context.Return(target); + } + private static void EmitTableBranch(ArmEmitterContext context, Operand guestAddress, bool isJump) { context.StoreToContext(); @@ -257,6 +269,8 @@ namespace ARMeilleure.Instructions if (isJump) { + DecreaseCallDepth(context, nativeContext); + context.Tailcall(hostAddress, nativeContext); } else @@ -278,8 +292,42 @@ namespace ARMeilleure.Instructions Operand lblContinue = context.GetLabel(nextAddr.Value); context.BranchIf(lblContinue, returnAddress, nextAddr, Comparison.Equal, BasicBlockFrequency.Cold); + DecreaseCallDepth(context, nativeContext); + context.Return(returnAddress); } } + + public static void EmitCallDepthCheckAndIncrement(EmitterContext context, Operand guestAddress) + { + if (!Optimizations.EnableDeepCallRecursionProtection) + { + return; + } + + Operand nativeContext = context.LoadArgument(OperandType.I64, 0); + Operand callDepthAddr = context.Add(nativeContext, Const((ulong)NativeContext.GetCallDepthOffset())); + Operand currentCallDepth = context.Load(OperandType.I32, callDepthAddr); + Operand lblDoCall = Label(); + + context.BranchIf(lblDoCall, currentCallDepth, Const(MaxCallDepth), Comparison.LessUI); + context.Store(callDepthAddr, context.Subtract(currentCallDepth, Const(1))); + context.Return(guestAddress); + + context.MarkLabel(lblDoCall); + context.Store(callDepthAddr, context.Add(currentCallDepth, Const(1))); + } + + private static void DecreaseCallDepth(EmitterContext context, Operand nativeContext) + { + if (!Optimizations.EnableDeepCallRecursionProtection) + { + return; + } + + Operand callDepthAddr = context.Add(nativeContext, Const((ulong)NativeContext.GetCallDepthOffset())); + Operand currentCallDepth = context.Load(OperandType.I32, callDepthAddr); + context.Store(callDepthAddr, context.Subtract(currentCallDepth, Const(1))); + } } } diff --git a/src/ARMeilleure/Optimizations.cs b/src/ARMeilleure/Optimizations.cs index 6dd7befe7..3a76b6d93 100644 --- a/src/ARMeilleure/Optimizations.cs +++ b/src/ARMeilleure/Optimizations.cs @@ -13,6 +13,7 @@ namespace ARMeilleure public static bool AllowLcqInFunctionTable { get; set; } = true; public static bool UseUnmanagedDispatchLoop { get; set; } = true; public static bool EnableDebugging { get; set; } = false; + public static bool EnableDeepCallRecursionProtection { get; set; } = true; public static bool UseAdvSimdIfAvailable { get; set; } = true; public static bool UseArm64AesIfAvailable { get; set; } = true; diff --git a/src/ARMeilleure/State/ExecutionContext.cs b/src/ARMeilleure/State/ExecutionContext.cs index fa1a4a032..365805e45 100644 --- a/src/ARMeilleure/State/ExecutionContext.cs +++ b/src/ARMeilleure/State/ExecutionContext.cs @@ -134,6 +134,11 @@ namespace ARMeilleure.State public bool GetFPstateFlag(FPState flag) => _nativeContext.GetFPStateFlag(flag); public void SetFPstateFlag(FPState flag, bool value) => _nativeContext.SetFPStateFlag(flag, value); + internal void ResetCallDepth() + { + _nativeContext.ResetCallDepth(); + } + internal void CheckInterrupt() { if (Interrupted) diff --git a/src/ARMeilleure/State/NativeContext.cs b/src/ARMeilleure/State/NativeContext.cs index a9f1c3dab..bc9e31888 100644 --- a/src/ARMeilleure/State/NativeContext.cs +++ b/src/ARMeilleure/State/NativeContext.cs @@ -22,6 +22,7 @@ namespace ARMeilleure.State public ulong ExclusiveValueHigh; public int Running; public long Tpidr2El0; + public int CallDepth; /// /// Precise PC value used for debugging. @@ -199,6 +200,8 @@ namespace ARMeilleure.State public bool GetRunning() => GetStorage().Running != 0; public void SetRunning(bool value) => GetStorage().Running = value ? 1 : 0; + public void ResetCallDepth() => GetStorage().CallDepth = 0; + public unsafe static int GetRegisterOffset(Register reg) { if (reg.Type == RegisterType.Integer) @@ -284,6 +287,11 @@ namespace ARMeilleure.State return StorageOffset(ref _dummyStorage, ref _dummyStorage.DebugPrecisePc); } + public static int GetCallDepthOffset() + { + return StorageOffset(ref _dummyStorage, ref _dummyStorage.CallDepth); + } + private static int StorageOffset(ref NativeCtxStorage storage, ref T target) { return (int)Unsafe.ByteOffset(ref Unsafe.As(ref storage), ref target); diff --git a/src/ARMeilleure/Translation/PTC/Ptc.cs b/src/ARMeilleure/Translation/PTC/Ptc.cs index c69ebcadb..cfa4377db 100644 --- a/src/ARMeilleure/Translation/PTC/Ptc.cs +++ b/src/ARMeilleure/Translation/PTC/Ptc.cs @@ -33,7 +33,7 @@ namespace ARMeilleure.Translation.PTC private const string OuterHeaderMagicString = "PTCohd\0\0"; private const string InnerHeaderMagicString = "PTCihd\0\0"; - private const uint InternalVersion = 7009; //! To be incremented manually for each change to the ARMeilleure project. + private const uint InternalVersion = 7010; //! To be incremented manually for each change to the ARMeilleure project. private const string ActualDir = "0"; private const string BackupDir = "1"; diff --git a/src/ARMeilleure/Translation/Translator.cs b/src/ARMeilleure/Translation/Translator.cs index 0ebb705a4..b098ff4cf 100644 --- a/src/ARMeilleure/Translation/Translator.cs +++ b/src/ARMeilleure/Translation/Translator.cs @@ -186,6 +186,7 @@ namespace ARMeilleure.Translation Statistics.StartTimer(); + context.ResetCallDepth(); ulong nextAddr = func.Execute(Stubs.ContextWrapper, context); Statistics.StopTimer(address); @@ -260,6 +261,7 @@ namespace ARMeilleure.Translation Logger.StartPass(PassName.Translation); + InstEmitFlowHelper.EmitCallDepthCheckAndIncrement(context, Const(address)); EmitSynchronization(context); if (blocks[0].Address != address) diff --git a/src/ARMeilleure/Translation/TranslatorStubs.cs b/src/ARMeilleure/Translation/TranslatorStubs.cs index 458a42434..2d95ceb99 100644 --- a/src/ARMeilleure/Translation/TranslatorStubs.cs +++ b/src/ARMeilleure/Translation/TranslatorStubs.cs @@ -262,10 +262,18 @@ namespace ARMeilleure.Translation Operand runningAddress = context.Add(nativeContext, Const((ulong)NativeContext.GetRunningOffset())); Operand dispatchAddress = context.Add(nativeContext, Const((ulong)NativeContext.GetDispatchAddressOffset())); + Operand callDepthAddress = context.Add(nativeContext, Const((ulong)NativeContext.GetCallDepthOffset())); EmitSyncFpContext(context, nativeContext, true); context.MarkLabel(beginLbl); + + if (Optimizations.EnableDeepCallRecursionProtection) + { + // Reset the call depth counter, since this is our first guest function call. + context.Store(callDepthAddress, Const(0)); + } + context.Store(dispatchAddress, guestAddress); context.Copy(guestAddress, context.Call(Const((ulong)DispatchStub), OperandType.I64, nativeContext)); context.BranchIfFalse(endLbl, guestAddress); diff --git a/src/Ryujinx.Audio/Renderer/Parameter/Effect/BiquadFilterEffectParameter2.cs b/src/Ryujinx.Audio/Renderer/Parameter/Effect/BiquadFilterEffectParameter2.cs index 0c74f1e7b..b1e61f6c1 100644 --- a/src/Ryujinx.Audio/Renderer/Parameter/Effect/BiquadFilterEffectParameter2.cs +++ b/src/Ryujinx.Audio/Renderer/Parameter/Effect/BiquadFilterEffectParameter2.cs @@ -19,6 +19,11 @@ namespace Ryujinx.Audio.Renderer.Parameter.Effect /// The output channel indices that will be used by the . /// public Array6 Output; + + /// + /// Reserved/unused. + /// + private readonly uint _padding; /// /// Biquad filter numerator (b0, b1, b2). diff --git a/src/Ryujinx.BuildValidationTasks/LocalesValidationTask.cs b/src/Ryujinx.BuildValidationTasks/LocalesValidationTask.cs index a07f0c4ae..a97e7b409 100644 --- a/src/Ryujinx.BuildValidationTasks/LocalesValidationTask.cs +++ b/src/Ryujinx.BuildValidationTasks/LocalesValidationTask.cs @@ -11,9 +11,7 @@ namespace Ryujinx.BuildValidationTasks { static readonly JsonSerializerOptions _jsonOptions = new() { - WriteIndented = true, - NewLine = "\n", - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + WriteIndented = true, NewLine = "\n", Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; public LocalesValidationTask() { } @@ -22,77 +20,116 @@ namespace Ryujinx.BuildValidationTasks { Console.WriteLine("Running Locale Validation Task..."); - string path = projectPath + "assets/locales.json"; + bool encounteredIssue = false; + string langPath = projectPath + "assets/Languages.json"; string data; - using (StreamReader sr = new(path)) + using (StreamReader sr = new(langPath)) { data = sr.ReadToEnd(); } - LocalesJson json; - 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 { - json = JsonSerializer.Deserialize(data); - + langJson = JsonSerializer.Deserialize(data); } catch (JsonException e) { throw new JsonException(e.Message); //shorter and easier stacktrace } - bool encounteredIssue = false; - - for (int i = 0; i < json.Locales.Count; i++) + foreach ((string code, string lang) in langJson.Languages) { - LocalesEntry locale = json.Locales[i]; - - foreach (string langCode in json.Languages.Where(lang => !locale.Translations.ContainsKey(lang))) + if (string.IsNullOrEmpty(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}'!"); - } + throw new JsonException($"{code} language name missing!"); } - - 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) - throw new JsonException("1 or more locales are invalid! Rebuild locally to fix..."); + string folderPath = projectPath + "assets/Locales/"; - 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(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!"); @@ -100,10 +137,13 @@ namespace Ryujinx.BuildValidationTasks return true; } + struct LanguagesJson + { + public Dictionary Languages { get; set; } + } + struct LocalesJson { - public Dictionary Info { get; set; } - public List Languages { get; set; } public List Locales { get; set; } } diff --git a/src/Ryujinx.Common/Configuration/AntiAliasing.cs b/src/Ryujinx.Common/Configuration/AntiAliasing.cs index 5e3e1b891..1510c23ee 100644 --- a/src/Ryujinx.Common/Configuration/AntiAliasing.cs +++ b/src/Ryujinx.Common/Configuration/AntiAliasing.cs @@ -1,9 +1,8 @@ -using Ryujinx.Common.Utilities; using System.Text.Json.Serialization; namespace Ryujinx.Common.Configuration { - [JsonConverter(typeof(TypedStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum AntiAliasing { None, diff --git a/src/Ryujinx.Common/Configuration/AspectRatioExtensions.cs b/src/Ryujinx.Common/Configuration/AspectRatioExtensions.cs index f3a9e1646..23ecd5870 100644 --- a/src/Ryujinx.Common/Configuration/AspectRatioExtensions.cs +++ b/src/Ryujinx.Common/Configuration/AspectRatioExtensions.cs @@ -1,9 +1,8 @@ -using Ryujinx.Common.Utilities; using System.Text.Json.Serialization; namespace Ryujinx.Common.Configuration { - [JsonConverter(typeof(TypedStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum AspectRatio { Fixed4x3, diff --git a/src/Ryujinx.Common/Configuration/BackendThreading.cs b/src/Ryujinx.Common/Configuration/BackendThreading.cs index 4fbb56bcb..198c2eed1 100644 --- a/src/Ryujinx.Common/Configuration/BackendThreading.cs +++ b/src/Ryujinx.Common/Configuration/BackendThreading.cs @@ -1,9 +1,8 @@ -using Ryujinx.Common.Utilities; using System.Text.Json.Serialization; namespace Ryujinx.Common.Configuration { - [JsonConverter(typeof(TypedStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum BackendThreading { Auto, diff --git a/src/Ryujinx.Common/Configuration/GraphicsBackend.cs b/src/Ryujinx.Common/Configuration/GraphicsBackend.cs index e3b4f91b0..ca6298eb5 100644 --- a/src/Ryujinx.Common/Configuration/GraphicsBackend.cs +++ b/src/Ryujinx.Common/Configuration/GraphicsBackend.cs @@ -1,9 +1,8 @@ -using Ryujinx.Common.Utilities; using System.Text.Json.Serialization; namespace Ryujinx.Common.Configuration { - [JsonConverter(typeof(TypedStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum GraphicsBackend { Vulkan, diff --git a/src/Ryujinx.Common/Configuration/GraphicsDebugLevel.cs b/src/Ryujinx.Common/Configuration/GraphicsDebugLevel.cs index dfe28405c..707715bbb 100644 --- a/src/Ryujinx.Common/Configuration/GraphicsDebugLevel.cs +++ b/src/Ryujinx.Common/Configuration/GraphicsDebugLevel.cs @@ -1,9 +1,8 @@ -using Ryujinx.Common.Utilities; using System.Text.Json.Serialization; namespace Ryujinx.Common.Configuration { - [JsonConverter(typeof(TypedStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum GraphicsDebugLevel { None, diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/GamepadInputId.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/GamepadInputId.cs index 3e317c47c..2d4c23ea7 100644 --- a/src/Ryujinx.Common/Configuration/Hid/Controller/GamepadInputId.cs +++ b/src/Ryujinx.Common/Configuration/Hid/Controller/GamepadInputId.cs @@ -1,9 +1,8 @@ -using Ryujinx.Common.Utilities; using System.Text.Json.Serialization; namespace Ryujinx.Common.Configuration.Hid.Controller { - [JsonConverter(typeof(TypedStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum GamepadInputId : byte { Unbound, diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionInputBackendType.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionInputBackendType.cs index fd8391289..fa23e00d3 100644 --- a/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionInputBackendType.cs +++ b/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionInputBackendType.cs @@ -1,9 +1,8 @@ -using Ryujinx.Common.Utilities; using System.Text.Json.Serialization; namespace Ryujinx.Common.Configuration.Hid.Controller.Motion { - [JsonConverter(typeof(TypedStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum MotionInputBackendType : byte { Invalid, diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/StickInputId.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/StickInputId.cs index 8f9539c46..6e9920edd 100644 --- a/src/Ryujinx.Common/Configuration/Hid/Controller/StickInputId.cs +++ b/src/Ryujinx.Common/Configuration/Hid/Controller/StickInputId.cs @@ -1,15 +1,14 @@ -using Ryujinx.Common.Utilities; using System.Text.Json.Serialization; namespace Ryujinx.Common.Configuration.Hid.Controller { - [JsonConverter(typeof(TypedStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum StickInputId : byte { Unbound, Left, Right, - + Count, } } diff --git a/src/Ryujinx.Common/Configuration/Hid/ControllerType.cs b/src/Ryujinx.Common/Configuration/Hid/ControllerType.cs index cf61b79fd..904c6d13d 100644 --- a/src/Ryujinx.Common/Configuration/Hid/ControllerType.cs +++ b/src/Ryujinx.Common/Configuration/Hid/ControllerType.cs @@ -1,4 +1,3 @@ -using Ryujinx.Common.Utilities; using System; 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 [Flags] - [JsonConverter(typeof(TypedStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum ControllerType { None, diff --git a/src/Ryujinx.Common/Configuration/Hid/InputBackendType.cs b/src/Ryujinx.Common/Configuration/Hid/InputBackendType.cs index b8cfcae2d..c3336dc64 100644 --- a/src/Ryujinx.Common/Configuration/Hid/InputBackendType.cs +++ b/src/Ryujinx.Common/Configuration/Hid/InputBackendType.cs @@ -1,9 +1,8 @@ -using Ryujinx.Common.Utilities; using System.Text.Json.Serialization; namespace Ryujinx.Common.Configuration.Hid { - [JsonConverter(typeof(TypedStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum InputBackendType { Invalid, diff --git a/src/Ryujinx.Common/Configuration/Hid/Key.cs b/src/Ryujinx.Common/Configuration/Hid/Key.cs index e3dd8e50c..cf0f4c076 100644 --- a/src/Ryujinx.Common/Configuration/Hid/Key.cs +++ b/src/Ryujinx.Common/Configuration/Hid/Key.cs @@ -1,9 +1,8 @@ -using Ryujinx.Common.Utilities; using System.Text.Json.Serialization; namespace Ryujinx.Common.Configuration.Hid { - [JsonConverter(typeof(TypedStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum Key { Unknown, diff --git a/src/Ryujinx.Common/Configuration/Hid/PlayerIndex.cs b/src/Ryujinx.Common/Configuration/Hid/PlayerIndex.cs index 05e8f3fa4..4675486ef 100644 --- a/src/Ryujinx.Common/Configuration/Hid/PlayerIndex.cs +++ b/src/Ryujinx.Common/Configuration/Hid/PlayerIndex.cs @@ -1,10 +1,9 @@ -using Ryujinx.Common.Utilities; using System.Text.Json.Serialization; namespace Ryujinx.Common.Configuration.Hid { // This enum was duplicated from Ryujinx.HLE.HOS.Services.Hid.PlayerIndex and should be kept identical - [JsonConverter(typeof(TypedStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum PlayerIndex { Player1 = 0, diff --git a/src/Ryujinx.Common/Configuration/MemoryManagerMode.cs b/src/Ryujinx.Common/Configuration/MemoryManagerMode.cs index 93031928f..83d1e8ae2 100644 --- a/src/Ryujinx.Common/Configuration/MemoryManagerMode.cs +++ b/src/Ryujinx.Common/Configuration/MemoryManagerMode.cs @@ -1,9 +1,8 @@ -using Ryujinx.Common.Utilities; using System.Text.Json.Serialization; namespace Ryujinx.Common.Configuration { - [JsonConverter(typeof(TypedStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum MemoryManagerMode : byte { SoftwarePageTable, diff --git a/src/Ryujinx.Common/Configuration/ScalingFilter.cs b/src/Ryujinx.Common/Configuration/ScalingFilter.cs index 474685d49..9040b1be0 100644 --- a/src/Ryujinx.Common/Configuration/ScalingFilter.cs +++ b/src/Ryujinx.Common/Configuration/ScalingFilter.cs @@ -1,9 +1,8 @@ -using Ryujinx.Common.Utilities; using System.Text.Json.Serialization; namespace Ryujinx.Common.Configuration { - [JsonConverter(typeof(TypedStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum ScalingFilter { Bilinear, diff --git a/src/Ryujinx.Common/Logging/LogClass.cs b/src/Ryujinx.Common/Logging/LogClass.cs index eb0821db6..7c6810599 100644 --- a/src/Ryujinx.Common/Logging/LogClass.cs +++ b/src/Ryujinx.Common/Logging/LogClass.cs @@ -1,9 +1,8 @@ -using Ryujinx.Common.Utilities; using System.Text.Json.Serialization; namespace Ryujinx.Common.Logging { - [JsonConverter(typeof(TypedStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum LogClass { Application, diff --git a/src/Ryujinx.Common/Logging/LogLevel.cs b/src/Ryujinx.Common/Logging/LogLevel.cs index 54261cee5..282b07111 100644 --- a/src/Ryujinx.Common/Logging/LogLevel.cs +++ b/src/Ryujinx.Common/Logging/LogLevel.cs @@ -1,9 +1,8 @@ -using Ryujinx.Common.Utilities; using System.Text.Json.Serialization; namespace Ryujinx.Common.Logging { - [JsonConverter(typeof(TypedStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum LogLevel { Debug, diff --git a/src/Ryujinx.Common/Memory/MemoryStreamManager.cs b/src/Ryujinx.Common/Memory/MemoryStreamManager.cs index 834210e07..88f5956e9 100644 --- a/src/Ryujinx.Common/Memory/MemoryStreamManager.cs +++ b/src/Ryujinx.Common/Memory/MemoryStreamManager.cs @@ -7,6 +7,9 @@ namespace Ryujinx.Common.Memory { private static readonly RecyclableMemoryStreamManager _shared = new(); + private static readonly ObjectPool _streamPool = + new(() => new RecyclableMemoryStream(_shared, Guid.NewGuid(), null, 0)); + /// /// We don't expose the RecyclableMemoryStreamManager directly because version 2.x /// returns them as MemoryStream. This Shared class is here to a) offer only the GetStream() versions we use @@ -19,7 +22,12 @@ namespace Ryujinx.Common.Memory /// /// A RecyclableMemoryStream public static RecyclableMemoryStream GetStream() - => new(_shared); + { + RecyclableMemoryStream stream = _streamPool.Allocate(); + stream.SetLength(0); + + return stream; + } /// /// Retrieve a new MemoryStream object with the contents copied from the provided @@ -55,7 +63,8 @@ namespace Ryujinx.Common.Memory RecyclableMemoryStream stream = null; try { - stream = new RecyclableMemoryStream(_shared, id, tag, buffer.Length); + stream = _streamPool.Allocate(); + stream.SetLength(0); stream.Write(buffer); stream.Position = 0; return stream; @@ -83,7 +92,8 @@ namespace Ryujinx.Common.Memory RecyclableMemoryStream stream = null; try { - stream = new RecyclableMemoryStream(_shared, id, tag, count); + stream = _streamPool.Allocate(); + stream.SetLength(0); stream.Write(buffer, offset, count); stream.Position = 0; return stream; @@ -94,6 +104,11 @@ namespace Ryujinx.Common.Memory throw; } } + + public static void ReleaseStream(RecyclableMemoryStream stream) + { + _streamPool.Release(stream); + } } } } diff --git a/src/Ryujinx.Common/Utilities/EmbeddedResources.cs b/src/Ryujinx.Common/Utilities/EmbeddedResources.cs index 45bb7d537..35cfa0a69 100644 --- a/src/Ryujinx.Common/Utilities/EmbeddedResources.cs +++ b/src/Ryujinx.Common/Utilities/EmbeddedResources.cs @@ -127,7 +127,7 @@ namespace Ryujinx.Common public static string[] GetAllAvailableResources(string path, string ext = "") { return ResolveManifestPath(path).Item1.GetManifestResourceNames() - .Where(r => r.EndsWith(ext)) + .Where(r => r.StartsWith(path.Replace('/', '.')) && r.EndsWith(ext)) .ToArray(); } diff --git a/src/Ryujinx.Common/Utilities/OsUtils.cs b/src/Ryujinx.Common/Utilities/OsUtils.cs index a0791b092..29c6e187c 100644 --- a/src/Ryujinx.Common/Utilities/OsUtils.cs +++ b/src/Ryujinx.Common/Utilities/OsUtils.cs @@ -20,5 +20,21 @@ namespace Ryujinx.Common.Utilities 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); + } + } } } diff --git a/src/Ryujinx.Common/Utilities/TypedStringEnumConverter.cs b/src/Ryujinx.Common/Utilities/TypedStringEnumConverter.cs deleted file mode 100644 index d7eb3d556..000000000 --- a/src/Ryujinx.Common/Utilities/TypedStringEnumConverter.cs +++ /dev/null @@ -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 -{ - /// - /// Specifies that value of will be serialized as string in JSONs - /// - /// - /// Trimming friendly alternative to . - /// Get rid of this converter if dotnet supports similar functionality out of the box. - /// - /// Type of enum to serialize - public sealed class TypedStringEnumConverter : JsonConverter 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()); - } - } -} diff --git a/src/Ryujinx.Graphics.GAL/IPipeline.cs b/src/Ryujinx.Graphics.GAL/IPipeline.cs index b8409a573..da20da870 100644 --- a/src/Ryujinx.Graphics.GAL/IPipeline.cs +++ b/src/Ryujinx.Graphics.GAL/IPipeline.cs @@ -82,7 +82,7 @@ namespace Ryujinx.Graphics.GAL void SetRasterizerDiscard(bool discard); void SetRenderTargetColorMasks(ReadOnlySpan componentMask); - void SetRenderTargets(ITexture[] colors, ITexture depthStencil); + void SetRenderTargets(Span colors, ITexture depthStencil); void SetScissors(ReadOnlySpan> regions); diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetRenderTargetsCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetRenderTargetsCommand.cs index ca7c8c8c2..2641ae528 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetRenderTargetsCommand.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetRenderTargetsCommand.cs @@ -1,5 +1,6 @@ using Ryujinx.Graphics.GAL.Multithreading.Model; using Ryujinx.Graphics.GAL.Multithreading.Resources; +using System; using System.Buffers; namespace Ryujinx.Graphics.GAL.Multithreading.Commands @@ -8,11 +9,13 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands { public static readonly ArrayPool ArrayPool = ArrayPool.Create(512, 50); public readonly CommandType CommandType => CommandType.SetRenderTargets; + private int _colorsCount; private TableRef _colors; private TableRef _depthStencil; - public void Set(TableRef colors, TableRef depthStencil) + public void Set(int colorsCount, TableRef colors, TableRef depthStencil) { + _colorsCount = colorsCount; _colors = colors; _depthStencil = depthStencil; } @@ -20,16 +23,15 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands public static void Run(ref SetRenderTargetsCommand command, ThreadedRenderer threaded, IRenderer renderer) { ITexture[] colors = command._colors.Get(threaded); - ITexture[] colorsCopy = ArrayPool.Rent(colors.Length); + Span 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(threaded)?.Base); + renderer.Pipeline.SetRenderTargets(colorsSpan, command._depthStencil.GetAs(threaded)?.Base); - ArrayPool.Return(colorsCopy); ArrayPool.Return(colors); } } diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs index ea3fd1e11..6873574b7 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs @@ -267,12 +267,12 @@ namespace Ryujinx.Graphics.GAL.Multithreading _renderer.QueueCommand(); } - public unsafe void SetRenderTargets(ITexture[] colors, ITexture depthStencil) + public unsafe void SetRenderTargets(Span colors, ITexture depthStencil) { ITexture[] colorsCopy = SetRenderTargetsCommand.ArrayPool.Rent(colors.Length); - colors.CopyTo(colorsCopy, 0); + colors.CopyTo(colorsCopy.AsSpan()); - _renderer.New()->Set(Ref(colorsCopy), Ref(depthStencil)); + _renderer.New()->Set(colors.Length, Ref(colorsCopy), Ref(depthStencil)); _renderer.QueueCommand(); } diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs index 3c179da36..66ac31ab4 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs @@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading /// 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 QueueCount = 10000; diff --git a/src/Ryujinx.Graphics.Gpu/GpuContext.cs b/src/Ryujinx.Graphics.Gpu/GpuContext.cs index 8b1277c47..7ee32e83d 100644 --- a/src/Ryujinx.Graphics.Gpu/GpuContext.cs +++ b/src/Ryujinx.Graphics.Gpu/GpuContext.cs @@ -404,9 +404,12 @@ namespace Ryujinx.Graphics.Gpu 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) diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs index fe22b9e63..1f2ee1a47 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs @@ -411,7 +411,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// flushes often enough, which is determined by the flush balance. /// /// - public void SyncPreAction(bool syncpoint) + public bool SyncPreAction(bool syncpoint) { if (syncpoint || NextSyncCopies()) { @@ -421,6 +421,8 @@ namespace Ryujinx.Graphics.Gpu.Image _registeredBufferSync = _modifiedSync; } } + + return true; } /// diff --git a/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs b/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs index 277a30689..3bf02f54d 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs @@ -15,7 +15,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Buffer, used to store vertex and index data, uniform and storage buffers, and others. /// - class Buffer : INonOverlappingRange, ISyncActionHandler, IDisposable + class Buffer : INonOverlappingRange, ISyncActionHandler, IDisposable { private const ulong GranularBufferThreshold = 4096; @@ -41,6 +41,9 @@ namespace Ryujinx.Graphics.Gpu.Memory /// End address of the buffer in guest memory. /// public ulong EndAddress => Address + Size; + + public Buffer Next { get; set; } + public Buffer Previous { get; set; } /// /// Increments when the buffer is (partially) unmapped or disposed. @@ -87,6 +90,7 @@ namespace Ryujinx.Graphics.Gpu.Memory private readonly bool _useGranular; private bool _syncActionRegistered; + private bool _bufferInherited; private int _referenceCount = 1; @@ -113,7 +117,7 @@ namespace Ryujinx.Graphics.Gpu.Memory ulong size, BufferStage stage, bool sparseCompatible, - RangeItem[] baseBuffers) + Buffer[] baseBuffers) { _context = context; _physicalMemory = physicalMemory; @@ -134,15 +138,15 @@ namespace Ryujinx.Graphics.Gpu.Memory if (baseBuffers.Length != 0) { baseHandles = new List(); - foreach (RangeItem 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 { - 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. /// /// Start address of the range - /// Size in bytes of the range + /// End address of the range /// True if the range overlaps, false otherwise - 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 Split(ulong splitAddress) { throw new NotImplementedException(); } @@ -389,11 +393,16 @@ namespace Ryujinx.Graphics.Gpu.Memory /// This will copy any buffer ranges designated for pre-flushing. /// /// True if the action is a guest syncpoint - public void SyncPreAction(bool syncpoint) + public bool SyncPreAction(bool syncpoint) { + if (_bufferInherited) + { + return true; + } + if (_referenceCount == 0) { - return; + return false; } if (BackingState.ShouldChangeBacking()) @@ -410,6 +419,8 @@ namespace Ryujinx.Graphics.Gpu.Memory _modifiedRanges?.GetRangesAtSync(Address, Size, _context.SyncNumber, _syncPreRangeAction); } } + + return false; } void SyncPreRangeAction(ulong address, ulong size) @@ -426,10 +437,13 @@ namespace Ryujinx.Graphics.Gpu.Memory { _syncActionRegistered = false; + if (_bufferInherited) + { + return true; + } + if (_useGranular) { - - _modifiedRanges?.GetRanges(Address, Size, _syncRangeAction); } else @@ -453,6 +467,8 @@ namespace Ryujinx.Graphics.Gpu.Memory /// The buffer to inherit from public void InheritModifiedRanges(Buffer from) { + from._bufferInherited = true; + if (from._modifiedRanges is { HasRanges: true }) { if (from._syncActionRegistered && !_syncActionRegistered) diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferBackingState.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferBackingState.cs index a81e7e98f..e674eb1d7 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/BufferBackingState.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferBackingState.cs @@ -56,7 +56,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Parent buffer /// Initial buffer stage /// Buffers to inherit state from - public BufferBackingState(GpuContext context, Buffer parent, BufferStage stage, RangeItem[] baseBuffers) + public BufferBackingState(GpuContext context, Buffer parent, BufferStage stage, Buffer[] baseBuffers) { _size = (int)parent.Size; _systemMemoryType = context.Capabilities.MemoryType; @@ -102,9 +102,9 @@ namespace Ryujinx.Graphics.Gpu.Memory if (baseBuffers.Length != 0) { - foreach (RangeItem item in baseBuffers) + foreach (Buffer item in baseBuffers) { - CombineState(item.Value.BackingState); + CombineState(item.BackingState); } } } diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs index 0d623ff95..83869ed02 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs @@ -79,16 +79,13 @@ namespace Ryujinx.Graphics.Gpu.Memory for (int index = 0; index < range.Count; index++) { MemoryRange subRange = range.GetSubRange(index); - - _buffers.Lock.EnterReadLock(); - Span> overlaps = _buffers.FindOverlapsAsSpan(subRange.Address, subRange.Size); + + ReadOnlySpan overlaps = _buffers.FindOverlapsAsSpan(subRange.Address, subRange.Size); 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 alignedSize = alignedEndAddress - alignedAddress; - Buffer buffer = _buffers.FindOverlap(alignedAddress, alignedSize).Value; + Buffer buffer = _buffers.FindOverlap(alignedAddress, alignedSize); BufferRange bufferRange = buffer.GetRange(alignedAddress, alignedSize, false); alignedSubRanges[i] = new MemoryRange(alignedAddress, alignedSize); @@ -395,7 +392,7 @@ namespace Ryujinx.Graphics.Gpu.Memory 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); physicalBuffers.Add(buffer); @@ -487,10 +484,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// The type of usage that created the buffer private void CreateBufferAligned(ulong address, ulong size, BufferStage stage) { - Buffer newBuffer = null; - - _buffers.Lock.EnterWriteLock(); - Span> overlaps = _buffers.FindOverlapsAsSpan(address, size); + ReadOnlySpan overlaps = _buffers.FindOverlapsAsSpan(address, size); 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. // 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; size = Math.Max(size, growthSize); @@ -535,39 +529,22 @@ namespace Ryujinx.Graphics.Gpu.Memory for (int i = 0; i < overlaps.Length; i++) { - anySparseCompatible |= overlaps[i].Value.SparseCompatible; + anySparseCompatible |= overlaps[i].SparseCompatible; } - RangeItem[] overlapsArray = overlaps.ToArray(); + Buffer[] overlapsArray = overlaps.ToArray(); _buffers.RemoveRange(overlaps[0], overlaps[^1]); - _buffers.Lock.ExitWriteLock(); - ulong newSize = endAddress - address; - newBuffer = CreateBufferAligned(address, newSize, stage, anySparseCompatible, overlapsArray); - } - else - { - _buffers.Lock.ExitWriteLock(); + _buffers.Add(CreateBufferAligned(address, newSize, stage, anySparseCompatible, overlapsArray)); } } else { - _buffers.Lock.ExitWriteLock(); - // No overlap, just create a new buffer. - newBuffer = new(_context, _physicalMemory, address, size, stage, sparseCompatible: false, []); - } - - if (newBuffer is not null) - { - _buffers.Lock.EnterWriteLock(); - - _buffers.Add(newBuffer); - - _buffers.Lock.ExitWriteLock(); + _buffers.Add(new(_context, _physicalMemory, address, size, stage, sparseCompatible: false, [])); } } @@ -583,10 +560,8 @@ namespace Ryujinx.Graphics.Gpu.Memory private void CreateBufferAligned(ulong address, ulong size, BufferStage stage, ulong alignment) { bool sparseAligned = alignment >= SparseBufferAlignmentSize; - Buffer newBuffer = null; - _buffers.Lock.EnterWriteLock(); - Span> overlaps = _buffers.FindOverlapsAsSpan(address, size); + ReadOnlySpan overlaps = _buffers.FindOverlapsAsSpan(address, size); if (overlaps.Length != 0) { @@ -598,7 +573,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (overlaps[0].Address > address || overlaps[0].EndAddress < endAddress || (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. // However, after the range is aligned, it is possible that it @@ -622,35 +597,18 @@ namespace Ryujinx.Graphics.Gpu.Memory ulong newSize = endAddress - address; - RangeItem[] overlapsArray = overlaps.ToArray(); + Buffer[] overlapsArray = overlaps.ToArray(); _buffers.RemoveRange(overlaps[0], overlaps[^1]); - _buffers.Lock.ExitWriteLock(); - - newBuffer = CreateBufferAligned(address, newSize, stage, sparseAligned, overlapsArray); - } - else - { - _buffers.Lock.ExitWriteLock(); + _buffers.Add(CreateBufferAligned(address, newSize, stage, sparseAligned, overlapsArray)); } } else { - _buffers.Lock.ExitWriteLock(); - // No overlap, just create a new buffer. - newBuffer = new(_context, _physicalMemory, address, size, stage, sparseAligned, []); - } - - if (newBuffer is not null) - { - _buffers.Lock.EnterWriteLock(); - - _buffers.Add(newBuffer); - - _buffers.Lock.ExitWriteLock(); - } + _buffers.Add(new(_context, _physicalMemory, address, size, stage, sparseAligned, [])); + } } /// @@ -663,13 +621,13 @@ namespace Ryujinx.Graphics.Gpu.Memory /// The type of usage that created the buffer /// Indicates if the buffer can be used in a sparse buffer mapping /// Buffers overlapping the range - private Buffer CreateBufferAligned(ulong address, ulong size, BufferStage stage, bool sparseCompatible, RangeItem[] overlaps) + private Buffer CreateBufferAligned(ulong address, ulong size, BufferStage stage, bool sparseCompatible, Buffer[] overlaps) { Buffer newBuffer = new(_context, _physicalMemory, address, size, stage, sparseCompatible, overlaps); for (int index = 0; index < overlaps.Length; index++) { - Buffer buffer = overlaps[index].Value; + Buffer buffer = overlaps[index]; int dstOffset = (int)(buffer.Address - newBuffer.Address); @@ -897,7 +855,7 @@ namespace Ryujinx.Graphics.Gpu.Memory { 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); @@ -945,7 +903,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (size != 0) { - buffer = _buffers.FindOverlap(address, size).Value; + buffer = _buffers.FindOverlap(address, size); buffer.CopyFromDependantVirtualBuffers(); buffer.SynchronizeMemory(address, size); @@ -957,7 +915,7 @@ namespace Ryujinx.Graphics.Gpu.Memory } else { - buffer = _buffers.FindOverlapFast(address, 1).Value; + buffer = _buffers.FindOverlapFast(address, 1); } return buffer; @@ -995,7 +953,7 @@ namespace Ryujinx.Graphics.Gpu.Memory { if (size != 0) { - Buffer buffer = _buffers.FindOverlap(address, size).Value; + Buffer buffer = _buffers.FindOverlap(address, size); if (copyBackVirtual) { diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs index 9c50eaf2f..aef04abc6 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs @@ -8,7 +8,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// A range within a buffer that has been modified by the GPU. /// - class BufferModifiedRange : INonOverlappingRange + class BufferModifiedRange : INonOverlappingRange { /// /// 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. /// public ulong EndAddress => Address + Size; + + public BufferModifiedRange Next { get; set; } + public BufferModifiedRange Previous { get; set; } /// /// 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. /// /// Start address of the range - /// Size in bytes of the range + /// End address of the range /// True if the range overlaps, false otherwise - 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 Split(ulong splitAddress) { 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. Lock.EnterReadLock(); - Span> overlaps = FindOverlapsAsSpan(address, size); + ReadOnlySpan overlaps = FindOverlapsAsSpan(address, size); for (int i = 0; i < overlaps.Length; i++) { - BufferModifiedRange overlap = overlaps[i].Value; + BufferModifiedRange overlap = overlaps[i]; if (overlap.Address > address) { @@ -157,7 +160,7 @@ namespace Ryujinx.Graphics.Gpu.Memory ulong syncNumber = _context.SyncNumber; // We may overlap with some existing modified regions. They must be cut into by the new entry. Lock.EnterWriteLock(); - (RangeItem first, RangeItem last) = FindOverlapsAsNodes(address, size); + (BufferModifiedRange first, BufferModifiedRange last) = FindOverlapsAsNodes(address, size); if (first is null) { @@ -170,34 +173,39 @@ namespace Ryujinx.Graphics.Gpu.Memory { if (first.Address == address && first.EndAddress == endAddress) { - first.Value.SyncNumber = syncNumber; - first.Value.Parent = this; + first.SyncNumber = syncNumber; + first.Parent = this; Lock.ExitWriteLock(); return; } if (first.Address < address) { - first.Value.Size = address - first.Address; - Update(first); - if (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 { if (first.EndAddress > endAddress) { - first.Value.Size = first.EndAddress - endAddress; - first.Value.Address = endAddress; - Update(first); + first.Size = first.EndAddress - endAddress; + first.Address = endAddress; } 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; } - BufferModifiedRange buffPre = null; - BufferModifiedRange buffPost = null; - bool extendsPost = false; - bool extendsPre = false; - if (first.Address < address) { - buffPre = new BufferModifiedRange(first.Address, address - first.Address, - first.Value.SyncNumber, first.Value.Parent); - extendsPre = true; + first.Size = address - first.Address; + first = first.Next; } if (last.EndAddress > endAddress) { - buffPost = new BufferModifiedRange(endAddress, last.EndAddress - endAddress, - last.Value.SyncNumber, last.Value.Parent); - extendsPost = true; + last.Size = last.EndAddress - endAddress; + last.Address = endAddress; + last = last.Previous; } - RemoveRange(first, last); - - if (extendsPre) + if (first.Address < last.Address) { - Add(buffPre); + RemoveRange(first.Next, last); + first.Address = address; + first.Size = size; + first.SyncNumber = syncNumber; + first.Parent = this; } - - if (extendsPost) + else if (first.Address == last.Address) { - Add(buffPost); + first.Address = address; + first.Size = size; + first.SyncNumber = syncNumber; + first.Parent = this; } - - Add(new BufferModifiedRange(address, size, syncNumber, this)); + else + { + Add(new BufferModifiedRange(address, size, syncNumber, this)); + } + Lock.ExitWriteLock(); } @@ -252,11 +261,11 @@ namespace Ryujinx.Graphics.Gpu.Memory public void GetRangesAtSync(ulong address, ulong size, ulong syncNumber, Action rangeAction) { Lock.EnterReadLock(); - Span> overlaps = FindOverlapsAsSpan(address, size); + ReadOnlySpan overlaps = FindOverlapsAsSpan(address, size); for (int i = 0; i < overlaps.Length; i++) { - BufferModifiedRange overlap = overlaps[i].Value; + BufferModifiedRange overlap = overlaps[i]; 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. Lock.EnterReadLock(); - RangeItem[] overlaps = FindOverlapsAsArray(address, size, out int length); + BufferModifiedRange[] overlaps = FindOverlapsAsArray(address, size, out int length); Lock.ExitReadLock(); if (length != 0) { for (int i = 0; i < length; i++) { - BufferModifiedRange overlap = overlaps[i].Value; + BufferModifiedRange overlap = overlaps[i]; rangeAction(overlap.Address, overlap.Size); } - ArrayPool>.Shared.Return(overlaps); + ArrayPool.Shared.Return(overlaps); } } @@ -301,7 +310,7 @@ namespace Ryujinx.Graphics.Gpu.Memory public bool HasRange(ulong address, ulong size) { Lock.EnterReadLock(); - RangeItem first = FindOverlapFast(address, size); + BufferModifiedRange first = FindOverlapFast(address, size); bool result = first is not null; Lock.ExitReadLock(); return result; @@ -336,7 +345,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// The start address of the flush range /// The end address of the flush range private void RemoveRangesAndFlush( - RangeItem[] overlaps, + BufferModifiedRange[] overlaps, int rangeCount, long highestDiff, ulong currentSync, @@ -349,7 +358,7 @@ namespace Ryujinx.Graphics.Gpu.Memory for (int i = 0; i < rangeCount; i++) { - BufferModifiedRange overlap = overlaps[i].Value; + BufferModifiedRange overlap = overlaps[i]; long diff = (long)(overlap.SyncNumber - currentSync); @@ -358,7 +367,14 @@ namespace Ryujinx.Graphics.Gpu.Memory ulong clampAddress = Math.Max(address, overlap.Address); 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); } @@ -398,7 +414,7 @@ namespace Ryujinx.Graphics.Gpu.Memory Lock.EnterWriteLock(); // We use the non-span method here because the array is partially modified by the code, which would invalidate a span. - RangeItem[] overlaps = FindOverlapsAsArray(address, size, out int rangeCount); + BufferModifiedRange[] overlaps = FindOverlapsAsArray(address, size, out int rangeCount); if (rangeCount == 0) { @@ -414,7 +430,7 @@ namespace Ryujinx.Graphics.Gpu.Memory for (int i = 0; i < rangeCount; i++) { - BufferModifiedRange overlap = overlaps![i].Value; + BufferModifiedRange overlap = overlaps![i]; long diff = (long)(overlap.SyncNumber - currentSync); @@ -436,7 +452,7 @@ namespace Ryujinx.Graphics.Gpu.Memory RemoveRangesAndFlush(overlaps, rangeCount, highestDiff, currentSync, address, endAddress); - ArrayPool>.Shared.Return(overlaps!); + ArrayPool.Shared.Return(overlaps!); Lock.ExitWriteLock(); } @@ -452,7 +468,9 @@ namespace Ryujinx.Graphics.Gpu.Memory public void InheritRanges(BufferModifiedRangeList ranges, Action registerRangeAction) { ranges.Lock.EnterReadLock(); - BufferModifiedRange[] inheritRanges = ranges.ToArray(); + int rangesCount = ranges.Count; + BufferModifiedRange[] inheritRanges = ArrayPool.Shared.Rent(ranges.Count); + ranges.Items.AsSpan(0, ranges.Count).CopyTo(inheritRanges); ranges.Lock.ExitReadLock(); // Copy over the migration from the previous range list @@ -478,22 +496,26 @@ namespace Ryujinx.Graphics.Gpu.Memory ranges._migrationTarget = this; Lock.EnterWriteLock(); - - foreach (BufferModifiedRange range in inheritRanges) + + for (int i = 0; i < rangesCount; i++) { + BufferModifiedRange range = inheritRanges[i]; Add(range); } Lock.ExitWriteLock(); ulong currentSync = _context.SyncNumber; - foreach (BufferModifiedRange range in inheritRanges) + for (int i = 0; i < rangesCount; i++) { + BufferModifiedRange range = inheritRanges[i]; if (range.SyncNumber != currentSync) { registerRangeAction(range.Address, range.Size); } } + + ArrayPool.Shared.Return(inheritRanges); } /// @@ -534,18 +556,25 @@ namespace Ryujinx.Graphics.Gpu.Memory 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 (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; } - - if (overlap.EndAddress > endAddress) + else 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; Lock.EnterWriteLock(); - (RangeItem first, RangeItem last) = FindOverlapsAsNodes(address, size); + (BufferModifiedRange first, BufferModifiedRange last) = FindOverlapsAsNodes(address, size); if (first is null) { @@ -570,26 +599,24 @@ namespace Ryujinx.Graphics.Gpu.Memory { if (first.Address < address) { - first.Value.Size = address - first.Address; - Update(first); + first.Size = address - first.Address; if (first.EndAddress > endAddress) { Add(new BufferModifiedRange(endAddress, first.EndAddress - endAddress, - first.Value.SyncNumber, first.Value.Parent)); + first.SyncNumber, first.Parent)); } } else { if (first.EndAddress > endAddress) { - first.Value.Size = first.EndAddress - endAddress; - first.Value.Address = endAddress; - Update(first); + first.Size = first.EndAddress - endAddress; + first.Address = endAddress; } else { - Remove(first.Value); + Remove(first); } } @@ -605,14 +632,14 @@ namespace Ryujinx.Graphics.Gpu.Memory if (first.Address < address) { buffPre = new BufferModifiedRange(first.Address, address - first.Address, - first.Value.SyncNumber, first.Value.Parent); + first.SyncNumber, first.Parent); extendsPre = true; } if (last.EndAddress > endAddress) { buffPost = new BufferModifiedRange(endAddress, last.EndAddress - endAddress, - last.Value.SyncNumber, last.Value.Parent); + last.SyncNumber, last.Parent); extendsPost = true; } diff --git a/src/Ryujinx.Graphics.Gpu/Memory/VirtualRangeCache.cs b/src/Ryujinx.Graphics.Gpu/Memory/VirtualRangeCache.cs index 1d44ee65f..0339d8617 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/VirtualRangeCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/VirtualRangeCache.cs @@ -15,7 +15,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Represents a GPU virtual memory range. /// - private class VirtualRange : INonOverlappingRange + private class VirtualRange : INonOverlappingRange { /// /// GPU virtual address where the range starts. @@ -32,6 +32,9 @@ namespace Ryujinx.Graphics.Gpu.Memory /// public ulong EndAddress => Address + Size; + public VirtualRange Next { get; set; } + public VirtualRange Previous { get; set; } + /// /// Physical regions where the GPU virtual region is mapped. /// @@ -54,14 +57,14 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Checks if a given range overlaps with the buffer. /// /// Start address of the range - /// Size in bytes of the range + /// End address of the range /// True if the range overlaps, false otherwise - 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 Split(ulong splitAddress) { throw new NotImplementedException(); } @@ -122,7 +125,7 @@ namespace Ryujinx.Graphics.Gpu.Memory ulong originalVa = gpuVa; _virtualRanges.Lock.EnterWriteLock(); - (RangeItem first, RangeItem last) = _virtualRanges.FindOverlapsAsNodes(gpuVa, size); + (VirtualRange first, VirtualRange last) = _virtualRanges.FindOverlapsAsNodes(gpuVa, size); if (first is not null) { @@ -147,8 +150,8 @@ namespace Ryujinx.Graphics.Gpu.Memory } else { - found = first.Value.Range.Count == 1 || IsSparseAligned(first.Value.Range); - range = first.Value.Range.Slice(gpuVa - first.Address, size); + found = first.Range.Count == 1 || IsSparseAligned(first.Range); + range = first.Range.Slice(gpuVa - first.Address, size); } } else diff --git a/src/Ryujinx.Graphics.Gpu/Synchronization/ISyncActionHandler.cs b/src/Ryujinx.Graphics.Gpu/Synchronization/ISyncActionHandler.cs index d470d2f07..b26ed8ad7 100644 --- a/src/Ryujinx.Graphics.Gpu/Synchronization/ISyncActionHandler.cs +++ b/src/Ryujinx.Graphics.Gpu/Synchronization/ISyncActionHandler.cs @@ -17,6 +17,6 @@ namespace Ryujinx.Graphics.Gpu.Synchronization /// Action to be performed immediately before sync is created. /// /// True if the action is a guest syncpoint - void SyncPreAction(bool syncpoint) { } + bool SyncPreAction(bool syncpoint) { return true; } } } diff --git a/src/Ryujinx.Graphics.OpenGL/Pipeline.cs b/src/Ryujinx.Graphics.OpenGL/Pipeline.cs index c8ca02140..e58e6f2b9 100644 --- a/src/Ryujinx.Graphics.OpenGL/Pipeline.cs +++ b/src/Ryujinx.Graphics.OpenGL/Pipeline.cs @@ -1166,7 +1166,7 @@ namespace Ryujinx.Graphics.OpenGL } } - public void SetRenderTargets(ITexture[] colors, ITexture depthStencil) + public void SetRenderTargets(Span colors, ITexture depthStencil) { EnsureFramebuffer(); diff --git a/src/Ryujinx.Graphics.RenderDocApi/Capture.cs b/src/Ryujinx.Graphics.RenderDocApi/Capture.cs new file mode 100644 index 000000000..dd75bc120 --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/Capture.cs @@ -0,0 +1,12 @@ +using System; + +namespace Ryujinx.Graphics.RenderDocApi +{ + public readonly record struct Capture(int Index, string FileName, DateTime Timestamp) + { + public void SetComments(string comments) + { + RenderDoc.SetCaptureFileComments(FileName, comments); + } + } +} diff --git a/src/Ryujinx.Graphics.RenderDocApi/CaptureOption.cs b/src/Ryujinx.Graphics.RenderDocApi/CaptureOption.cs new file mode 100644 index 000000000..cd3f860d2 --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/CaptureOption.cs @@ -0,0 +1,100 @@ +// ReSharper disable UnusedMember.Global + +namespace Ryujinx.Graphics.RenderDocApi +{ + public enum CaptureOption + { + /// + /// specifies whether the application is allowed to enable vsync. Default is on. + /// + AllowVsync = 0, + /// + /// specifies whether the application is allowed to enter exclusive fullscreen. Default is on. + /// + AllowFullscreen = 1, + /// + /// specifies whether (where possible) API-specific debugging is enabled. Default is off. + /// + ApiValidation = 2, + /// + /// specifies whether each API call should save a callstack. Default is off. + /// + CaptureCallstacks = 3, + /// + /// specifies whether, if is enabled, callstacks are only saved on actions. Default is off. + /// + CaptureCallstacksOnlyDraws = 4, + /// + /// specifies a delay in seconds after launching a process to pause, to allow debuggers to attach.
+ /// This will only apply to child processes since the delay happens at process startup. Default is 0. + ///
+ DelayForDebugger = 5, + /// + /// specifies whether any mapped memory updates should be bounds-checked for overruns, + /// and uninitialised buffers are initialized to 0xDDDDDDDD to catch use of uninitialised data. + /// Only supported on D3D11 and OpenGL. Default is off. + /// + /// + /// This option is only valid for OpenGL and D3D11. Explicit APIs such as D3D12 and Vulkan do + /// not do the same kind of interception & checking, and undefined contents are really undefined. + /// + VerifyBufferAccess = 6, + /// + /// Hooks any system API calls that create child processes, and injects + /// RenderDoc into them recursively with the same options. + /// + HookIntoChildren = 7, + /// + /// specifies whether all live resources at the time of capture should be included in the capture, + /// even if they are not referenced by the frame. Default is off. + /// + RefAllSources = 8, + /// + /// By default, RenderDoc skips saving initial states for resources where the + /// previous contents don't appear to be used, assuming that writes before + /// reads indicate previous contents aren't used. + /// + /// + /// **NOTE**: As of RenderDoc v1.1 this option has been deprecated. Setting or + /// getting it will be ignored, to allow compatibility with older versions. + /// In v1.1 the option acts as if it's always enabled. + /// + SaveAllInitials = 9, + /// + /// In APIs that allow for the recording of command lists to be replayed later, + /// RenderDoc may choose to not capture command lists before a frame capture is + /// triggered, to reduce overheads. This means any command lists recorded once + /// and replayed many times will not be available and may cause a failure to + /// capture. + /// + /// + /// NOTE: This is only true for APIs where multithreading is difficult or + /// discouraged. Newer APIs like Vulkan and D3D12 will ignore this option + /// and always capture all command lists since the API is heavily oriented + /// around it and the overheads have been reduced by API design. + /// + CaptureAllCmdLists = 10, + /// + /// Mute API debugging output when the option is enabled. + /// + DebugOutputMute = 11, + /// + /// Allow vendor extensions to be used even when they may be + /// incompatible with RenderDoc and cause corrupted replays or crashes. + /// + AllowUnsupportedVendorExtensions = 12, + /// + /// Define a soft memory limit which some APIs may aim to keep overhead under where + /// possible. Anything above this limit will where possible be saved directly to disk during + /// capture.
+ /// This will cause increased disk space use (which may cause a capture to fail if disk space is + /// exhausted) as well as slower capture times. + ///

+ /// Not all memory allocations may be deferred like this so it is not a guarantee of a memory + /// limit. + ///

+ /// Units are in MBs, suggested values would range from 200MB to 1000MB. + ///
+ SoftMemoryLimit = 13, + } +} diff --git a/src/Ryujinx.Graphics.RenderDocApi/InputButton.cs b/src/Ryujinx.Graphics.RenderDocApi/InputButton.cs new file mode 100644 index 000000000..adef8e8e7 --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/InputButton.cs @@ -0,0 +1,83 @@ + +// ReSharper disable UnusedMember.Global +namespace Ryujinx.Graphics.RenderDocApi +{ + public enum InputButton + { + // '0' - '9' matches ASCII values + Key0 = 0x30, + Key1 = 0x31, + Key2 = 0x32, + Key3 = 0x33, + Key4 = 0x34, + Key5 = 0x35, + Key6 = 0x36, + Key7 = 0x37, + Key8 = 0x38, + Key9 = 0x39, + + // 'A' - 'Z' matches ASCII values + A = 0x41, + B = 0x42, + C = 0x43, + D = 0x44, + E = 0x45, + F = 0x46, + G = 0x47, + H = 0x48, + I = 0x49, + J = 0x4A, + K = 0x4B, + L = 0x4C, + M = 0x4D, + N = 0x4E, + O = 0x4F, + P = 0x50, + Q = 0x51, + R = 0x52, + S = 0x53, + T = 0x54, + U = 0x55, + V = 0x56, + W = 0x57, + X = 0x58, + Y = 0x59, + Z = 0x5A, + + // leave the rest of the ASCII range free + // in case we want to use it later + NonPrintable = 0x100, + + Divide, + Multiply, + Subtract, + Plus, + + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + F11, + F12, + + Home, + End, + Insert, + Delete, + PageUp, + PageDn, + + Backspace, + Tab, + PrtScrn, + Pause, + + Max, + } +} diff --git a/src/Ryujinx.Graphics.RenderDocApi/OverlayBits.cs b/src/Ryujinx.Graphics.RenderDocApi/OverlayBits.cs new file mode 100644 index 000000000..e973e403c --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/OverlayBits.cs @@ -0,0 +1,39 @@ +// ReSharper disable UnusedMember.Global + +using System; + +namespace Ryujinx.Graphics.RenderDocApi +{ + [Flags] + public enum OverlayBits + { + /// + /// This single bit controls whether the overlay is enabled or disabled globally + /// + Enabled = 1 << 0, + /// + /// Show the average framerate over several seconds as well as min/max + /// + FrameRate = 1 << 1, + /// + /// Show the current frame number + /// + FrameNumber = 1 << 2, + /// + /// Show a list of recent captures, and how many captures have been made + /// + CaptureList = 1 << 3, + /// + /// Default values for the overlay mask + /// + Default = Enabled | FrameRate | FrameNumber | CaptureList, + /// + /// Enable all bits + /// + All = ~0, + /// + /// Disable all bits + /// + None = 0 + } +} diff --git a/src/Ryujinx.Graphics.RenderDocApi/README.md b/src/Ryujinx.Graphics.RenderDocApi/README.md new file mode 100644 index 000000000..51f568b28 --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/README.md @@ -0,0 +1,5 @@ +# Ryujinx.Graphics.RenderDocApi + +This is a C# binding for RenderDoc's application API. +This is a source-inclusion of https://github.com/utkumaden/RenderdocSharp. +I didn't use the NuGet package as I had a few minor changes I wanted to make, and I want to learn from it as well via hands-on experience. diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs new file mode 100644 index 000000000..9d1f53957 --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs @@ -0,0 +1,639 @@ +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; + +namespace Ryujinx.Graphics.RenderDocApi +{ + public static unsafe partial class RenderDoc + { + /// + /// True if the API is available. + /// + public static bool IsAvailable => Api != null; + + /// + /// Set the minimum version of the API you require. + /// + /// Set this before you do anything else with the RenderDoc API, including . + public static RenderDocVersion MinimumRequired { get; set; } = RenderDocVersion.Version_1_0_0; + + /// + /// Set to true to assert versions. + /// + public static bool AssertVersionEnabled { get; set; } = true; + + /// + /// Version of the API available. + /// + [MemberNotNullWhen(true, nameof(IsAvailable))] + public static Version? Version + { + get + { + if (!IsAvailable) + return null; + + int major, minor, build; + Api->GetApiVersion(&major, &minor, &build); + return new Version(major, minor, build); + } + } + + /// + /// The current mask which determines what sections of the overlay render on each window. + /// + [RenderDocApiVersion(1, 0)] + public static OverlayBits OverlayBits + { + get => Api->GetOverlayBits(); + set + { + Api->MaskOverlayBits(~value, value); + } + } + + /// + /// The template for new captures.
+ /// The template can either be a relative or absolute path, which determines where captures will be saved and how they will be named. + /// If the path template is 'my_captures/example', then captures saved will be e.g. + /// 'my_captures/example_frame123.rdc' and 'my_captures/example_frame456.rdc'.
+ /// Relative paths will be saved relative to the process’s current working directory.
+ ///
+ /// The default template is in a folder controlled by the UI - initially the system temporary folder, and the filename is the executable’s filename. + [RenderDocApiVersion(1, 0)] + public static string CaptureFilePathTemplate + { + get + { + byte* ptr = Api->GetCaptureFilePathTemplate(); + return Marshal.PtrToStringUTF8((nint)ptr)!; + } + set + { + fixed (byte* ptr = value.ToNullTerminatedByteArray()) + { + Api->SetCaptureFilePathTemplate(ptr); + } + } + } + + /// + /// The amount of frame captures that have been made. + /// + [RenderDocApiVersion(1, 0)] + public static int CaptureCount => Api->GetNumCaptures(); + + /// + /// Checks if the RenderDoc UI is currently connected to this process. + /// + [RenderDocApiVersion(1, 0)] + public static bool IsTargetControlConnected => Api is not null && Api->IsTargetControlConnected() != 0; + + /// + /// Checks if the current frame is capturing. + /// + [RenderDocApiVersion(1, 0)] + public static bool IsFrameCapturing => Api is not null && Api->IsFrameCapturing() != 0; + + /// + /// Set one of the options for tweaking some behaviors of capturing. + /// + /// specifies which capture option should be set. + /// the unsigned integer value to set for the option. + /// Note that each option only takes effect from after it is set - so it is advised to set these options as early as possible, ideally before any graphics API has been initialized. + /// + /// true, if the is valid, and the value set on the option is within valid ranges.
+ /// false, if the option is not a , or the value is not valid for the option. + ///
+ [RenderDocApiVersion(1, 0)] + public static bool SetCaptureOption(CaptureOption option, uint integer) + { + return Api is not null && Api->SetCaptureOptionU32(option, integer) != 0; + } + + /// + /// Set one of the options for tweaking some behaviors of capturing. + /// + /// specifies which capture option should be set. + /// the value to set for the option, converted to a 0 or 1 before setting. + /// Note that each option only takes effect from after it is set - so it is advised to set these options as early as possible, ideally before any graphics API has been initialized. + /// + /// true, if the is valid, and the value set on the option is within valid ranges.
+ /// false, if the option is not a , or the value is not valid for the option. + ///
+ [RenderDocApiVersion(1, 0)] + public static bool SetCaptureOption(CaptureOption option, bool boolean) + => SetCaptureOption(option, boolean ? 1 : 0); + + /// + /// Set one of the options for tweaking some behaviors of capturing. + /// + /// specifies which capture option should be set. + /// the floating point value to set for the option. + /// Note that each option only takes effect from after it is set - so it is advised to set these options as early as possible, ideally before any graphics API has been initialized. + /// + /// true, if the is valid, and the value set on the option is within valid ranges.
+ /// false, if the option is not a , or the value is not valid for the option. + ///
+ [RenderDocApiVersion(1, 0)] + public static bool SetCaptureOption(CaptureOption option, float single) + { + return Api is not null && Api->SetCaptureOptionF32(option, single) != 0; + } + + /// + /// Gets the current value of one of the different options in , writing it to an out parameter. + /// + /// specifies which capture option should be retrieved. + /// the value of the capture option, if the option is a valid enum. Otherwise, . + [RenderDocApiVersion(1, 0)] + public static void GetCaptureOption(CaptureOption option, out uint integer) + { + integer = Api->GetCaptureOptionU32(option); + } + + /// + /// Gets the current value of one of the different options in , writing it to an out parameter. + /// + /// specifies which capture option should be retrieved. + /// the value of the capture option, if the option is a valid enum. Otherwise, -. + [RenderDocApiVersion(1, 0)] + public static void GetCaptureOption(CaptureOption option, out float single) + { + single = Api->GetCaptureOptionF32(option); + } + + /// + /// Gets the current value of one of the different options in , + /// converted to a boolean. + /// + /// specifies which capture option should be retrieved. + /// + /// the value of the capture option, converted to bool, if the option is a valid enum. + /// Otherwise, returns null. + /// + [RenderDocApiVersion(1, 0)] + public static bool? GetCaptureOptionBool(CaptureOption option) + { + if (Api is null) return false; + + uint returnVal = GetCaptureOptionU32(option); + if (returnVal == uint.MaxValue) + return null; + + return returnVal is not 0; + } + + /// + /// Gets the current value of one of the different options in . + /// + /// specifies which capture option should be retrieved. + /// + /// the value of the capture option, if the option is a valid enum. + /// Otherwise, returns . + /// + [RenderDocApiVersion(1, 0)] + public static uint GetCaptureOptionU32(CaptureOption option) => Api->GetCaptureOptionU32(option); + + /// + /// Gets the current value of one of the different options in . + /// + /// specifies which capture option should be retrieved. + /// + /// the value of the capture option, if the option is a valid enum. + /// Otherwise, returns -. + /// + [RenderDocApiVersion(1, 0)] + public static float GetCaptureOptionF32(CaptureOption option) => Api->GetCaptureOptionF32(option); + + /// + /// Changes the key bindings in-application for changing the focussed window. + /// + /// lists the keys to bind. + [RenderDocApiVersion(1, 0)] + public static void SetFocusToggleKeys(ReadOnlySpan buttons) + { + if (Api is null) return; + + fixed (InputButton* ptr = buttons) + { + Api->SetFocusToggleKeys(ptr, buttons.Length); + } + } + + /// + /// Changes the key bindings in-application for triggering a capture on the current window. + /// + /// lists the keys to bind. + [RenderDocApiVersion(1, 0)] + public static void SetCaptureKeys(ReadOnlySpan buttons) + { + if (Api is null) return; + + fixed (InputButton* ptr = buttons) + { + Api->SetCaptureKeys(ptr, buttons.Length); + } + } + + /// + /// Attempts to remove RenderDoc and its hooks from the target process.
+ /// It must be called as early as possible in the process, and will have undefined results + /// if any graphics API functions have been called. + ///
+ [RenderDocApiVersion(1, 0)] + public static void RemoveHooks() + { + if (Api is null) return; + + Api->RemoveHooks(); + } + + /// + /// Remove RenderDoc’s crash handler from the target process.
+ /// If you have your own crash handler that you want to handle any exceptions, + /// RenderDoc’s handler could interfere; so it can be disabled. + ///
+ [RenderDocApiVersion(1, 0)] + public static void UnloadCrashHandler() + { + if (Api is null) return; + + Api->UnloadCrashHandler(); + } + + /// + /// Trigger a capture as if the user had pressed one of the capture hotkeys.
+ /// The capture will be taken from the next frame presented to whichever window is considered current. + ///
+ [RenderDocApiVersion(1, 0)] + public static void TriggerCapture() + { + if (Api is null) return; + + Api->TriggerCapture(); + } + + + /// + /// Gets the details of all frame capture in the current session. + /// This simply calls for each index available as specified by . + /// + /// An immutable array of structs representing RenderDoc Captures. + public static ImmutableArray GetCaptures() + { + if (Api is null) return []; + int captureCount = CaptureCount; + if (captureCount is 0) return []; + + ImmutableArray.Builder captures = ImmutableArray.CreateBuilder(captureCount); + + for (int captureIndex = 0; captureIndex < captureCount; captureIndex++) + { + if (GetCapture(captureIndex) is { } capture) + captures.Add(capture); + } + + return captures.DrainToImmutable(); + } + + /// + /// Gets the details of a particular frame capture, as specified by an index from 0 to - 1. + /// + /// specifies which capture to return the details of. Must be less than the value returned by . + /// A struct representing a RenderDoc Capture. + [RenderDocApiVersion(1, 0)] + public static Capture? GetCapture(int index) + { + if (Api is null) return null; + + int length = 0; + if (Api->GetCapture(index, null, &length, null) == 0) + { + return null; + } + + Span bytes = stackalloc byte[length + 1]; + long timestamp; + + fixed (byte* ptr = bytes) + Api->GetCapture(index, ptr, &length, ×tamp); + + string fileName = Encoding.UTF8.GetString(bytes[length..]); + return new Capture(index, fileName, DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime); + } + + /// + /// Determine the closest matching replay UI executable for the current RenderDoc module, and launch it. + /// + /// if the UI should immediately connect to the application. + /// string to be appended to the command line, e.g. a capture filename. If this parameter is null, the command line will be unmodified. + /// true if the UI was successfully launched; false otherwise. + [RenderDocApiVersion(1, 0)] + public static bool LaunchReplayUI(bool connectTargetControl, string? commandLine = null) + { + if (Api is null) return false; + + if (commandLine == null) + { + return Api->LaunchReplayUI(connectTargetControl ? 1u : 0u, null) != 0; + } + + fixed (byte* ptr = commandLine.ToNullTerminatedByteArray()) + { + return Api->LaunchReplayUI(connectTargetControl ? 1u : 0u, ptr) != 0; + } + } + + /// + /// Explicitly sets which window is considered active.
+ /// The active window is the one that will be captured when the keybind to trigger a capture is pressed. + ///
+ /// a handle to the API ‘device’ object that will be set active. Must be valid. + /// a handle to the platform window handle that will be set active. Must be valid. + [RenderDocApiVersion(1, 0)] + public static void SetActiveWindow(nint hDevice, nint hWindow) + { + if (Api is null) return; + + Api->SetActiveWindow((void*)hDevice, (void*)hWindow); + } + + /// + /// Immediately begin a capture for the specified device/window combination. + /// + /// a handle to the API ‘device’ object that will be set active. May be to wildcard match. + /// a handle to the platform window handle that will be set active. May be to wildcard match. + [RenderDocApiVersion(1, 0)] + public static void StartFrameCapture(nint hDevice, nint hWindow) + { + if (Api is null) return; + + Api->StartFrameCapture((void*)hDevice, (void*)hWindow); + } + + /// + /// Immediately end an active capture for the specified device/window combination. + /// + /// a handle to the API ‘device’ object that will be set active. May be to wildcard match. + /// a handle to the platform window handle that will be set active. May be to wildcard match. + /// true if the capture succeeded; false otherwise. + [RenderDocApiVersion(1, 0)] + public static bool EndFrameCapture(nint hDevice, nint hWindow) + { + if (Api is null) return false; + + return Api->EndFrameCapture((void*)hDevice, (void*)hWindow) != 0; + } + + /// + /// Trigger multiple sequential frame captures as if the user had pressed one of the capture hotkeys before each frame.
+ /// The captures will be taken from the next frames presented to whichever window is considered current.
+ /// Each capture will be taken independently and saved to a separate file, with no reference to the other frames. + ///
+ /// the number of frames to capture. + /// Requires RenderDoc API version 1.1 + [RenderDocApiVersion(1, 1)] + public static void TriggerMultiFrameCapture(uint numFrames) + { + if (Api is null) return; + + AssertAtLeast(1, 1); + Api->TriggerMultiFrameCapture(numFrames); + } + + /// + /// Adds an arbitrary comments field to the most recent capture, + /// which will then be displayed in the UI to anyone opening the capture. + ///

+ /// This is equivalent to calling with a null first (fileName) parameter. + ///
+ /// the comments to set in the capture file. + /// Requires RenderDoc API version 1.2 + public static void SetMostRecentCaptureFileComments(string comments) + { + if (Api is null) return; + + AssertAtLeast(1, 2); + + byte[] commentBytes = comments.ToNullTerminatedByteArray(); + + fixed (byte* pcomment = commentBytes) + { + Api->SetCaptureFileComments((byte*)nint.Zero, pcomment); + } + } + + /// + /// Adds an arbitrary comments field to an existing capture on disk, + /// which will then be displayed in the UI to anyone opening the capture. + /// + /// the path to the capture file to set comments in. If this path is null or an empty string, the most recent capture file that has been created will be used. + /// the comments to set in the capture file. + /// Requires RenderDoc API version 1.2 + [RenderDocApiVersion(1, 2)] + public static void SetCaptureFileComments(string? fileName, string comments) + { + if (Api is null) return; + + AssertAtLeast(1, 2); + + byte[] commentBytes = comments.ToNullTerminatedByteArray(); + + fixed (byte* pcomment = commentBytes) + { + if (fileName is null) + { + Api->SetCaptureFileComments((byte*)nint.Zero, pcomment); + } + else + { + byte[] fileBytes = fileName.ToNullTerminatedByteArray(); + + fixed (byte* pfile = fileBytes) + { + Api->SetCaptureFileComments(pfile, pcomment); + } + } + } + } + + /// + /// Similar to , but the capture contents will be discarded immediately, and not processed and written to disk.
+ /// This will be more efficient than if the frame capture is not needed. + ///
+ /// a handle to the API ‘device’ object that will be set active. May be to wildcard match. + /// a handle to the platform window handle that will be set active. May be to wildcard match. + /// true if the capture was discarded; false if there was an error or no capture was in progress. + /// Requires RenderDoc API version 1.4 + [RenderDocApiVersion(1, 4)] + public static bool DiscardFrameCapture(nint hDevice, nint hWindow) + { + if (Api is null) return false; + + AssertAtLeast(1, 4); + return Api->DiscardFrameCapture((void*)hDevice, (void*)hWindow) != 0; + } + + + /// + /// Requests that the currently connected replay UI raise its window to the top.
+ /// This is only possible if an instance of the replay UI is currently connected, otherwise this method does nothing.
+ /// This can be used in conjunction with and ,
to intelligently handle showing the UI after making a capture.

+ /// Given OS differences, it is not guaranteed that the UI will be successfully raised even if the request is passed on.
+ /// On some systems it may only be highlighted or otherwise indicated to the user. + ///
+ /// true if the request was passed onto the UI successfully; false if there is no UI connected or some other error occurred. + /// Requires RenderDoc API version 1.5 + [RenderDocApiVersion(1, 5)] + public static bool ShowReplayUI() + { + if (Api is null) return false; + + AssertAtLeast(1, 5); + return Api->ShowReplayUI() != 0; + } + + /// + /// Sets a given title for the currently in-progress capture, which will be displayed in the UI.
+ /// This can be used either with a user-defined capture using a manual start and end, + /// or an automatic capture triggered by or a keypress.
+ /// If multiple captures are ongoing at once, the title will be applied to the first capture to end only.
+ /// Any subsequent captures will not get any title unless the function is called again. + /// This function can only be called while a capture is in-progress, + /// after and before .
+ /// If it is called elsewhere it will have no effect. + /// If it is called multiple times within a capture, only the last title will have any effect. + ///
+ /// The title to set for the in-progress capture. + /// Requires RenderDoc API version 1.6 + [RenderDocApiVersion(1, 6)] + public static void SetCaptureTitle(string title) + { + if (Api is null) return; + + AssertAtLeast(1, 6); + fixed (byte* ptr = title.ToNullTerminatedByteArray()) + Api->SetCaptureTitle(ptr); + } + + #region Dynamic Library loading + + /// + /// Reload the internal RenderDoc API structure. Useful for manually refreshing while using process injection. + /// + /// Ignores the existing API function structure and overwrites it with a re-request. + /// The version of the RenderDoc API required by your application. + public static void ReloadApi(bool ignoreAlreadyLoaded = false, RenderDocVersion? requiredVersion = null) + { + if (_loaded && !ignoreAlreadyLoaded) + return; + + lock (typeof(RenderDoc)) + { + // Prevent double loads. + if (_loaded && !ignoreAlreadyLoaded) + return; + + if (requiredVersion.HasValue) + MinimumRequired = requiredVersion.Value; + + _loaded = true; + _api = GetApi(MinimumRequired); + + if (_api != null) + AssertAtLeast(MinimumRequired); + } + } + + private static RenderDocApi* _api = null; + private static bool _loaded; + + private static RenderDocApi* Api + { + get + { + ReloadApi(); + return _api; + } + } + + private static readonly Regex _dynamicLibraryPattern = RenderDocApiDynamicLibraryRegex(); + + private static RenderDocApi* GetApi(RenderDocVersion minimumRequired = RenderDocVersion.Version_1_0_0) + { + foreach (ProcessModule module in Process.GetCurrentProcess().Modules) + { + string moduleName = module.FileName ?? string.Empty; + + if (!_dynamicLibraryPattern.IsMatch(moduleName)) + continue; + + if (!NativeLibrary.TryLoad(moduleName, out nint moduleHandle)) + return null; + + if (!NativeLibrary.TryGetExport(moduleHandle, "RENDERDOC_GetAPI", out nint procAddress)) + return null; + + var RENDERDOC_GetApi = (delegate* unmanaged[Cdecl])procAddress; + + RenderDocApi* api; + return RENDERDOC_GetApi(minimumRequired, &api) != 0 ? api : null; + } + + return null; + } + + private static void AssertAtLeast(RenderDocVersion rdv, [CallerMemberName] string callee = "") + { + Version ver = rdv.SystemVersion; + AssertAtLeast(ver.Major, ver.Minor, ver.Build, callee); + } + + private static void AssertAtLeast(int major, int minor, int patch = 0, [CallerMemberName] string callee = "") + { + if (!AssertVersionEnabled) + return; + + if (Version!.Major < major) + goto fail; + + if (Version.Major > major) + goto success; + if (Version.Minor < minor) + goto fail; + if (Version.Minor > minor) + goto success; + if (Version.Build < patch) + goto fail; + + success: + return; + + fail: + Version minVersion = + typeof(RenderDoc).GetMethod(callee)!.GetCustomAttribute()!.MinVersion; + throw new NotSupportedException( + $"This API was introduced in RenderDoc API {minVersion}. Current API version is {Version}."); + } + + private static byte[] ToNullTerminatedByteArray(this string str, Encoding? encoding = null) + { + encoding ??= Encoding.UTF8; + + return encoding.GetBytes(str + '\0'); + } + + [GeneratedRegex(@"(lib)?renderdoc(\.dll|\.so|\.dylib)(\.\d+)?", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex RenderDocApiDynamicLibraryRegex(); + + #endregion + } +} diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDocApi.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDocApi.cs new file mode 100644 index 000000000..70565b55a --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDocApi.cs @@ -0,0 +1,51 @@ +namespace Ryujinx.Graphics.RenderDocApi +{ +#pragma warning disable CS0649 + internal unsafe struct RenderDocApi + { + public delegate* unmanaged[Cdecl] GetApiVersion; + + public delegate* unmanaged[Cdecl] SetCaptureOptionU32; + public delegate* unmanaged[Cdecl] SetCaptureOptionF32; + public delegate* unmanaged[Cdecl] GetCaptureOptionU32; + public delegate* unmanaged[Cdecl] GetCaptureOptionF32; + + public delegate* unmanaged[Cdecl] SetFocusToggleKeys; + public delegate* unmanaged[Cdecl] SetCaptureKeys; + + public delegate* unmanaged[Cdecl] GetOverlayBits; + public delegate* unmanaged[Cdecl] MaskOverlayBits; + + public delegate* unmanaged[Cdecl] RemoveHooks; + public delegate* unmanaged[Cdecl] UnloadCrashHandler; + public delegate* unmanaged[Cdecl] SetCaptureFilePathTemplate; + public delegate* unmanaged[Cdecl] GetCaptureFilePathTemplate; + + public delegate* unmanaged[Cdecl] GetNumCaptures; + public delegate* unmanaged[Cdecl] GetCapture; + public delegate* unmanaged[Cdecl] TriggerCapture; + public delegate* unmanaged[Cdecl] IsTargetControlConnected; + public delegate* unmanaged[Cdecl] LaunchReplayUI; + + public delegate* unmanaged[Cdecl] SetActiveWindow; + public delegate* unmanaged[Cdecl] StartFrameCapture; + public delegate* unmanaged[Cdecl] IsFrameCapturing; + public delegate* unmanaged[Cdecl] EndFrameCapture; + + // 1.1 + public delegate* unmanaged[Cdecl] TriggerMultiFrameCapture; + + // 1.2 + public delegate* unmanaged[Cdecl] SetCaptureFileComments; + + // 1.3 + public delegate* unmanaged[Cdecl] DiscardFrameCapture; + + // 1.5 + public delegate* unmanaged[Cdecl] ShowReplayUI; + + // 1.6 + public delegate* unmanaged[Cdecl] SetCaptureTitle; + } +#pragma warning restore CS0649 +} diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDocApiVersionAttribute.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDocApiVersionAttribute.cs new file mode 100644 index 000000000..ffbe3701e --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDocApiVersionAttribute.cs @@ -0,0 +1,16 @@ + +using System; + +namespace Ryujinx.Graphics.RenderDocApi +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] + public sealed class RenderDocApiVersionAttribute : Attribute + { + public Version MinVersion { get; } + + public RenderDocApiVersionAttribute(int major, int minor, int patch = 0) + { + MinVersion = new Version(major, minor, patch); + } + } +} diff --git a/src/Ryujinx.Graphics.RenderDocApi/RenderDocVersion.cs b/src/Ryujinx.Graphics.RenderDocApi/RenderDocVersion.cs new file mode 100644 index 000000000..1b386c435 --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/RenderDocVersion.cs @@ -0,0 +1,47 @@ +using System; + +namespace Ryujinx.Graphics.RenderDocApi +{ + public enum RenderDocVersion + { + Version_1_0_0 = 10000, + Version_1_0_1 = 10001, + Version_1_0_2 = 10002, + Version_1_1_0 = 10100, + Version_1_1_1 = 10101, + Version_1_1_2 = 10102, + Version_1_2_0 = 10200, + Version_1_3_0 = 10300, + Version_1_4_0 = 10400, + Version_1_4_1 = 10401, + Version_1_4_2 = 10402, + Version_1_5_0 = 10500, + Version_1_6_0 = 10600, + } + + public static partial class Helpers + { + extension(RenderDocVersion rdv) + { + public Version SystemVersion + { + get + { + int i = (int)rdv; + return new (i / 10000, (i % 10000) / 100, i % 100); + } + } + } + + extension(Version sv) + { + public RenderDocVersion RenderDocVersion + { + get + { + return (RenderDocVersion)(sv.Major * 10000 + sv.Minor * 100 + sv.Build); + } + } + } + } +} diff --git a/src/Ryujinx.Graphics.RenderDocApi/Ryujinx.Graphics.RenderDocApi.csproj b/src/Ryujinx.Graphics.RenderDocApi/Ryujinx.Graphics.RenderDocApi.csproj new file mode 100644 index 000000000..29c1d818a --- /dev/null +++ b/src/Ryujinx.Graphics.RenderDocApi/Ryujinx.Graphics.RenderDocApi.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + disable + enable + true + + diff --git a/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs b/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs index 919c45b9d..6d03fcd0d 100644 --- a/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs +++ b/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs @@ -71,17 +71,31 @@ namespace Ryujinx.Graphics.Vulkan HasDepthStencil = isDepthStencil; } - public FramebufferParams(Device device, ITexture[] colors, ITexture depthStencil) + public FramebufferParams(Device device, ReadOnlySpan colors, ITexture depthStencil) { _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); _attachments = new Auto[count]; _colors = new TextureView[colorsCount]; - _colorsCanonical = colors.Select(color => color is TextureView view && view.Valid ? view : null).ToArray(); AttachmentSamples = new uint[count]; AttachmentFormats = new VkFormat[count]; @@ -165,9 +179,17 @@ namespace Ryujinx.Graphics.Vulkan _totalCount = colors.Length; } - public FramebufferParams Update(ITexture[] colors, ITexture depthStencil) + public FramebufferParams Update(ReadOnlySpan 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); diff --git a/src/Ryujinx.Graphics.Vulkan/Helpers.cs b/src/Ryujinx.Graphics.Vulkan/Helpers.cs new file mode 100644 index 000000000..d29ac3440 --- /dev/null +++ b/src/Ryujinx.Graphics.Vulkan/Helpers.cs @@ -0,0 +1,32 @@ +using Silk.NET.Vulkan; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Graphics.Vulkan +{ + public static class Helpers + { + extension(Vk api) + { + /// + /// C# implementation of the RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE macro from the RenderDoc API header, since we cannot use macros from C#. + /// + /// The dispatch table pointer, which sits as the first pointer-sized object in the memory pointed to by the 's pointer. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void* GetRenderDocDevicePointer() => + api.CurrentInstance is not null + ? api.CurrentInstance.Value.GetRenderDocDevicePointer() + : null; + } + + extension(Instance instance) + { + /// + /// C# implementation of the RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE macro from the RenderDoc API header, since we cannot use macros from C#. + /// + /// The dispatch table pointer, which sits as the first pointer-sized object in the memory pointed to by the 's pointer. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void* GetRenderDocDevicePointer() + => (*((void**)(instance.Handle))); + } + } +} diff --git a/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs b/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs index 40ad7716d..b226ce1f3 100644 --- a/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs +++ b/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs @@ -1,7 +1,7 @@ +using Ryujinx.Common; using Ryujinx.Common.Memory; using Silk.NET.Vulkan; using System; -using System.Buffers; namespace Ryujinx.Graphics.Vulkan { @@ -10,6 +10,8 @@ namespace Ryujinx.Graphics.Vulkan /// class MultiFenceHolder { + public static readonly ObjectPool FencePool = new(() => new FenceHolder[CommandBufferPool.MaxCommandBuffers]); + private const int BufferUsageTrackingGranularity = 4096; public FenceHolder[] Fences { get; } @@ -20,7 +22,7 @@ namespace Ryujinx.Graphics.Vulkan /// public MultiFenceHolder() { - Fences = ArrayPool.Shared.Rent(CommandBufferPool.MaxCommandBuffers); + Fences = FencePool.Allocate(); } /// @@ -29,7 +31,7 @@ namespace Ryujinx.Graphics.Vulkan /// Size of the buffer public MultiFenceHolder(int size) { - Fences = ArrayPool.Shared.Rent(CommandBufferPool.MaxCommandBuffers); + Fences = FencePool.Allocate(); _bufferUsageBitmap = new BufferUsageBitmap(size, BufferUsageTrackingGranularity); } diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs index f2f68378f..0172b5b56 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs @@ -1035,7 +1035,7 @@ namespace Ryujinx.Graphics.Vulkan } } - private void SetRenderTargetsInternal(ITexture[] colors, ITexture depthStencil, bool filterWriteMasked) + private void SetRenderTargetsInternal(Span colors, ITexture depthStencil, bool filterWriteMasked) { CreateFramebuffer(colors, depthStencil, filterWriteMasked); CreateRenderPass(); @@ -1043,7 +1043,7 @@ namespace Ryujinx.Graphics.Vulkan SignalAttachmentChange(); } - public void SetRenderTargets(ITexture[] colors, ITexture depthStencil) + public void SetRenderTargets(Span colors, ITexture depthStencil) { _framebufferUsingColorWriteMask = false; SetRenderTargetsInternal(colors, depthStencil, Gd.IsTBDR); @@ -1389,7 +1389,7 @@ namespace Ryujinx.Graphics.Vulkan _currentPipelineHandle = 0; } - private void CreateFramebuffer(ITexture[] colors, ITexture depthStencil, bool filterWriteMasked) + private void CreateFramebuffer(Span colors, ITexture depthStencil, bool filterWriteMasked) { if (filterWriteMasked) { @@ -1399,7 +1399,7 @@ namespace Ryujinx.Graphics.Vulkan // Just try to remove duplicate attachments. // Save a copy of the array to rebind when mask changes. - void MaskOut() + void MaskOut(ReadOnlySpan colors) { if (!_framebufferUsingColorWriteMask) { @@ -1436,12 +1436,12 @@ namespace Ryujinx.Graphics.Vulkan if (vkBlend.ColorWriteMask == 0) { colors[i] = null; - MaskOut(); + MaskOut(colors); } else if (vkBlend2.ColorWriteMask == 0) { colors[j] = null; - MaskOut(); + MaskOut(colors); } } } diff --git a/src/Ryujinx.Graphics.Vulkan/SyncManager.cs b/src/Ryujinx.Graphics.Vulkan/SyncManager.cs index 149759906..15759b0de 100644 --- a/src/Ryujinx.Graphics.Vulkan/SyncManager.cs +++ b/src/Ryujinx.Graphics.Vulkan/SyncManager.cs @@ -1,6 +1,6 @@ using Ryujinx.Common.Logging; using Silk.NET.Vulkan; -using System.Buffers; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -193,7 +193,8 @@ namespace Ryujinx.Graphics.Vulkan { _firstHandle = first.ID + 1; _handles.RemoveAt(0); - ArrayPool.Shared.Return(first.Waitable.Fences); + Array.Clear(first.Waitable.Fences); + MultiFenceHolder.FencePool.Release(first.Waitable.Fences); first.Waitable = null; } } diff --git a/src/Ryujinx.HLE/Debugger/Debugger.Rcmd.cs b/src/Ryujinx.HLE/Debugger/Debugger.Rcmd.cs index d3094d842..0fb70c6b0 100644 --- a/src/Ryujinx.HLE/Debugger/Debugger.Rcmd.cs +++ b/src/Ryujinx.HLE/Debugger/Debugger.Rcmd.cs @@ -1,43 +1,91 @@ using Gommon; using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Kernel.Memory; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Text; namespace Ryujinx.HLE.Debugger { public partial class Debugger { + private sealed record RcmdEntry(string[] Names, Func 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() { - _rcmdDelegates.Add(["help"], - _ => _rcmdDelegates.Keys - .Where(x => !x[0].Equals("help")) - .Select(x => x.JoinToString('\n')) - .JoinToString('\n') + '\n' - ); - _rcmdDelegates.Add(["get info"], dbgr => dbgr.GetProcessInfo()); - _rcmdDelegates.Add(["backtrace", "bt"], dbgr => dbgr.GetStackTrace()); - _rcmdDelegates.Add(["registers", "reg"], dbgr => dbgr.GetRegisters()); - _rcmdDelegates.Add(["minidump"], dbgr => dbgr.GetMinidump()); + _rcmdDelegates.Add(new RcmdEntry( + ["help"], + (dbgr, _) => _rcmdDelegates + .Where(entry => entry.HelpLines.Length > 0) + .SelectMany(entry => entry.HelpLines) + .JoinToString('\n') + '\n', + Array.Empty())); + + _rcmdDelegates.Add(new RcmdEntry(["get info"], (dbgr, _) => dbgr.GetProcessInfo(), ["get info"])); + _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> _rcmdDelegates = new(); + private static readonly List _rcmdDelegates = []; - public static Func FindRcmdDelegate(string command) + public static string CallRcmdDelegate(Debugger debugger, string command) { - Func searchResult = _ => $"Unknown command: {command}\n"; + string originalCommand = command ?? string.Empty; + string trimmedCommand = originalCommand.Trim(); - foreach ((string[] names, Func dlg) in _rcmdDelegates) + foreach (RcmdEntry entry in _rcmdDelegates) { - if (names.ContainsIgnoreCase(command.Trim())) + foreach (string name in entry.Names) { - searchResult = dlg; - break; + if (trimmedCommand.Equals(name, StringComparison.OrdinalIgnoreCase)) + { + 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() @@ -86,5 +134,181 @@ namespace Ryujinx.HLE.Debugger 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}"; + } } } diff --git a/src/Ryujinx.HLE/Debugger/Gdb/Commands.cs b/src/Ryujinx.HLE/Debugger/Gdb/Commands.cs index 66308003f..0b9e12f71 100644 --- a/src/Ryujinx.HLE/Debugger/Gdb/Commands.cs +++ b/src/Ryujinx.HLE/Debugger/Gdb/Commands.cs @@ -404,9 +404,8 @@ namespace Ryujinx.HLE.Debugger.Gdb string command = Helpers.FromHex(hexCommand); Logger.Debug?.Print(LogClass.GdbStub, $"Received Rcmd: {command}"); - Func rcmd = Debugger.FindRcmdDelegate(command); - - Processor.ReplyHex(rcmd(Debugger)); + string response = Debugger.CallRcmdDelegate(Debugger, command); + Processor.ReplyHex(response); } catch (Exception e) { diff --git a/src/Ryujinx.HLE/FileSystem/ContentManager.cs b/src/Ryujinx.HLE/FileSystem/ContentManager.cs index 9f38de42b..d0fe0f1a7 100644 --- a/src/Ryujinx.HLE/FileSystem/ContentManager.cs +++ b/src/Ryujinx.HLE/FileSystem/ContentManager.cs @@ -483,10 +483,29 @@ namespace Ryujinx.HLE.FileSystem { 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); - File.Copy(filePath, Path.Combine(installDirectory, Path.GetFileName(filePath)), true); + try + { + 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; @@ -501,13 +520,25 @@ namespace Ryujinx.HLE.FileSystem 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); - File.Copy(keysSource, Path.Combine(installDirectory, info.Name), true); - } - else + } + catch + { 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) @@ -985,8 +1016,8 @@ namespace Ryujinx.HLE.FileSystem public static void VerifyKeysFile(string filePath) { // 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 titlePattern = @"^[a-z0-9]{32} = [a-z0-9]{32}$"; + string genericPattern = "^[a-z0-9_]+ = [a-z0-9]+$"; + string titlePattern = "^[a-z0-9]{32} = [a-z0-9]{32}$"; if (File.Exists(filePath)) { @@ -994,24 +1025,13 @@ namespace Ryujinx.HLE.FileSystem string fileName = Path.GetFileName(filePath); string[] lines = File.ReadAllLines(filePath); - bool verified; - switch (fileName) + bool verified = fileName switch { - case "prod.keys": - verified = VerifyKeys(lines, genericPattern); - break; - case "title.keys": - 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."); - } + "prod.keys" or "console.keys" or "dev.keys" => VerifyKeys(lines, genericPattern), + "title.keys" => VerifyKeys(lines, titlePattern), + _ => throw new FormatException( + $"Keys file name \"{fileName}\" not supported. Only \"prod.keys\", \"title.keys\", \"console.keys\", \"dev.keys\" are supported.") + }; if (!verified) { diff --git a/src/Ryujinx.HLE/HOS/Ipc/IpcMessage.cs b/src/Ryujinx.HLE/HOS/Ipc/IpcMessage.cs index 7df2b778f..13a93db39 100644 --- a/src/Ryujinx.HLE/HOS/Ipc/IpcMessage.cs +++ b/src/Ryujinx.HLE/HOS/Ipc/IpcMessage.cs @@ -2,6 +2,7 @@ using Microsoft.IO; using Ryujinx.Common; using Ryujinx.Common.Memory; using System; +using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -37,7 +38,7 @@ namespace Ryujinx.HLE.HOS.Ipc public IpcMessage(ReadOnlySpan data, long cmdPtr) { - using RecyclableMemoryStream ms = MemoryStreamManager.Shared.GetStream(data); + RecyclableMemoryStream ms = MemoryStreamManager.Shared.GetStream(data); BinaryReader reader = new(ms); @@ -123,6 +124,8 @@ namespace Ryujinx.HLE.HOS.Ipc } ObjectIds = []; + + MemoryStreamManager.Shared.ReleaseStream(ms); } public RecyclableMemoryStream GetStream(long cmdPtr, ulong recvListAddr) diff --git a/src/Ryujinx.HLE/HOS/Kernel/Ipc/KBufferDescriptorTable.cs b/src/Ryujinx.HLE/HOS/Kernel/Ipc/KBufferDescriptorTable.cs index 373899b7b..d22cfb469 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Ipc/KBufferDescriptorTable.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Ipc/KBufferDescriptorTable.cs @@ -20,6 +20,15 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc _exchangeBufferDescriptors = new List(MaxInternalBuffersCount); } + public KBufferDescriptorTable Clear() + { + _sendBufferDescriptors.Clear(); + _receiveBufferDescriptors.Clear(); + _exchangeBufferDescriptors.Clear(); + + return this; + } + public Result AddSendBuffer(ulong src, ulong dst, ulong size, MemoryState state) { return Add(_sendBufferDescriptors, src, dst, size, state); diff --git a/src/Ryujinx.HLE/HOS/Kernel/Ipc/KClientSession.cs b/src/Ryujinx.HLE/HOS/Kernel/Ipc/KClientSession.cs index 385f09020..d83e14ba3 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Ipc/KClientSession.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Ipc/KClientSession.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common; using Ryujinx.HLE.HOS.Kernel.Common; using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.HLE.HOS.Kernel.Threading; @@ -32,7 +33,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc { KThread currentThread = KernelStatic.GetCurrentThread(); - KSessionRequest request = new(currentThread, customCmdBuffAddr, customCmdBuffSize); + KSessionRequest request = _parent.ServerSession.RequestPool.Allocate().Set(currentThread, customCmdBuffAddr, customCmdBuffSize); KernelContext.CriticalSection.Enter(); @@ -55,7 +56,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc { 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(); diff --git a/src/Ryujinx.HLE/HOS/Kernel/Ipc/KServerSession.cs b/src/Ryujinx.HLE/HOS/Kernel/Ipc/KServerSession.cs index edc3d819e..f2c22c9f3 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Ipc/KServerSession.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Ipc/KServerSession.cs @@ -10,6 +10,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc { class KServerSession : KSynchronizationObject { + public readonly ObjectPool RequestPool = new(() => new KSessionRequest()); + private static readonly MemoryState[] _ipcMemoryStates = [ MemoryState.IpcBuffer3, @@ -274,6 +276,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc KernelContext.CriticalSection.Leave(); WakeClientThread(request, clientResult); + + RequestPool.Release(request); } if (clientHeader.ReceiveListType < 2 && @@ -627,6 +631,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc CloseAllHandles(clientMsg, serverHeader, clientProcess); FinishRequest(request, clientResult); + + RequestPool.Release(request); } if (clientHeader.ReceiveListType < 2 && @@ -865,6 +871,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc // Unmap buffers from server. FinishRequest(request, clientResult); + + RequestPool.Release(request); return serverResult; } @@ -1098,6 +1106,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc foreach (KSessionRequest request in IterateWithRemovalOfAllRequests()) { FinishRequest(request, KernelResult.PortRemoteClosed); + + RequestPool.Release(request); } } @@ -1117,6 +1127,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc { SendResultToAsyncRequestClient(request, KernelResult.PortRemoteClosed); } + + RequestPool.Release(request); } WakeServerThreads(KernelResult.PortRemoteClosed); diff --git a/src/Ryujinx.HLE/HOS/Kernel/Ipc/KSessionRequest.cs b/src/Ryujinx.HLE/HOS/Kernel/Ipc/KSessionRequest.cs index bc3eef71e..69a0d3a02 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Ipc/KSessionRequest.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Ipc/KSessionRequest.cs @@ -5,18 +5,18 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc { 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 KWritableEvent AsyncEvent { get; } + public KWritableEvent AsyncEvent { get; private set; } - public ulong CustomCmdBuffAddr { get; } - public ulong CustomCmdBuffSize { get; } + public ulong CustomCmdBuffAddr { get; private set; } + public ulong CustomCmdBuffSize { get; private set; } - public KSessionRequest( + public KSessionRequest Set( KThread clientThread, ulong customCmdBuffAddr, ulong customCmdBuffSize, @@ -27,7 +27,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc CustomCmdBuffSize = customCmdBuffSize; AsyncEvent = asyncEvent; - BufferDescriptorTable = new KBufferDescriptorTable(); + BufferDescriptorTable = BufferDescriptorTable?.Clear() ?? new KBufferDescriptorTable(); + + return this; } } } diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/KAddressArbiter.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/KAddressArbiter.cs index 278a9b2ff..c9ac86fc9 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KAddressArbiter.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KAddressArbiter.cs @@ -1,10 +1,8 @@ -using Ryujinx.Common; using Ryujinx.HLE.HOS.Kernel.Common; using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.Horizon.Common; using System; using System.Collections.Generic; -using System.Linq; using System.Threading; namespace Ryujinx.HLE.HOS.Kernel.Threading @@ -12,12 +10,12 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading class KAddressArbiter { private const int HasListenersMask = 0x40000000; - private static readonly ObjectPool _threadArrayPool = new(() => []); private readonly KernelContext _context; - private readonly List _condVarThreads; - private readonly List _arbiterThreads; + private readonly Dictionary> _condVarThreads; + private readonly Dictionary> _arbiterThreads; + private readonly ByDynamicPriority _byDynamicPriority; public KAddressArbiter(KernelContext context) { @@ -25,6 +23,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading _condVarThreads = []; _arbiterThreads = []; + _byDynamicPriority = new ByDynamicPriority(); } public Result ArbitrateLock(int ownerHandle, ulong mutexAddress, int requesterHandle) @@ -140,9 +139,23 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading currentThread.MutexAddress = mutexAddress; currentThread.ThreadHandleForUserMutex = threadHandle; - currentThread.CondVarAddress = condVarAddress; - _condVarThreads.Add(currentThread); + if (_condVarThreads.TryGetValue(condVarAddress, out List 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) { @@ -165,7 +178,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading currentThread.MutexOwner?.RemoveMutexWaiter(currentThread); - _condVarThreads.Remove(currentThread); + _condVarThreads[condVarAddress].Remove(currentThread); _context.CriticalSection.Leave(); @@ -200,13 +213,14 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading { _context.CriticalSection.Enter(); - static bool SignalProcessWideKeyPredicate(KThread thread, ulong address) + int validThreads = 0; + _condVarThreads.TryGetValue(address, out List 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) { KernelTransfer.KernelToUser(address, 0); @@ -315,9 +329,24 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading currentThread.MutexAddress = address; currentThread.WaitingInArbitration = true; + + if (_arbiterThreads.TryGetValue(address, out List 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); if (timeout > 0) @@ -336,7 +365,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading if (currentThread.WaitingInArbitration) { - _arbiterThreads.Remove(currentThread); + _arbiterThreads[address].Remove(currentThread); currentThread.WaitingInArbitration = false; } @@ -392,9 +421,24 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading currentThread.MutexAddress = address; currentThread.WaitingInArbitration = true; + + if (_arbiterThreads.TryGetValue(address, out List 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); if (timeout > 0) @@ -413,7 +457,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading if (currentThread.WaitingInArbitration) { - _arbiterThreads.Remove(currentThread); + _arbiterThreads[address].Remove(currentThread); 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 negative. It is incremented if there are no threads waiting. int waitingCount = 0; - - foreach (KThread thread in _arbiterThreads) + + if (_arbiterThreads.TryGetValue(address, out List threads)) { - if (thread.MutexAddress == address && - ++waitingCount >= count) - { - break; - } + waitingCount = threads.Count; } + if (waitingCount > 0) { @@ -561,55 +602,38 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading thread.WaitingInArbitration = false; } - static bool ArbiterThreadPredecate(KThread thread, ulong address) - { - return thread.MutexAddress == address; - } + _arbiterThreads.TryGetValue(address, out List threads); - WakeThreads(_arbiterThreads, count, RemoveArbiterThread, ArbiterThreadPredecate, address); + if (threads is not null && threads.Count > 0) + { + WakeThreads(threads, count, RemoveArbiterThread); + } } private static int WakeThreads( List threads, int count, - Action removeCallback, - Func predicate, - ulong address = 0) + Action removeCallback) { - KThread[] candidates = _threadArrayPool.Allocate(); - 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 candidatesSpan = candidates.AsSpan(..validCount); - - candidatesSpan.Sort((x, y) => (x.DynamicPriority.CompareTo(y.DynamicPriority))); + int validCount = count > 0 ? Math.Min(count, threads.Count) : threads.Count; - if (count > 0) - { - candidatesSpan = candidatesSpan[..Math.Min(count, candidatesSpan.Length)]; - } - - foreach (KThread thread in candidatesSpan) + for (int i = 0; i < validCount; i++) { + KThread thread = threads[i]; removeCallback(thread); - threads.Remove(thread); } - _threadArrayPool.Release(candidates); - + threads.RemoveRange(0, validCount); + return validCount; } + + private class ByDynamicPriority : IComparer + { + public int Compare(KThread x, KThread y) + { + return x!.DynamicPriority.CompareTo(y!.DynamicPriority); + } + } } } diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs index 64cd4d595..aaa4ccd99 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs @@ -61,8 +61,6 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading public KSynchronizationObject SignaledObj { get; set; } - public ulong CondVarAddress { get; set; } - private ulong _entrypoint; private ThreadStart _customThreadStart; private bool _forcedUnschedulable; diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/AccountState.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/AccountState.cs index 0e35b4812..cb61f6e3c 100644 --- a/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/AccountState.cs +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/Types/AccountState.cs @@ -1,9 +1,8 @@ -using Ryujinx.Common.Utilities; using System.Text.Json.Serialization; namespace Ryujinx.HLE.HOS.Services.Account.Acc { - [JsonConverter(typeof(TypedStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum AccountState { Closed, diff --git a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/ISelfController.cs b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/ISelfController.cs index 8e0f515ba..7aac6f3ea 100644 --- a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/ISelfController.cs +++ b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/ISelfController.cs @@ -416,7 +416,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys return ResultCode.InvalidParameters; } - Logger.Stub?.PrintStub(LogClass.ServiceAm, new { albumReportOption }); + context.Device.UIHandler.TakeScreenshot(); return ResultCode.Success; } diff --git a/src/Ryujinx.HLE/HOS/Services/IpcService.cs b/src/Ryujinx.HLE/HOS/Services/IpcService.cs index 4c354ebc6..c7dee64fb 100644 --- a/src/Ryujinx.HLE/HOS/Services/IpcService.cs +++ b/src/Ryujinx.HLE/HOS/Services/IpcService.cs @@ -23,6 +23,9 @@ namespace Ryujinx.HLE.HOS.Services private int _selfId; 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) { Stopwatch sw = Stopwatch.StartNew(); @@ -146,7 +149,9 @@ namespace Ryujinx.HLE.HOS.Services { 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 { @@ -196,7 +201,9 @@ namespace Ryujinx.HLE.HOS.Services { 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 { diff --git a/src/Ryujinx.HLE/HOS/Services/Nv/INvDrvServices.cs b/src/Ryujinx.HLE/HOS/Services/Nv/INvDrvServices.cs index 598c7e6e2..ed14b3e15 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nv/INvDrvServices.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nv/INvDrvServices.cs @@ -59,6 +59,10 @@ namespace Ryujinx.HLE.HOS.Services.Nv // TODO: This should call set:sys::GetDebugModeFlag private readonly bool _debugModeEnabled = false; + + private byte[] _ioctl2Buffer = []; + private byte[] _ioctlArgumentBuffer = []; + private byte[] _ioctl3Buffer = []; 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)) { - 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); } - else - { - arguments = arguments.ToArray(); - } } else if (isWrite) { - byte[] outputData = new byte[outputDataSize]; - - arguments = new Span(outputData); + if (_ioctlArgumentBuffer.Length < (int)outputDataSize) + { + Array.Resize(ref _ioctlArgumentBuffer, (int)outputDataSize); + } + + arguments = _ioctlArgumentBuffer.AsSpan(0, (int)outputDataSize); } 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); - - arguments = new Span(temp); + context.Memory.Read(inputDataPosition, arguments); + } } return NvResult.Success; @@ -270,7 +285,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv 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 arguments); - byte[] inlineInBuffer = null; - if (!context.Memory.TryReadUnsafe(inlineInBufferPosition, (int)inlineInBufferSize, out Span inlineInBufferSpan)) { - inlineInBuffer = _byteArrayPool.Rent((int)inlineInBufferSize); - inlineInBufferSpan = inlineInBuffer; - context.Memory.Read(inlineInBufferPosition, inlineInBufferSpan[..(int)inlineInBufferSize]); + if (_ioctl2Buffer.Length < (int)inlineInBufferSize) + { + Array.Resize(ref _ioctl2Buffer, (int)inlineInBufferSize); + } + + inlineInBufferSpan = _ioctl2Buffer.AsSpan(0, (int)inlineInBufferSize); + context.Memory.Read(inlineInBufferPosition, inlineInBufferSpan); } if (errorCode == NvResult.Success) @@ -489,7 +506,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv if (errorCode == NvResult.Success) { - NvInternalResult internalResult = deviceFile.Ioctl2(ioctlCommand, arguments, inlineInBufferSpan[..(int)inlineInBufferSize]); + NvInternalResult internalResult = deviceFile.Ioctl2(ioctlCommand, arguments, inlineInBufferSpan); if (internalResult == NvInternalResult.NotImplemented) { @@ -500,15 +517,10 @@ namespace Ryujinx.HLE.HOS.Services.Nv 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); @@ -531,13 +543,15 @@ namespace Ryujinx.HLE.HOS.Services.Nv errorCode = GetIoctlArgument(context, ioctlCommand, out Span arguments); - byte[] inlineOutBuffer = null; - if (!context.Memory.TryReadUnsafe(inlineOutBufferPosition, (int)inlineOutBufferSize, out Span inlineOutBufferSpan)) { - inlineOutBuffer = _byteArrayPool.Rent((int)inlineOutBufferSize); - inlineOutBufferSpan = inlineOutBuffer; - context.Memory.Read(inlineOutBufferPosition, inlineOutBufferSpan[..(int)inlineOutBufferSize]); + if (_ioctl3Buffer.Length < (int)inlineOutBufferSize) + { + Array.Resize(ref _ioctl3Buffer, (int)inlineOutBufferSize); + } + + inlineOutBufferSpan = _ioctl3Buffer.AsSpan(0, (int)inlineOutBufferSize); + context.Memory.Read(inlineOutBufferPosition, inlineOutBufferSpan); } if (errorCode == NvResult.Success) @@ -546,7 +560,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv if (errorCode == NvResult.Success) { - NvInternalResult internalResult = deviceFile.Ioctl3(ioctlCommand, arguments, inlineOutBufferSpan[..(int)inlineOutBufferSize]); + NvInternalResult internalResult = deviceFile.Ioctl3(ioctlCommand, arguments, inlineOutBufferSpan); if (internalResult == NvInternalResult.NotImplemented) { @@ -557,16 +571,11 @@ namespace Ryujinx.HLE.HOS.Services.Nv if ((ioctlCommand.DirectionValue & NvIoctl.Direction.Write) != 0) { - context.Memory.Write(context.Request.GetBufferType0x22(0).Position, arguments.ToArray()); - context.Memory.Write(inlineOutBufferPosition, inlineOutBufferSpan[..(int)inlineOutBufferSize].ToArray()); + context.Memory.Write(context.Request.GetBufferType0x22(0).Position, arguments); + context.Memory.Write(inlineOutBufferPosition, inlineOutBufferSpan); } } } - - if (inlineOutBuffer is not null) - { - _byteArrayPool.Return(inlineOutBuffer); - } } context.ResponseData.Write((uint)errorCode); diff --git a/src/Ryujinx.HLE/HOS/Services/ServerBase.cs b/src/Ryujinx.HLE/HOS/Services/ServerBase.cs index dcc01bf38..a54dc637e 100644 --- a/src/Ryujinx.HLE/HOS/Services/ServerBase.cs +++ b/src/Ryujinx.HLE/HOS/Services/ServerBase.cs @@ -454,8 +454,9 @@ namespace Ryujinx.HLE.HOS.Services response.RawData = _responseDataStream.ToArray(); - using RecyclableMemoryStream responseStream = response.GetStreamTipc(); + RecyclableMemoryStream responseStream = response.GetStreamTipc(); _selfProcess.CpuMemory.Write(_selfThread.TlsAddress, responseStream.GetReadOnlySequence()); + MemoryStreamManager.Shared.ReleaseStream(responseStream); } else { @@ -464,8 +465,9 @@ namespace Ryujinx.HLE.HOS.Services 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()); + MemoryStreamManager.Shared.ReleaseStream(responseStream); } return shouldReply; diff --git a/src/Ryujinx.HLE/UI/IHostUIHandler.cs b/src/Ryujinx.HLE/UI/IHostUIHandler.cs index b5c5cb168..79b479d8a 100644 --- a/src/Ryujinx.HLE/UI/IHostUIHandler.cs +++ b/src/Ryujinx.HLE/UI/IHostUIHandler.cs @@ -68,5 +68,10 @@ namespace Ryujinx.HLE.UI /// Displays the player select dialog and returns the selected profile. /// UserProfile ShowPlayerSelectDialog(); + + /// + /// Takes a screenshot from the current renderer and saves it in the screenshots folder. + /// + void TakeScreenshot(); } } diff --git a/src/Ryujinx.Horizon/Sdk/OsTypes/Impl/MultiWaitImpl.cs b/src/Ryujinx.Horizon/Sdk/OsTypes/Impl/MultiWaitImpl.cs index d723e5322..412337b6b 100644 --- a/src/Ryujinx.Horizon/Sdk/OsTypes/Impl/MultiWaitImpl.cs +++ b/src/Ryujinx.Horizon/Sdk/OsTypes/Impl/MultiWaitImpl.cs @@ -19,6 +19,9 @@ namespace Ryujinx.Horizon.Sdk.OsTypes.Impl private int _waitingThreadHandle; private MultiWaitHolderBase _signaledHolder; + + ObjectPool _objectHandlePool = new(() => new int[64]); + ObjectPool _objectPool = new(() => new MultiWaitHolderBase[64]); public long CurrentTime { get; private set; } @@ -76,11 +79,15 @@ namespace Ryujinx.Horizon.Sdk.OsTypes.Impl private MultiWaitHolderBase WaitAnyHandleImpl(bool infinite, long timeout) { - Span objectHandles = new int[64]; + int[] objectHandles = _objectHandlePool.Allocate(); + Span objectHandlesSpan = objectHandles; + objectHandlesSpan.Clear(); - Span objects = new MultiWaitHolderBase[64]; + MultiWaitHolderBase[] objects = _objectPool.Allocate(); + Span objectsSpan = objects; + objectsSpan.Clear(); - int count = FillObjectsArray(objectHandles, objects); + int count = FillObjectsArray(objectHandlesSpan, objectsSpan); long endTime = infinite ? long.MaxValue : PerformanceCounter.ElapsedMilliseconds * 1000000; @@ -98,7 +105,7 @@ namespace Ryujinx.Horizon.Sdk.OsTypes.Impl } else { - index = WaitSynchronization(objectHandles[..count], minTimeout); + index = WaitSynchronization(objectHandlesSpan[..count], minTimeout); DebugUtil.Assert(index != WaitInvalid); } @@ -116,12 +123,18 @@ namespace Ryujinx.Horizon.Sdk.OsTypes.Impl { _signaledHolder = minTimeoutObject; + _objectHandlePool.Release(objectHandles); + _objectPool.Release(objects); + return _signaledHolder; } } } else { + _objectHandlePool.Release(objectHandles); + _objectPool.Release(objects); + return null; } @@ -131,6 +144,9 @@ namespace Ryujinx.Horizon.Sdk.OsTypes.Impl { if (_signaledHolder != null) { + _objectHandlePool.Release(objectHandles); + _objectPool.Release(objects); + return _signaledHolder; } } @@ -139,8 +155,11 @@ namespace Ryujinx.Horizon.Sdk.OsTypes.Impl default: lock (_lock) { - _signaledHolder = objects[index]; + _signaledHolder = objectsSpan[index]; + _objectHandlePool.Release(objectHandles); + _objectPool.Release(objects); + return _signaledHolder; } } diff --git a/src/Ryujinx.Input/HLE/NpadController.cs b/src/Ryujinx.Input/HLE/NpadController.cs index dd8907a4b..84f9e89ab 100644 --- a/src/Ryujinx.Input/HLE/NpadController.cs +++ b/src/Ryujinx.Input/HLE/NpadController.cs @@ -532,8 +532,6 @@ namespace Ryujinx.Input.HLE hidKeyboard.Modifier |= value << entry.Target; } - - ArrayPool.Shared.Return(keyboardState.KeysState); return hidKeyboard; diff --git a/src/Ryujinx.Input/HLE/NpadManager.cs b/src/Ryujinx.Input/HLE/NpadManager.cs index 866504128..f2936aa72 100644 --- a/src/Ryujinx.Input/HLE/NpadManager.cs +++ b/src/Ryujinx.Input/HLE/NpadManager.cs @@ -20,7 +20,6 @@ namespace Ryujinx.Input.HLE { public class NpadManager : IDisposable { - private static readonly ObjectPool> _hleMotionStatesPool = new (() => new List(NpadDevices.MaxControllers)); private readonly CemuHookClient _cemuHookClient; private readonly Lock _lock = new(); @@ -40,6 +39,9 @@ namespace Ryujinx.Input.HLE private bool _enableKeyboard; private bool _enableMouse; private Switch _device; + + private readonly List _hleInputStates = []; + private readonly List _hleMotionStates = new(NpadDevices.MaxControllers); public NpadManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver, IGamepadDriver mouseDriver) { @@ -217,8 +219,8 @@ namespace Ryujinx.Input.HLE { lock (_lock) { - List hleInputStates = []; - List hleMotionStates = _hleMotionStatesPool.Allocate(); + _hleInputStates.Clear(); + _hleMotionStates.Clear(); KeyboardInput? hleKeyboardInput = null; @@ -260,14 +262,14 @@ namespace Ryujinx.Input.HLE inputState.PlayerId = playerIndex; motionState.Item1.PlayerId = playerIndex; - hleInputStates.Add(inputState); - hleMotionStates.Add(motionState.Item1); + _hleInputStates.Add(inputState); + _hleMotionStates.Add(motionState.Item1); if (isJoyconPair && !motionState.Item2.Equals(default)) { motionState.Item2.PlayerId = playerIndex; - hleMotionStates.Add(motionState.Item2); + _hleMotionStates.Add(motionState.Item2); } } @@ -276,8 +278,8 @@ namespace Ryujinx.Input.HLE hleKeyboardInput = NpadController.GetHLEKeyboardInput(_keyboardDriver); } - _device.Hid.Npads.Update(hleInputStates); - _device.Hid.Npads.UpdateSixAxis(hleMotionStates); + _device.Hid.Npads.Update(_hleInputStates); + _device.Hid.Npads.UpdateSixAxis(_hleMotionStates); if (hleKeyboardInput.HasValue) { @@ -328,10 +330,7 @@ namespace Ryujinx.Input.HLE _device.Hid.Mouse.Update(0, 0); } - _device.TamperMachine.UpdateInput(hleInputStates); - - hleMotionStates.Clear(); - _hleMotionStatesPool.Release(hleMotionStates); + _device.TamperMachine.UpdateInput(_hleInputStates); } } diff --git a/src/Ryujinx.Input/IKeyboard.cs b/src/Ryujinx.Input/IKeyboard.cs index 7fecaaa5d..c51d5aea3 100644 --- a/src/Ryujinx.Input/IKeyboard.cs +++ b/src/Ryujinx.Input/IKeyboard.cs @@ -8,6 +8,8 @@ namespace Ryujinx.Input /// public interface IKeyboard : IGamepad { + private static bool[] _keyState; + /// /// Check if a given key is pressed on the keyboard. /// @@ -29,15 +31,17 @@ namespace Ryujinx.Input [MethodImpl(MethodImplOptions.AggressiveInlining)] static KeyboardStateSnapshot GetStateSnapshot(IKeyboard keyboard) { + if (_keyState is null) + { + _keyState = new bool[(int)Key.Count]; + } - bool[] keysState = ArrayPool.Shared.Rent((int)Key.Count); - 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); } } } diff --git a/src/Ryujinx.Memory/Range/INonOverlappingRange.cs b/src/Ryujinx.Memory/Range/INonOverlappingRange.cs index c6a0197d4..09311a830 100644 --- a/src/Ryujinx.Memory/Range/INonOverlappingRange.cs +++ b/src/Ryujinx.Memory/Range/INonOverlappingRange.cs @@ -3,7 +3,7 @@ namespace Ryujinx.Memory.Range /// /// Range of memory that can be split in two. /// - public interface INonOverlappingRange : IRange + public interface INonOverlappingRange : IRangeListRange where T : class, IRangeListRange { /// /// Split this region into two, around the specified address. @@ -11,6 +11,6 @@ namespace Ryujinx.Memory.Range /// /// Address to split the region around /// The second part of the split region, with start address at the given split. - public INonOverlappingRange Split(ulong splitAddress); + public INonOverlappingRange Split(ulong splitAddress); } } diff --git a/src/Ryujinx.Memory/Range/IRange.cs b/src/Ryujinx.Memory/Range/IRange.cs index c85e21d1d..c8f50a921 100644 --- a/src/Ryujinx.Memory/Range/IRange.cs +++ b/src/Ryujinx.Memory/Range/IRange.cs @@ -24,8 +24,8 @@ namespace Ryujinx.Memory.Range /// Check if this range overlaps with another. /// /// Base address - /// Size of the range + /// EndAddress of the range /// True if overlapping, false otherwise - bool OverlapsWith(ulong address, ulong size); + bool OverlapsWith(ulong address, ulong endAddress); } } diff --git a/src/Ryujinx.Memory/Range/NonOverlappingRangeList.cs b/src/Ryujinx.Memory/Range/NonOverlappingRangeList.cs index 7560d2ae7..1ef51644c 100644 --- a/src/Ryujinx.Memory/Range/NonOverlappingRangeList.cs +++ b/src/Ryujinx.Memory/Range/NonOverlappingRangeList.cs @@ -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. /// /// Type of the range. - public unsafe class NonOverlappingRangeList : RangeListBase where T : class, INonOverlappingRange + public class NonOverlappingRangeList : RangeListBase where T : class, INonOverlappingRange { public readonly ReaderWriterLockSlim Lock = new(); @@ -32,83 +32,18 @@ namespace Ryujinx.Memory.Range /// The item to be added public override void Add(T item) { + Debug.Assert(item.Address != item.EndAddress); + int index = BinarySearch(item.Address); if (index < 0) { index = ~index; } - - RangeItem rangeItem = _rangeItemPool.Allocate().Set(item); - - Insert(index, rangeItem); - } - - /// - /// Updates an item's end address on the list. Address must be the same. - /// - /// The item to be updated - /// True if the item was located and updated, false otherwise - protected override bool Update(T item) - { - int index = BinarySearch(item.Address); - - if (index >= 0 && Items[index].Value.Equals(item)) - { - RangeItem 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; - } - - /// - /// Updates an item's end address on the list. Address must be the same. - /// - /// The RangeItem to be updated - /// True if the item was located and updated, false otherwise - protected override bool Update(RangeItem item) - { - int index = BinarySearch(item.Address); - - RangeItem 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 item) - { - Debug.Assert(item.Address != item.EndAddress); if (Count + 1 > Items.Length) { - Array.Resize(ref Items, Items.Length + BackingGrowthSize); + Array.Resize(ref Items, (int)(Items.Length * 1.5)); } if (index >= Count) @@ -145,8 +80,6 @@ namespace Ryujinx.Memory.Range [MethodImpl(MethodImplOptions.AggressiveInlining)] private void RemoveAt(int index) { - _rangeItemPool.Release(Items[index]); - if (index < Count - 1) { Items[index + 1].Previous = index > 0 ? Items[index - 1] : null; @@ -173,7 +106,7 @@ namespace Ryujinx.Memory.Range { int index = BinarySearch(item.Address); - if (index >= 0 && Items[index].Value.Equals(item)) + if (index >= 0 && Items[index] == item) { RemoveAt(index); @@ -188,7 +121,7 @@ namespace Ryujinx.Memory.Range /// /// The first item in the range of items to be removed /// The last item in the range of items to be removed - public override void RemoveRange(RangeItem startItem, RangeItem endItem) + public override void RemoveRange(T startItem, T endItem) { if (startItem is null) { @@ -197,7 +130,7 @@ namespace Ryujinx.Memory.Range if (startItem == endItem) { - Remove(startItem.Value); + Remove(startItem); return; } @@ -229,42 +162,45 @@ namespace Ryujinx.Memory.Range /// Size of the range public void RemoveRange(ulong address, ulong size) { - int startIndex = BinarySearchLeftEdge(address, address + size); + (int startIndex, int endIndex) = BinarySearchEdges(address, address + size); if (startIndex < 0) { return; } - int endIndex = startIndex; - - while (Items[endIndex] is not null && Items[endIndex].Address < address + size) + if (startIndex == endIndex - 1) { - if (endIndex == Count - 1) - { - break; - } - - endIndex++; + RemoveAt(startIndex); + return; } - if (endIndex < Count - 1) + RemoveRangeInternal(startIndex, endIndex); + } + + /// + /// Removes a range of items from the item list + /// + /// Start index of the range + /// End index of the range (exclusive) + 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 - 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; } /// @@ -296,8 +232,8 @@ namespace Ryujinx.Memory.Range // So we need to return both the split 0-1 and 1-2 ranges. Lock.EnterWriteLock(); - (RangeItem first, RangeItem last) = FindOverlapsAsNodes(address, size); - list = new List(); + (T first, T last) = FindOverlapsAsNodes(address, size); + list = []; if (first is null) { @@ -311,42 +247,41 @@ namespace Ryujinx.Memory.Range ulong lastAddress = address; ulong endAddress = address + size; - RangeItem current = first; + T current = first; while (last is not null && current is not null && current.Address < endAddress) { - T region = current.Value; - if (first == last && region.Address == address && region.Size == size) + if (first == last && current.Address == address && current.Size == size) { // Exact match, no splitting required. - list.Add(region); + list.Add(current); Lock.ExitWriteLock(); return; } - if (lastAddress < region.Address) + if (lastAddress < current.Address) { // 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); Add(fillRegion); } - if (region.Address < address) + if (current.Address < address) { // 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(region, address + size); + Split(current, address + size); } - list.Add(region); - lastAddress = region.EndAddress; + list.Add(current); + lastAddress = current.EndAddress; current = current.Next; } @@ -374,7 +309,6 @@ namespace Ryujinx.Memory.Range private T Split(T region, ulong splitAddress) { T newRegion = (T)region.Split(splitAddress); - Update(region); Add(newRegion); return newRegion; } @@ -386,16 +320,11 @@ namespace Ryujinx.Memory.Range /// Size in bytes of the range /// The leftmost overlapping item, or null if none is found [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override RangeItem FindOverlap(ulong address, ulong size) + public override T FindOverlap(ulong address, ulong size) { int index = BinarySearchLeftEdge(address, address + size); - if (index < 0) - { - return null; - } - - return Items[index]; + return index < 0 ? null : Items[index]; } /// @@ -405,16 +334,11 @@ namespace Ryujinx.Memory.Range /// Size in bytes of the range /// The overlapping item, or null if none is found [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override RangeItem FindOverlapFast(ulong address, ulong size) + public override T FindOverlapFast(ulong address, ulong size) { int index = BinarySearch(address, address + size); - if (index < 0) - { - return null; - } - - return Items[index]; + return index < 0 ? null : Items[index]; } /// @@ -424,23 +348,18 @@ namespace Ryujinx.Memory.Range /// Size in bytes of the range /// The first and last overlapping items, or null if none are found [MethodImpl(MethodImplOptions.AggressiveInlining)] - public (RangeItem, RangeItem) FindOverlapsAsNodes(ulong address, ulong size) + public (T, T) FindOverlapsAsNodes(ulong address, ulong size) { (int index, int endIndex) = BinarySearchEdges(address, address + size); - if (index < 0) - { - return (null, null); - } - - return (Items[index], Items[endIndex - 1]); + return index < 0 ? (null, null) : (Items[index], Items[endIndex - 1]); } - public RangeItem[] 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); - RangeItem[] result; + T[] result; if (index < 0) { @@ -449,29 +368,20 @@ namespace Ryujinx.Memory.Range } else { - result = ArrayPool>.Shared.Rent(endIndex - index); + result = ArrayPool.Shared.Rent(endIndex - index); length = endIndex - index; - Array.Copy(Items, index, result, 0, endIndex - index); + Items.AsSpan(index, endIndex - index).CopyTo(result); } return result; } - public Span> FindOverlapsAsSpan(ulong address, ulong size) + public ReadOnlySpan FindOverlapsAsSpan(ulong address, ulong size) { (int index, int endIndex) = BinarySearchEdges(address, address + size); - Span> result; - - if (index < 0) - { - result = []; - } - else - { - result = Items.AsSpan().Slice(index, endIndex - index); - } + ReadOnlySpan result = index < 0 ? [] : Items.AsSpan(index, endIndex - index); return result; } @@ -480,7 +390,7 @@ namespace Ryujinx.Memory.Range { for (int i = 0; i < Count; i++) { - yield return Items[i].Value; + yield return Items[i]; } } } diff --git a/src/Ryujinx.Memory/Range/RangeList.cs b/src/Ryujinx.Memory/Range/RangeList.cs index 63025f1e8..e7ea55a94 100644 --- a/src/Ryujinx.Memory/Range/RangeList.cs +++ b/src/Ryujinx.Memory/Range/RangeList.cs @@ -14,14 +14,14 @@ namespace Ryujinx.Memory.Range /// startIndex is inclusive. /// endIndex is exclusive. /// - public readonly struct OverlapResult where T : IRange + public readonly struct OverlapResult where T : class, IRangeListRange { public readonly int StartIndex = -1; public readonly int EndIndex = -1; - public readonly RangeItem QuickResult; + public readonly T QuickResult; public int Count => EndIndex - StartIndex; - public OverlapResult(int startIndex, int endIndex, RangeItem quickResult = null) + public OverlapResult(int startIndex, int endIndex, T quickResult = null) { this.StartIndex = startIndex; this.EndIndex = endIndex; @@ -33,7 +33,7 @@ namespace Ryujinx.Memory.Range /// Sorted list of ranges that supports binary search. /// /// Type of the range. - public class RangeList : RangeListBase where T : IRange + public class RangeList : RangeListBase where T : class, IRangeListRange { public readonly ReaderWriterLockSlim Lock = new(); @@ -61,104 +61,6 @@ namespace Ryujinx.Memory.Range index = ~index; } - Insert(index, new RangeItem(item)); - } - - /// - /// Updates an item's end address on the list. Address must be the same. - /// - /// The item to be updated - /// True if the item was located and updated, false otherwise - protected override bool Update(T item) - { - int index = BinarySearch(item.Address); - - if (index >= 0) - { - while (index < Count) - { - if (Items[index].Value.Equals(item)) - { - RangeItem 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; - } - - /// - /// Updates an item's end address on the list. Address must be the same. - /// - /// The RangeItem to be updated - /// True if the item was located and updated, false otherwise - protected override bool Update(RangeItem item) - { - int index = BinarySearch(item.Address); - - if (index >= 0) - { - while (index < Count) - { - if (Items[index].Equals(item)) - { - RangeItem 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 item) - { - Debug.Assert(item.Address != item.EndAddress); - - Debug.Assert(item.Address % 32 == 0); - if (Count + 1 > Items.Length) { Array.Resize(ref Items, Items.Length + BackingGrowthSize); @@ -220,7 +122,7 @@ namespace Ryujinx.Memory.Range /// The first item in the range of items to be removed /// The last item in the range of items to be removed [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void RemoveRange(RangeItem startItem, RangeItem endItem) + public override void RemoveRange(T startItem, T endItem) { if (startItem is null) { @@ -229,30 +131,29 @@ namespace Ryujinx.Memory.Range if (startItem == endItem) { - Remove(startItem.Value); + Remove(startItem); return; } - int startIndex = BinarySearch(startItem.Address); - int endIndex = BinarySearch(endItem.Address); + (int index, int endIndex) = BinarySearchEdges(startItem.Address, endItem.EndAddress); - 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; } /// @@ -268,7 +169,7 @@ namespace Ryujinx.Memory.Range { while (index < Count) { - if (Items[index].Value.Equals(item)) + if (Items[index] == item) { RemoveAt(index); @@ -298,7 +199,7 @@ namespace Ryujinx.Memory.Range /// Size in bytes of the range /// The overlapping item, or the default value for the type if none found [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override RangeItem FindOverlap(ulong address, ulong size) + public override T FindOverlap(ulong address, ulong size) { int index = BinarySearchLeftEdge(address, address + size); @@ -321,7 +222,7 @@ namespace Ryujinx.Memory.Range /// Size in bytes of the range /// The overlapping item, or the default value for the type if none found [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override RangeItem FindOverlapFast(ulong address, ulong size) + public override T FindOverlapFast(ulong address, ulong size) { int index = BinarySearch(address, address + size); @@ -340,7 +241,7 @@ namespace Ryujinx.Memory.Range /// Size in bytes of the range /// Output array where matches will be written. It is automatically resized to fit the results /// Range information of overlapping items found - private OverlapResult FindOverlaps(ulong address, ulong size, ref RangeItem[] output) + private OverlapResult FindOverlaps(ulong address, ulong size, ref T[] output) { int outputCount = 0; @@ -353,7 +254,7 @@ namespace Ryujinx.Memory.Range for (int i = startIndex; i < Count; i++) { - ref RangeItem item = ref Items[i]; + T item = Items[i]; if (item.Address >= endAddress) { @@ -398,7 +299,7 @@ namespace Ryujinx.Memory.Range { for (int i = 0; i < Count; i++) { - yield return Items[i].Value; + yield return Items[i]; } } } diff --git a/src/Ryujinx.Memory/Range/RangeListBase.cs b/src/Ryujinx.Memory/Range/RangeListBase.cs index 01fe1b0dc..9f0284253 100644 --- a/src/Ryujinx.Memory/Range/RangeListBase.cs +++ b/src/Ryujinx.Memory/Range/RangeListBase.cs @@ -1,56 +1,22 @@ using Ryujinx.Common; +using System; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; namespace Ryujinx.Memory.Range { - public class RangeItem where TValue : IRange + public interface IRangeListRange : IRange where TValue : class, IRangeListRange { - public RangeItem Next; - public RangeItem Previous; - - 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 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 TValue Next { get; set; } + public TValue Previous { get; set; } } - public unsafe abstract class RangeListBase : IEnumerable where T : IRange + public unsafe abstract class RangeListBase : IEnumerable where T : class, IRangeListRange { - protected static readonly ObjectPool> _rangeItemPool = new(() => new RangeItem()); private const int BackingInitialSize = 1024; - protected RangeItem[] Items; + protected T[] Items; protected readonly int BackingGrowthSize; public int Count { get; protected set; } @@ -62,32 +28,18 @@ namespace Ryujinx.Memory.Range protected RangeListBase(int backingInitialSize = BackingInitialSize) { BackingGrowthSize = backingInitialSize; - Items = new RangeItem[backingInitialSize]; + Items = new T[backingInitialSize]; } public abstract void Add(T item); - - /// - /// Updates an item's end address on the list. Address must be the same. - /// - /// The item to be updated - /// True if the item was located and updated, false otherwise - protected abstract bool Update(T item); - - /// - /// Updates an item's end address on the list. Address must be the same. - /// - /// The RangeItem to be updated - /// True if the item was located and updated, false otherwise - protected abstract bool Update(RangeItem item); public abstract bool Remove(T item); - public abstract void RemoveRange(RangeItem startItem, RangeItem endItem); + public abstract void RemoveRange(T startItem, T endItem); - public abstract RangeItem FindOverlap(ulong address, ulong size); + public abstract T FindOverlap(ulong address, ulong size); - public abstract RangeItem FindOverlapFast(ulong address, ulong size); + public abstract T FindOverlapFast(ulong address, ulong size); /// /// Performs binary search on the internal list of items. @@ -106,7 +58,7 @@ namespace Ryujinx.Memory.Range int middle = left + (range >> 1); - ref RangeItem item = ref Items[middle]; + T item = Items[middle]; if (item.Address == address) { @@ -144,7 +96,7 @@ namespace Ryujinx.Memory.Range int middle = left + (range >> 1); - ref RangeItem item = ref Items[middle]; + T item = Items[middle]; if (item.OverlapsWith(address, endAddress)) { @@ -185,7 +137,7 @@ namespace Ryujinx.Memory.Range int middle = left + (range >> 1); - ref RangeItem item = ref Items[middle]; + T item = Items[middle]; bool match = item.OverlapsWith(address, endAddress); @@ -237,7 +189,7 @@ namespace Ryujinx.Memory.Range int middle = right - (range >> 1); - ref RangeItem item = ref Items[middle]; + T item = Items[middle]; bool match = item.OverlapsWith(address, endAddress); @@ -282,7 +234,7 @@ namespace Ryujinx.Memory.Range if (Count == 1) { - ref RangeItem item = ref Items[0]; + T item = Items[0]; if (item.OverlapsWith(address, endAddress)) { @@ -312,7 +264,7 @@ namespace Ryujinx.Memory.Range int middle = left + (range >> 1); - ref RangeItem item = ref Items[middle]; + T item = Items[middle]; bool match = item.OverlapsWith(address, endAddress); @@ -369,7 +321,7 @@ namespace Ryujinx.Memory.Range int middle = right - (range >> 1); - ref RangeItem item = ref Items[middle]; + T item = Items[middle]; bool match = item.OverlapsWith(address, endAddress); diff --git a/src/Ryujinx.Memory/Tracking/AbstractRegion.cs b/src/Ryujinx.Memory/Tracking/AbstractRegion.cs index 7226fe954..09703a253 100644 --- a/src/Ryujinx.Memory/Tracking/AbstractRegion.cs +++ b/src/Ryujinx.Memory/Tracking/AbstractRegion.cs @@ -5,7 +5,7 @@ namespace Ryujinx.Memory.Tracking /// /// A region of memory. /// - abstract class AbstractRegion : INonOverlappingRange + abstract class AbstractRegion : INonOverlappingRange where T : class, INonOverlappingRange { /// /// Base address. @@ -21,6 +21,9 @@ namespace Ryujinx.Memory.Tracking /// End address. /// public ulong EndAddress => Address + Size; + + public T Next { get; set; } + public T Previous { get; set; } /// /// Create a new region. @@ -37,11 +40,11 @@ namespace Ryujinx.Memory.Tracking /// Check if this range overlaps with another. /// /// Base address - /// Size of the range + /// End address /// True if overlapping, false otherwise - 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; } /// @@ -68,6 +71,6 @@ namespace Ryujinx.Memory.Tracking /// /// Address to split the region around /// The second part of the split region, with start address at the given split. - public abstract INonOverlappingRange Split(ulong splitAddress); + public abstract INonOverlappingRange Split(ulong splitAddress); } } diff --git a/src/Ryujinx.Memory/Tracking/MemoryTracking.cs b/src/Ryujinx.Memory/Tracking/MemoryTracking.cs index b4b06da8c..0160791e8 100644 --- a/src/Ryujinx.Memory/Tracking/MemoryTracking.cs +++ b/src/Ryujinx.Memory/Tracking/MemoryTracking.cs @@ -81,10 +81,10 @@ namespace Ryujinx.Memory.Tracking { NonOverlappingRangeList regions = type == 0 ? _virtualRegions : _guestVirtualRegions; regions.Lock.EnterReadLock(); - Span> overlaps = regions.FindOverlapsAsSpan(va, size); + ReadOnlySpan overlaps = regions.FindOverlapsAsSpan(va, size); 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. bool remapped = _memoryManager.IsRangeMapped(region.Address, region.Size); @@ -117,11 +117,11 @@ namespace Ryujinx.Memory.Tracking { NonOverlappingRangeList regions = type == 0 ? _virtualRegions : _guestVirtualRegions; regions.Lock.EnterReadLock(); - Span> overlaps = regions.FindOverlapsAsSpan(va, size); + ReadOnlySpan overlaps = regions.FindOverlapsAsSpan(va, size); for (int i = 0; i < overlaps.Length; i++) { - overlaps[i].Value.SignalMappingChanged(false); + overlaps[i].SignalMappingChanged(false); } 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. regions.Lock.EnterReadLock(); - RangeItem[] overlaps = regions.FindOverlapsAsArray(address, size, out int length); + VirtualRegion[] overlaps = regions.FindOverlapsAsArray(address, size, out int length); regions.Lock.ExitReadLock(); if (length == 0 && !precise) @@ -327,7 +327,7 @@ namespace Ryujinx.Memory.Tracking for (int i = 0; i < length; i++) { - VirtualRegion region = overlaps[i].Value; + VirtualRegion region = overlaps[i]; if (precise) { @@ -341,7 +341,7 @@ namespace Ryujinx.Memory.Tracking if (length != 0) { - ArrayPool>.Shared.Return(overlaps); + ArrayPool.Shared.Return(overlaps); } } } diff --git a/src/Ryujinx.Memory/Tracking/VirtualRegion.cs b/src/Ryujinx.Memory/Tracking/VirtualRegion.cs index b86631db2..e95754c7a 100644 --- a/src/Ryujinx.Memory/Tracking/VirtualRegion.cs +++ b/src/Ryujinx.Memory/Tracking/VirtualRegion.cs @@ -6,7 +6,7 @@ namespace Ryujinx.Memory.Tracking /// /// A region of virtual memory. /// - class VirtualRegion : AbstractRegion + class VirtualRegion : AbstractRegion { public List Handles = []; @@ -137,7 +137,7 @@ namespace Ryujinx.Memory.Tracking } } - public override INonOverlappingRange Split(ulong splitAddress) + public override INonOverlappingRange Split(ulong splitAddress) { VirtualRegion newRegion = new(_tracking, splitAddress, EndAddress - splitAddress, Guest, _lastPermission); Size = splitAddress - Address; diff --git a/src/Ryujinx.UI.LocaleGenerator/LocaleGenerator.cs b/src/Ryujinx.UI.LocaleGenerator/LocaleGenerator.cs index 4de92ee4a..068931fa3 100644 --- a/src/Ryujinx.UI.LocaleGenerator/LocaleGenerator.cs +++ b/src/Ryujinx.UI.LocaleGenerator/LocaleGenerator.cs @@ -1,5 +1,7 @@ using Microsoft.CodeAnalysis; using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; using System.Linq; using System.Text; @@ -10,23 +12,34 @@ namespace Ryujinx.UI.LocaleGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { - IncrementalValuesProvider localeFile = context.AdditionalTextsProvider.Where(static x => x.Path.EndsWith("locales.json")); + IncrementalValuesProvider localeFiles = context.AdditionalTextsProvider.Where(static x => Path.GetDirectoryName(x.Path)?.Replace('\\', '/').EndsWith("assets/Locales") ?? false); - IncrementalValuesProvider contents = localeFile.Select((text, cancellationToken) => text.GetText(cancellationToken)!.ToString()); + IncrementalValueProvider> 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 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(); enumSourceBuilder.AppendLine("namespace Ryujinx.Ava.Common.Locale;"); enumSourceBuilder.AppendLine("public enum LocaleKeys"); enumSourceBuilder.AppendLine("{"); - foreach (string? line in lines) + + foreach ((string, string) content in contents) { - enumSourceBuilder.AppendLine($" {line},"); + IEnumerable 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("}"); spc.AddSource("LocaleKeys", enumSourceBuilder.ToString()); diff --git a/src/Ryujinx/Common/LocaleManager.cs b/src/Ryujinx/Common/LocaleManager.cs index bc9cfdf15..4433f9859 100644 --- a/src/Ryujinx/Common/LocaleManager.cs +++ b/src/Ryujinx/Common/LocaleManager.cs @@ -8,6 +8,8 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; +using System.IO; +using System.Linq; using System.Text.Json.Serialization; namespace Ryujinx.Ava.Common.Locale @@ -158,52 +160,86 @@ namespace Ryujinx.Ava.Common.Locale LocaleChanged?.Invoke(); } - private static LocalesJson? _localeData; + private static LocalesData? _localeData; private static Dictionary LoadJsonLanguage(string languageCode) { Dictionary localeStrings = new(); - _localeData ??= EmbeddedResources.ReadAllText("Ryujinx/Assets/Locale.json") - .Into(it => JsonHelper.Deserialize(it, LocalesJsonContext.Default.LocalesJson)); - - foreach (LocalesEntry locale in _localeData.Value.Locales) + if (_localeData is null) { - if (locale.Translations.Count < _localeData.Value.Languages.Count) + Dictionary locales = []; + + foreach (string uri in EmbeddedResources.GetAllAvailableResources("Ryujinx/Assets/Locales", ".json")) { - throw new Exception( - $"Locale key {{{locale.ID}}} is missing languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!"); + string path = uri[..^".json".Length]; + 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( - $"Locale key {{{locale.ID}}} has too many languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!"); - } + Languages = EmbeddedResources.ReadAllText("Ryujinx/Assets/Languages.json") + .Into(it => JsonHelper.Deserialize(it, LanguagesJsonContext.Default.LanguagesJson)).Languages.Keys.ToList(), + LocalesFiles = locales + }; + - if (!Enum.TryParse(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)) + foreach ((string fileName, LocalesJson file) in _localeData.Value.LocalesFiles) + { + foreach (LocalesEntry locale in file.Locales) { - throw new Exception( - $"Locale key '{locale.ID}' has no valid translations for desired language {languageCode}! {DefaultLanguageCode} is an empty string or null"); - } + if (locale.Translations.Count < _localeData.Value.Languages.Count) + { + 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(fileName == "Root.json" ? locale.ID : $"{fileName[..^".json".Length]}_{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; } } - public struct LocalesJson + public struct LocalesData { public List Languages { get; set; } + public Dictionary LocalesFiles { get; set; } + } + + public struct LanguagesJson + { + public Dictionary Languages { get; set; } + } + + public struct LocalesJson + { public List Locales { get; set; } } @@ -216,4 +252,8 @@ namespace Ryujinx.Ava.Common.Locale [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(LocalesJson))] internal partial class LocalesJsonContext : JsonSerializerContext; + + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(LanguagesJson))] + internal partial class LanguagesJsonContext : JsonSerializerContext; } diff --git a/src/Ryujinx/Headless/Windows/WindowBase.cs b/src/Ryujinx/Headless/Windows/WindowBase.cs index 8e06a3f20..49b2a389a 100644 --- a/src/Ryujinx/Headless/Windows/WindowBase.cs +++ b/src/Ryujinx/Headless/Windows/WindowBase.cs @@ -580,5 +580,10 @@ namespace Ryujinx.Headless { return AccountSaveDataManager.GetLastUsedUser(); } + + public void TakeScreenshot() + { + throw new NotImplementedException(); + } } } diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index abab3daca..8d03f81da 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -17,6 +17,8 @@ using Ryujinx.Common.Configuration; using Ryujinx.Common.GraphicsDriver; using Ryujinx.Common.Logging; using Ryujinx.Common.SystemInterop; +using Ryujinx.Common.Utilities; +using Ryujinx.Graphics.RenderDocApi; using Ryujinx.Graphics.Vulkan.MoltenVK; using Ryujinx.Headless; using Ryujinx.SDL3.Common; @@ -46,7 +48,7 @@ namespace Ryujinx.Ava public static int Main(string[] args) { Version = ReleaseInformation.Version; - + if (OperatingSystem.IsWindows()) { if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041)) @@ -55,8 +57,11 @@ namespace Ryujinx.Ava return 0; } - if (Environment.CurrentDirectory.StartsWithIgnoreCase("C:\\Program Files") || - Environment.CurrentDirectory.StartsWithIgnoreCase("C:\\Program Files (x86)")) + var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + 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); return 0; @@ -73,11 +78,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; - if (args.Length > 0 && args[0] is "--no-gui" or "nogui") + if (noGuiArg) { - HeadlessRyujinx.Entrypoint(args[1..]); + HeadlessRyujinx.Entrypoint(args); return 0; } @@ -112,6 +129,14 @@ namespace Ryujinx.Ava : [Win32RenderingMode.Software] }); + private static bool ConsumeCommandLineArgument(ref string[] args, string targetArgument) + { + List argList = [.. args]; + bool found = argList.Remove(targetArgument); + args = argList.ToArray(); + return found; + } + private static void Initialize(string[] args) { // Ensure Discord presence timestamp begins at the absolute start of when Ryujinx is launched @@ -177,7 +202,6 @@ namespace Ryujinx.Ava } } - public static string GetDirGameUserConfig(string gameId, bool changeFolderForGame = false) { if (string.IsNullOrEmpty(gameId)) @@ -196,22 +220,26 @@ namespace Ryujinx.Ava return gameDir; } - public static void ReloadConfig() + public static void ReloadConfig(bool isRunGameWithCustomConfig = false) { string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName); string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName); - // Now load the configuration as the other subsystems are now registered - if (File.Exists(localConfigurationPath)) - { - ConfigurationPath = localConfigurationPath; - } - else if (File.Exists(appDataConfigurationPath)) - { - ConfigurationPath = appDataConfigurationPath; - } + if (!isRunGameWithCustomConfig) // To return settings from the game folder if the user configuration exists + { + // Now load the configuration as the other subsystems are now registered + if (File.Exists(localConfigurationPath)) + { + ConfigurationPath = localConfigurationPath; + } + else if (File.Exists(appDataConfigurationPath)) + { + ConfigurationPath = appDataConfigurationPath; + } + } + if (ConfigurationPath == null) { // No configuration, we load the default values and save it to disk diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index 31dc20aac..28aec175b 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -78,7 +78,9 @@ - + + + @@ -86,7 +88,6 @@ - @@ -134,7 +135,7 @@ - + @@ -156,8 +157,8 @@ Assets\RyujinxGameCompatibility.csv - - Assets\Locale.json + + Assets @@ -178,6 +179,6 @@ - + diff --git a/src/Ryujinx/Systems/Configuration/AudioBackend.cs b/src/Ryujinx/Systems/Configuration/AudioBackend.cs index f1a0c2362..da75c9f7c 100644 --- a/src/Ryujinx/Systems/Configuration/AudioBackend.cs +++ b/src/Ryujinx/Systems/Configuration/AudioBackend.cs @@ -1,9 +1,8 @@ -using Ryujinx.Common.Utilities; using System.Text.Json.Serialization; namespace Ryujinx.Ava.Systems.Configuration { - [JsonConverter(typeof(TypedStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum AudioBackend { Dummy, diff --git a/src/Ryujinx/Systems/Configuration/System/Language.cs b/src/Ryujinx/Systems/Configuration/System/Language.cs index 3087653e9..dd44dff37 100644 --- a/src/Ryujinx/Systems/Configuration/System/Language.cs +++ b/src/Ryujinx/Systems/Configuration/System/Language.cs @@ -1,10 +1,9 @@ -using Ryujinx.Common.Utilities; using Ryujinx.HLE.HOS.SystemState; using System.Text.Json.Serialization; namespace Ryujinx.Ava.Systems.Configuration.System { - [JsonConverter(typeof(TypedStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum Language { Japanese, diff --git a/src/Ryujinx/Systems/Configuration/System/Region.cs b/src/Ryujinx/Systems/Configuration/System/Region.cs index 2ba657876..a85dcb85e 100644 --- a/src/Ryujinx/Systems/Configuration/System/Region.cs +++ b/src/Ryujinx/Systems/Configuration/System/Region.cs @@ -1,10 +1,9 @@ -using Ryujinx.Common.Utilities; using Ryujinx.HLE.HOS.SystemState; using System.Text.Json.Serialization; namespace Ryujinx.Ava.Systems.Configuration.System { - [JsonConverter(typeof(TypedStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum Region { Japan, diff --git a/src/Ryujinx/Systems/Configuration/UI/FocusLostType.cs b/src/Ryujinx/Systems/Configuration/UI/FocusLostType.cs index 9e22f3dca..96eb8ee26 100644 --- a/src/Ryujinx/Systems/Configuration/UI/FocusLostType.cs +++ b/src/Ryujinx/Systems/Configuration/UI/FocusLostType.cs @@ -1,9 +1,8 @@ -using Ryujinx.Common.Utilities; using System.Text.Json.Serialization; namespace Ryujinx.Ava.Systems.Configuration.UI { - [JsonConverter(typeof(TypedStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum FocusLostType { DoNothing, diff --git a/src/Ryujinx/Systems/Configuration/UI/UpdaterType.cs b/src/Ryujinx/Systems/Configuration/UI/UpdaterType.cs index bc2696780..584763722 100644 --- a/src/Ryujinx/Systems/Configuration/UI/UpdaterType.cs +++ b/src/Ryujinx/Systems/Configuration/UI/UpdaterType.cs @@ -1,9 +1,8 @@ -using Ryujinx.Common.Utilities; using System.Text.Json.Serialization; namespace Ryujinx.Ava.Systems.Configuration.UI { - [JsonConverter(typeof(TypedStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum UpdaterType { Off, diff --git a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs index 38670e5d5..45235ee3f 100644 --- a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs +++ b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs @@ -327,5 +327,10 @@ namespace Ryujinx.Ava.UI.Applet return profile; } + + public void TakeScreenshot() + { + _parent.ViewModel.AppHost.ScreenshotRequested = true; + } } } diff --git a/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs b/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs index e360d42f7..687047672 100644 --- a/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs +++ b/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs @@ -2,8 +2,12 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Platform; using Ryujinx.Ava.Systems.Configuration; +using Ryujinx.Ava.Utilities; using Ryujinx.Common.Configuration; using Ryujinx.Common.Helper; +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.RenderDocApi; +using Ryujinx.HLE; using SPB.Graphics; using SPB.Platform; using SPB.Platform.GLX; @@ -30,6 +34,7 @@ namespace Ryujinx.Ava.UI.Renderer protected nint MetalLayer { get; set; } public delegate void UpdateBoundsCallbackDelegate(Rect rect); + private UpdateBoundsCallbackDelegate _updateBoundsCallback; public event EventHandler WindowCreated; @@ -46,6 +51,55 @@ namespace Ryujinx.Ava.UI.Renderer protected virtual void OnWindowDestroyed() { } + public bool ToggleRenderDocCapture(Switch device) + { + if (!RenderDoc.IsAvailable) return false; + + if (RenderDoc.IsFrameCapturing) + { + if (EndRenderDocCapture()) + { + Logger.Info?.Print(LogClass.Application, "Ended RenderDoc capture."); + return true; + } + } + else if (StartRenderDocCapture(device)) + { + Logger.Info?.Print(LogClass.Application, "Starting RenderDoc capture."); + return true; + } + + return false; + } + + public bool StartRenderDocCapture(Switch device) + { + if (!RenderDoc.IsAvailable) return false; + + if (RenderDoc.IsFrameCapturing) return false; + + RenderDoc.StartFrameCapture(nint.Zero, WindowHandle); + RenderDoc.SetCaptureTitle(TitleHelper.FormatRenderDocCaptureTitle(device.Processes.ActiveApplication, Program.Version)); + + return true; + } + + public bool EndRenderDocCapture() + { + if (!RenderDoc.IsAvailable) return false; + if (!RenderDoc.IsFrameCapturing) return false; + + return RenderDoc.IsFrameCapturing && RenderDoc.EndFrameCapture(nint.Zero, WindowHandle); + } + + public bool DiscardRenderDocCapture() + { + if (!RenderDoc.IsAvailable) return false; + if (!RenderDoc.IsFrameCapturing) return false; + + return RenderDoc.IsFrameCapturing && RenderDoc.DiscardFrameCapture(nint.Zero, WindowHandle); + } + protected virtual void OnWindowDestroying() { WindowHandle = nint.Zero; @@ -124,7 +178,9 @@ namespace Ryujinx.Ava.UI.Renderer } else { - X11Window = PlatformHelper.CreateOpenGLWindow(new FramebufferFormat(new ColorFormat(8, 8, 8, 0), 16, 0, ColorFormat.Zero, 0, 2, false), 0, 0, 100, 100) as GLXWindow; + X11Window = PlatformHelper.CreateOpenGLWindow( + new FramebufferFormat(new ColorFormat(8, 8, 8, 0), 16, 0, ColorFormat.Zero, 0, 2, false), 0, 0, 100, + 100) as GLXWindow; } WindowHandle = X11Window.WindowHandle.RawHandle; @@ -138,7 +194,7 @@ namespace Ryujinx.Ava.UI.Renderer { _className = "NativeWindow-" + Guid.NewGuid(); - _wndProcDelegate = delegate (nint hWnd, WindowsMessages msg, nint wParam, nint lParam) + _wndProcDelegate = delegate(nint hWnd, WindowsMessages msg, nint wParam, nint lParam) { switch (msg) { @@ -161,7 +217,8 @@ namespace Ryujinx.Ava.UI.Renderer RegisterClassEx(ref wndClassEx); - WindowHandle = CreateWindowEx(0, _className, "NativeWindow", WindowStyles.WsChild, 0, 0, 640, 480, control.Handle, nint.Zero, nint.Zero, nint.Zero); + WindowHandle = CreateWindowEx(0, _className, "NativeWindow", WindowStyles.WsChild, 0, 0, 640, 480, + control.Handle, nint.Zero, nint.Zero, nint.Zero); SetWindowLongPtrW(control.Handle, GWLP_WNDPROC, wndClassEx.lpfnWndProc); diff --git a/src/Ryujinx/UI/RyujinxApp.axaml.cs b/src/Ryujinx/UI/RyujinxApp.axaml.cs index 34c2d96ca..c778f27fb 100644 --- a/src/Ryujinx/UI/RyujinxApp.axaml.cs +++ b/src/Ryujinx/UI/RyujinxApp.axaml.cs @@ -15,6 +15,7 @@ using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.Utilities; using Ryujinx.Common; using Ryujinx.Common.Logging; +using Ryujinx.Graphics.RenderDocApi; using System; using System.Diagnostics; @@ -56,6 +57,8 @@ namespace Ryujinx.Ava 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"); } } diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 2236b27f6..96159a1ea 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -38,6 +38,7 @@ using Ryujinx.Common.Logging; using Ryujinx.Common.UI; using Ryujinx.Common.Utilities; using Ryujinx.Cpu; +using Ryujinx.Graphics.RenderDocApi; using Ryujinx.HLE; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; @@ -104,7 +105,7 @@ namespace Ryujinx.Ava.UI.ViewModels [ObservableProperty] public partial Brush ProgressBarForegroundColor { get; set; } [ObservableProperty] public partial Brush ProgressBarBackgroundColor { get; set; } - + #pragma warning disable MVVMTK0042 // Must stay a normal observable field declaration since this is used as an out parameter target [ObservableProperty] private ReadOnlyObservableCollection _appsObservableList; #pragma warning restore MVVMTK0042 @@ -129,8 +130,7 @@ namespace Ryujinx.Ava.UI.ViewModels [ObservableProperty] public partial string LastScannedAmiiboId { get; set; } - [ObservableProperty] - public partial long LastFullscreenToggle { get; set; } = Environment.TickCount64; + [ObservableProperty] public partial long LastFullscreenToggle { get; set; } = Environment.TickCount64; [ObservableProperty] public partial bool ShowContent { get; set; } = true; [ObservableProperty] public partial float VolumeBeforeMute { get; set; } @@ -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; @@ -1862,6 +1865,29 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public void ReloadRenderDocApi() + { + RenderDoc.ReloadApi(ignoreAlreadyLoaded: true); + + OnPropertiesChanged(nameof(ShowStartCaptureButton), nameof(ShowEndCaptureButton), nameof(RenderDocIsAvailable)); + + if (RenderDoc.IsAvailable) + RenderDocIsCapturing = RenderDoc.IsFrameCapturing; + + NotificationHelper.ShowInformation( + "RenderDoc API reloaded", + RenderDoc.IsAvailable ? "RenderDoc is now available." : "RenderDoc is no longer available." + ); + } + + public void ToggleCapture() + { + if (ShowLoadProgress) return; + + AppHost.RendererHost.EmbeddedWindow.ToggleRenderDocCapture(AppHost.Device); + RenderDocIsCapturing = RenderDoc.IsFrameCapturing; + } + public void ToggleFullscreen() { if (Environment.TickCount64 - LastFullscreenToggle < HotKeyPressDelayMs) @@ -1952,7 +1978,8 @@ namespace Ryujinx.Ava.UI.ViewModels if (ConfigurationState.Instance.Debug.EnableGdbStub) { NotificationHelper.ShowInformation( - LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.NotificationLaunchCheckGdbStubTitle, ConfigurationState.Instance.Debug.GdbStubPort.Value), + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.NotificationLaunchCheckGdbStubTitle, + ConfigurationState.Instance.Debug.GdbStubPort.Value), LocaleManager.Instance[LocaleKeys.NotificationLaunchCheckGdbStubMessage]); } @@ -1961,10 +1988,12 @@ namespace Ryujinx.Ava.UI.ViewModels var memoryConfigurationLocaleKey = ConfigurationState.Instance.System.DramSize.Value switch { MemoryConfiguration.MemoryConfiguration4GiB or - MemoryConfiguration.MemoryConfiguration4GiBAppletDev or - MemoryConfiguration.MemoryConfiguration4GiBSystemDev => LocaleKeys.SettingsTabSystemDramSize4GiB, + MemoryConfiguration.MemoryConfiguration4GiBAppletDev or + MemoryConfiguration.MemoryConfiguration4GiBSystemDev => + LocaleKeys.SettingsTabSystemDramSize4GiB, MemoryConfiguration.MemoryConfiguration6GiB or - MemoryConfiguration.MemoryConfiguration6GiBAppletDev => LocaleKeys.SettingsTabSystemDramSize6GiB, + MemoryConfiguration.MemoryConfiguration6GiBAppletDev => + LocaleKeys.SettingsTabSystemDramSize6GiB, MemoryConfiguration.MemoryConfiguration8GiB => LocaleKeys.SettingsTabSystemDramSize8GiB, MemoryConfiguration.MemoryConfiguration12GiB => LocaleKeys.SettingsTabSystemDramSize12GiB, _ => LocaleKeys.SettingsTabSystemDramSize4GiB, @@ -1972,9 +2001,9 @@ namespace Ryujinx.Ava.UI.ViewModels NotificationHelper.ShowWarning( LocaleManager.Instance.UpdateAndGetDynamicValue( - LocaleKeys.NotificationLaunchCheckDramSizeTitle, + LocaleKeys.NotificationLaunchCheckDramSizeTitle, LocaleManager.Instance[memoryConfigurationLocaleKey] - ), + ), LocaleManager.Instance[LocaleKeys.NotificationLaunchCheckDramSizeMessage]); } } @@ -2459,6 +2488,67 @@ namespace Ryujinx.Ava.UI.ViewModels png.SaveTo(fileStream); }); + public bool ShowStartCaptureButton => !RenderDocIsCapturing && RenderDoc.IsAvailable; + public bool ShowEndCaptureButton => RenderDocIsCapturing && RenderDoc.IsAvailable; + public static bool RenderDocIsAvailable => RenderDoc.IsAvailable; + + public bool RenderDocIsCapturing + { + get; + set + { + field = value; + OnPropertyChanged(); + OnPropertiesChanged(nameof(ShowStartCaptureButton), nameof(ShowEndCaptureButton)); + } + } + + public static RelayCommand StartRenderDocCapture { get; } = + Commands.CreateConditional(vm => RenderDoc.IsAvailable && !vm.ShowLoadProgress, + viewModel => + { + if (!RenderDoc.IsFrameCapturing) + { + if (viewModel.AppHost.RendererHost + .EmbeddedWindow.StartRenderDocCapture(viewModel.AppHost.Device)) + { + Logger.Info?.Print(LogClass.Application, "Starting RenderDoc capture."); + } + } + + viewModel.RenderDocIsCapturing = RenderDoc.IsFrameCapturing; + }); + + public static RelayCommand EndRenderDocCapture { get; } = + Commands.CreateConditional(vm => RenderDoc.IsAvailable && !vm.ShowLoadProgress, + viewModel => + { + if (RenderDoc.IsFrameCapturing) + { + if (viewModel.AppHost.RendererHost.EmbeddedWindow.EndRenderDocCapture()) + { + Logger.Info?.Print(LogClass.Application, "Ended RenderDoc capture."); + } + } + + viewModel.RenderDocIsCapturing = RenderDoc.IsFrameCapturing; + }); + + public static RelayCommand DiscardRenderDocCapture { get; } = + Commands.CreateConditional(vm => RenderDoc.IsAvailable && !vm.ShowLoadProgress, + viewModel => + { + if (RenderDoc.IsFrameCapturing) + { + if (viewModel.AppHost.RendererHost.EmbeddedWindow.DiscardRenderDocCapture()) + { + Logger.Info?.Print(LogClass.Application, "Discarded RenderDoc capture."); + } + } + + viewModel.RenderDocIsCapturing = RenderDoc.IsFrameCapturing; + }); + #endregion } } diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 233c30bad..d5d9b8218 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -88,6 +88,11 @@ namespace Ryujinx.Ava.UI.ViewModels get; } + public bool IsCustomConfig + { + get; + } + public bool IsGameTitleNotNull => !string.IsNullOrEmpty(GameTitle); public double PanelOpacity => IsGameTitleNotNull ? 0.5 : 1; @@ -459,7 +464,7 @@ namespace Ryujinx.Ava.UI.ViewModels using MemoryStream ms = new(gameIconData); GameIcon = new Bitmap(ms); } - + IsCustomConfig = customConfig; IsGameRunning = gameRunning; GamePath = gamePath; GameTitle = gameName; @@ -869,16 +874,11 @@ namespace Ryujinx.Ava.UI.ViewModels GameListNeedsRefresh = false; } - private static void RevertIfNotSaved() + private static void RevertIfNotSaved(bool isCustomConfig = false, bool isGameRunning = false) { - /* - maybe this is an unnecessary check(all options need to be tested) - if (string.IsNullOrEmpty(Program.GlobalConfigurationPath)) - { - Program.ReloadConfig(); - } - */ - Program.ReloadConfig(); + // Restores settings for a custom configuration during a game, if the condition is met. + // If the condition is not met (parameter is false), restores global (default) configuration instead. + Program.ReloadConfig(isCustomConfig && isGameRunning); } public void ApplyButton() @@ -895,14 +895,14 @@ namespace Ryujinx.Ava.UI.ViewModels File.Delete(gameDir); } - RevertIfNotSaved(); + RevertIfNotSaved(IsCustomConfig, IsGameRunning); CloseWindow?.Invoke(); } public void SaveUserConfig() { SaveSettings(); - RevertIfNotSaved(); // Revert global configuration after saving user configuration + RevertIfNotSaved(IsCustomConfig, IsGameRunning); // Revert global or custom configuration after saving user configuration CloseWindow?.Invoke(); } @@ -934,7 +934,7 @@ namespace Ryujinx.Ava.UI.ViewModels public void CancelButton() { - RevertIfNotSaved(); + RevertIfNotSaved(IsCustomConfig, IsGameRunning); CloseWindow?.Invoke(); } } diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index 47f79725c..13a5d4a40 100755 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -8,6 +8,7 @@ xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:common="clr-namespace:Ryujinx.Common;assembly=Ryujinx.Common" + xmlns:renderDocApi="clr-namespace:Ryujinx.Graphics.RenderDocApi;assembly=Ryujinx.Graphics.RenderDocApi" x:DataType="viewModels:MainWindowViewModel" x:Class="Ryujinx.Ava.UI.Views.Main.MainMenuBarView"> @@ -200,6 +201,29 @@ Header="{ext:Locale GameListContextMenuManageCheat}" Icon="{ext:Icon fa-solid fa-code}" IsEnabled="{Binding IsGameRunning}" /> + + + + diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs index f6bf43795..f35d72b6f 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -85,37 +85,24 @@ namespace Ryujinx.Ava.UI.Views.Main private static IEnumerable 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; - 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; - - if (index == -1) - { - languageName = language; - } - else - { - string tr = locales.Locales[index].Translations[language]; - languageName = string.IsNullOrEmpty(tr) - ? language - : tr; - } + string languageName = string.IsNullOrEmpty(language) ? code : language; MenuItem menuItem = new() { Padding = new Thickness(15, 0, 0, 0), Margin = new Thickness(3, 0, 3, 0), HorizontalAlignment = HorizontalAlignment.Stretch, - Header = language == currentLanguageCode ? $"{languageName} ✔" : languageName, - Command = Commands.Create(() => MainWindowViewModel.ChangeLanguage(language)) + Header = code == currentLanguageCode ? $"{languageName} ✔" : languageName, + Command = Commands.Create(() => MainWindowViewModel.ChangeLanguage(code)) }; yield return menuItem; diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml b/src/Ryujinx/UI/Windows/MainWindow.axaml index 684b39ef3..b7385c9cb 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml @@ -41,6 +41,8 @@ + + diff --git a/src/Ryujinx/Utilities/CommandLineState.cs b/src/Ryujinx/Utilities/CommandLineState.cs index 28f302e9d..a4e6cd811 100644 --- a/src/Ryujinx/Utilities/CommandLineState.cs +++ b/src/Ryujinx/Utilities/CommandLineState.cs @@ -19,6 +19,9 @@ namespace Ryujinx.Ava.Utilities public static string OverrideSystemLanguage { get; private set; } public static string OverrideHideCursor { get; private set; } public static string BaseDirPathArg { get; private set; } + + public static string RenderDocCaptureTitleFormat { get; private set; } = + "{EmuVersion}\n{GuestName} {GuestVersion} {GuestTitleId} {GuestArch}"; public static Optional FirmwareToInstallPathArg { get; set; } public static string Profile { get; private set; } public static string LaunchPathArg { get; private set; } @@ -54,6 +57,20 @@ namespace Ryujinx.Ava.Utilities BaseDirPathArg = args[++i]; + arguments.Add(arg); + arguments.Add(args[i]); + break; + case "-rdct": + case "--rd-capture-title-format": + if (i + 1 >= args.Length) + { + Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'"); + + continue; + } + + RenderDocCaptureTitleFormat = args[++i]; + arguments.Add(arg); arguments.Add(args[i]); break; diff --git a/src/Ryujinx/Utilities/TitleHelper.cs b/src/Ryujinx/Utilities/TitleHelper.cs index 5e0916c27..3d1e53fd7 100644 --- a/src/Ryujinx/Utilities/TitleHelper.cs +++ b/src/Ryujinx/Utilities/TitleHelper.cs @@ -1,3 +1,4 @@ +using Gommon; using Ryujinx.HLE.Loaders.Processes; namespace Ryujinx.Ava.Utilities @@ -22,5 +23,23 @@ namespace Ryujinx.Ava.Utilities ? appTitle + $" ({pauseString})" : appTitle; } + + public static string FormatRenderDocCaptureTitle(ProcessResult activeProcess, string applicationVersion) + { + if (activeProcess == null) + return string.Empty; + + string titleNameSection = string.IsNullOrWhiteSpace(activeProcess.Name) ? string.Empty : activeProcess.Name; + string titleVersionSection = string.IsNullOrWhiteSpace(activeProcess.DisplayVersion) ? string.Empty : $"v{activeProcess.DisplayVersion}"; + string titleIdSection = $"({activeProcess.ProgramIdText.ToUpper()})"; + string titleArchSection = activeProcess.Is64Bit ? "(64-bit)" : "(32-bit)"; + + return CommandLineState.RenderDocCaptureTitleFormat + .ReplaceIgnoreCase("{EmuVersion}", applicationVersion) + .ReplaceIgnoreCase("{GuestName}", titleNameSection) + .ReplaceIgnoreCase("{GuestVersion}", titleVersionSection) + .ReplaceIgnoreCase("{GuestTitleId}", titleIdSection) + .ReplaceIgnoreCase("{GuestArch}", titleArchSection); + } } }