mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2026-05-26 23:19:15 +00:00
Compare commits
53 Commits
Canary-1.3
...
setup-wiza
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7dd718d6f | ||
|
|
6ee7957574 | ||
|
|
bf62531802 | ||
|
|
17be50ea80 | ||
|
|
ec50a1ec3e | ||
|
|
5a20047e5e | ||
|
|
f9fed4cf4d | ||
|
|
2970dcd3c7 | ||
|
|
4be6cb2fa1 | ||
|
|
c90d2af9cd | ||
|
|
13ff9cb162 | ||
|
|
b35ba58831 | ||
|
|
e12a77d4a3 | ||
|
|
804a4e0bcb | ||
|
|
94870eafaa | ||
|
|
7e6cc31866 | ||
|
|
3b25c43abf | ||
|
|
1804dd031b | ||
|
|
211498e060 | ||
|
|
4bdee89288 | ||
|
|
d8a6364cca | ||
|
|
2f794794c6 | ||
|
|
1d6c2426df | ||
|
|
6cd03f15fa | ||
|
|
3fe7600382 | ||
|
|
dc2aa837b3 | ||
|
|
133ac41425 | ||
|
|
fd2ecee479 | ||
|
|
8f529d17a8 | ||
|
|
884d0f526c | ||
|
|
c5b325bde2 | ||
|
|
8ab851ead8 | ||
|
|
5a060cf451 | ||
|
|
9b0fa3bf6d | ||
|
|
325e13a490 | ||
|
|
e202cccc6e | ||
|
|
e0ed8f56ea | ||
|
|
46b2fb92d7 | ||
|
|
8563e7d4dc | ||
|
|
ee10cbf735 | ||
|
|
b033adbde7 | ||
|
|
66f339d265 | ||
|
|
6cdbdfd329 | ||
|
|
9f817d60d5 | ||
|
|
5cffc95be6 | ||
|
|
2c0977f6b3 | ||
|
|
3a593b6084 | ||
|
|
c3155fcadb | ||
|
|
fd7554425a | ||
|
|
52700f71dc | ||
|
|
b018a44ff0 | ||
|
|
d522bfef62 | ||
|
|
39f55b2af3 |
101
.github/workflows/canary.yml
vendored
101
.github/workflows/canary.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Canary release job
|
name: Canary CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -19,7 +19,6 @@ concurrency: release
|
|||||||
env:
|
env:
|
||||||
POWERSHELL_TELEMETRY_OPTOUT: 1
|
POWERSHELL_TELEMETRY_OPTOUT: 1
|
||||||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||||
RYUJINX_BASE_VERSION: "1.3"
|
|
||||||
RYUJINX_TARGET_RELEASE_CHANNEL_NAME: "canary"
|
RYUJINX_TARGET_RELEASE_CHANNEL_NAME: "canary"
|
||||||
RELEASE: 1
|
RELEASE: 1
|
||||||
|
|
||||||
@@ -30,8 +29,8 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
platform:
|
platform:
|
||||||
- { name: win-x64, os: windows-latest, zip_os_name: win_x64 }
|
- { name: win-x64, os: ubuntu-latest, zip_os_name: win_x64 }
|
||||||
#- { name: win-arm64, os: windows-latest, zip_os_name: win_arm64 }
|
#- { name: win-arm64, os: ubuntu-latest, zip_os_name: win_arm64 }
|
||||||
- { name: linux-x64, os: ubuntu-latest, zip_os_name: linux_x64 }
|
- { name: linux-x64, os: ubuntu-latest, zip_os_name: linux_x64 }
|
||||||
- { name: linux-arm64, os: ubuntu-latest, zip_os_name: linux_arm64 }
|
- { name: linux-arm64, os: ubuntu-latest, zip_os_name: linux_arm64 }
|
||||||
steps:
|
steps:
|
||||||
@@ -44,12 +43,27 @@ jobs:
|
|||||||
- name: Overwrite csc problem matcher
|
- name: Overwrite csc problem matcher
|
||||||
run: echo "::add-matcher::.github/csc.json"
|
run: echo "::add-matcher::.github/csc.json"
|
||||||
|
|
||||||
|
- name: Install 7zip
|
||||||
|
run: |
|
||||||
|
sudo apt install -y 7zip
|
||||||
|
|
||||||
|
- name: Install gli
|
||||||
|
run: |
|
||||||
|
mkdir -p $HOME/.bin
|
||||||
|
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
|
||||||
|
chmod +x gli
|
||||||
|
mv gli $HOME/.bin/
|
||||||
|
echo "$HOME/.bin" >> $GITHUB_PATH
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Get version info
|
- name: Get version info
|
||||||
id: version_info
|
id: version_info
|
||||||
run: |
|
run: |
|
||||||
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT
|
echo "build_version=$(gli get-next-version -c Canary -R)" >> $GITHUB_OUTPUT
|
||||||
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT
|
echo "prev_build_version=$(gli get-current-version -c Canary -R)" >> $GITHUB_OUTPUT
|
||||||
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
|
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
|
||||||
|
echo "commit_message=$(git log -1 --pretty=%B)" >> $GITHUB_OUTPUT
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Configure for release
|
- name: Configure for release
|
||||||
@@ -69,33 +83,20 @@ jobs:
|
|||||||
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx --self-contained
|
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx --self-contained
|
||||||
|
|
||||||
- name: Packing Windows builds
|
- name: Packing Windows builds
|
||||||
if: matrix.platform.os == 'windows-latest'
|
if: contains(matrix.platform.name, 'win')
|
||||||
run: |
|
run: |
|
||||||
pushd publish
|
pushd publish
|
||||||
rm libarmeilleure-jitsupport.dylib
|
rm libarmeilleure-jitsupport.dylib
|
||||||
7z a ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish
|
7z a ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish
|
||||||
popd
|
popd
|
||||||
|
|
||||||
gh release download -R GreemDev/GLI -O gli.exe -p 'GitLabCli-win_x64.exe'
|
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -p release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip
|
||||||
|
|
||||||
./gli.exe --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/canary --command=UploadGenericPackage "Ryubing-Canary|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip"
|
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Install GitLabCli
|
|
||||||
if: matrix.platform.os == 'ubuntu-latest'
|
|
||||||
run: |
|
|
||||||
mkdir -p $HOME/.bin
|
|
||||||
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64'
|
|
||||||
chmod +x gli
|
|
||||||
mv gli $HOME/.bin/
|
|
||||||
echo "$HOME/.bin" >> $GITHUB_PATH
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Packing Linux builds
|
- name: Packing Linux builds
|
||||||
if: matrix.platform.os == 'ubuntu-latest'
|
if: contains(matrix.platform.name, 'linux')
|
||||||
run: |
|
run: |
|
||||||
pushd publish
|
pushd publish
|
||||||
rm libarmeilleure-jitsupport.dylib
|
rm libarmeilleure-jitsupport.dylib
|
||||||
@@ -103,11 +104,11 @@ jobs:
|
|||||||
tar -czvf ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish
|
tar -czvf ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish
|
||||||
popd
|
popd
|
||||||
|
|
||||||
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/canary --command=UploadGenericPackage "Ryubing-Canary|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz"
|
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -p release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Build AppImage (Linux)
|
- name: Build AppImage (Linux)
|
||||||
if: matrix.platform.os == 'ubuntu-latest'
|
if: contains(matrix.platform.name, 'linux')
|
||||||
run: |
|
run: |
|
||||||
BUILD_VERSION="${{ steps.version_info.outputs.build_version }}"
|
BUILD_VERSION="${{ steps.version_info.outputs.build_version }}"
|
||||||
PLATFORM_NAME="${{ matrix.platform.name }}"
|
PLATFORM_NAME="${{ matrix.platform.name }}"
|
||||||
@@ -139,8 +140,8 @@ jobs:
|
|||||||
pushd publish_appimage
|
pushd publish_appimage
|
||||||
mv Ryujinx.AppImage ../release_output/ryujinx-canary-$BUILD_VERSION-$ARCH_NAME.AppImage
|
mv Ryujinx.AppImage ../release_output/ryujinx-canary-$BUILD_VERSION-$ARCH_NAME.AppImage
|
||||||
popd
|
popd
|
||||||
|
|
||||||
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/canary --command=UploadGenericPackage "Ryubing-Canary|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-canary-$BUILD_VERSION-$ARCH_NAME.AppImage"
|
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -p release_output/ryujinx-canary-$BUILD_VERSION-$ARCH_NAME.AppImage
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
macos_release:
|
macos_release:
|
||||||
@@ -159,10 +160,10 @@ jobs:
|
|||||||
chmod +x llvm.sh
|
chmod +x llvm.sh
|
||||||
sudo ./llvm.sh 17
|
sudo ./llvm.sh 17
|
||||||
|
|
||||||
- name: Install GitLabCli
|
- name: Install gli
|
||||||
run: |
|
run: |
|
||||||
mkdir -p $HOME/.bin
|
mkdir -p $HOME/.bin
|
||||||
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64'
|
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
|
||||||
chmod +x gli
|
chmod +x gli
|
||||||
mv gli $HOME/.bin/
|
mv gli $HOME/.bin/
|
||||||
echo "$HOME/.bin" >> $GITHUB_PATH
|
echo "$HOME/.bin" >> $GITHUB_PATH
|
||||||
@@ -183,9 +184,10 @@ jobs:
|
|||||||
- name: Get version info
|
- name: Get version info
|
||||||
id: version_info
|
id: version_info
|
||||||
run: |
|
run: |
|
||||||
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT
|
echo "build_version=$(gli get-next-version -c Canary -R)" >> $GITHUB_OUTPUT
|
||||||
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT
|
echo "prev_build_version=$(gli get-current-version -c Canary -R)" >> $GITHUB_OUTPUT
|
||||||
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
|
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Configure for release
|
- name: Configure for release
|
||||||
run: |
|
run: |
|
||||||
@@ -200,7 +202,7 @@ jobs:
|
|||||||
- name: Publish macOS Ryujinx
|
- name: Publish macOS Ryujinx
|
||||||
run: |
|
run: |
|
||||||
./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish_ava ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 1
|
./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish_ava ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 1
|
||||||
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/canary --command=UploadGenericPackage "Ryubing-Canary|${{ steps.version_info.outputs.build_version }}|publish_ava/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz"
|
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -p publish_ava/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz
|
||||||
|
|
||||||
create_gitlab_release:
|
create_gitlab_release:
|
||||||
name: Create GitLab Release
|
name: Create GitLab Release
|
||||||
@@ -210,37 +212,42 @@ jobs:
|
|||||||
- release
|
- release
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Get version info
|
- name: Install gli
|
||||||
id: version_info
|
|
||||||
run: |
|
|
||||||
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT
|
|
||||||
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Install GitLabCli
|
|
||||||
run: |
|
run: |
|
||||||
mkdir -p $HOME/.bin
|
mkdir -p $HOME/.bin
|
||||||
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64'
|
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
|
||||||
chmod +x gli
|
chmod +x gli
|
||||||
mv gli $HOME/.bin/
|
mv gli $HOME/.bin/
|
||||||
echo "$HOME/.bin" >> $GITHUB_PATH
|
echo "$HOME/.bin" >> $GITHUB_PATH
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Get version info
|
||||||
|
id: version_info
|
||||||
|
run: |
|
||||||
|
echo "build_version=$(gli get-next-version -c Canary -R)" >> $GITHUB_OUTPUT
|
||||||
|
echo "prev_build_version=$(gli get-current-version -c Canary -R)" >> $GITHUB_OUTPUT
|
||||||
|
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
|
||||||
|
echo "commit_message=$(git log -1 --pretty=%B)" >> $GITHUB_OUTPUT
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Create tag
|
- name: Create tag
|
||||||
run: |
|
run: |
|
||||||
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=CreateTag "Canary-${{ steps.version_info.outputs.build_version }}|${{ steps.version_info.outputs.git_short_hash }}"
|
gli create-tag -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Canary-${{ steps.version_info.outputs.build_version }} -r ${{ steps.version_info.outputs.git_short_hash }} -c "${{ steps.version_info.outputs.commit_message }}"
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
run: |
|
run: |
|
||||||
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/canary --command=CreateReleaseFromGenericPackageFiles "Ryubing-Canary|${{ steps.version_info.outputs.build_version }}|main|Canary ${{ steps.version_info.outputs.build_version }}|**Full Changelog:** [${{ steps.version_info.outputs.prev_build_version }}...${{ steps.version_info.outputs.build_version }}](https://git.ryujinx.app/ryubing/ryujinx/-/compare/Canary-${{ steps.version_info.outputs.prev_build_version }}...Canary-${{ steps.version_info.outputs.build_version }})"
|
gli create-release-from-generic-package-files -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -r main -t "Canary ${{ steps.version_info.outputs.build_version }}" -b "**Full Changelog:** [${{ steps.version_info.outputs.prev_build_version }}...${{ steps.version_info.outputs.build_version }}](https://git.ryujinx.app/ryubing/ryujinx/-/compare/Canary-${{ steps.version_info.outputs.prev_build_version }}...Canary-${{ steps.version_info.outputs.build_version }})"
|
||||||
|
|
||||||
- name: Send notification webhook
|
- name: Send notification webhook
|
||||||
run: |
|
run: |
|
||||||
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/canary --command=SendUpdateMessage "${{ steps.version_info.outputs.build_version }}|FF4500|${{ secrets.CANARY_DISCORD_WEBHOOK }}|https://avatars.githubusercontent.com/u/192939710?s=200&v=4|false"
|
gli send-update-message -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -t ${{ steps.version_info.outputs.build_version }} -c FF4500 -w ${{ secrets.CANARY_DISCORD_WEBHOOK }} -i https://avatars.githubusercontent.com/u/192939710?s=200&v=4
|
||||||
|
|
||||||
- name: Notify update server of new builds
|
- name: Notify update server of new builds
|
||||||
run: |
|
run: |
|
||||||
curl 'https://update.ryujinx.app/api/v1/admin/refresh_cache?rc=canary' -X PATCH -H 'accept: */*' -H 'Authorization: ${{ secrets.UPDATE_SERVER_ADMIN_TOKEN }}'
|
gli refresh-version-cache -T ${{ secrets.UPDATE_SERVER_ADMIN_TOKEN }} -c Canary
|
||||||
|
|
||||||
|
- name: Advance to the next version
|
||||||
|
run: |
|
||||||
|
gli increment-version -T ${{ secrets.UPDATE_SERVER_ADMIN_TOKEN }} -c Canary
|
||||||
|
|||||||
224
.github/workflows/debug_release.yml
vendored
224
.github/workflows/debug_release.yml
vendored
@@ -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"
|
|
||||||
123
.github/workflows/release.yml
vendored
123
.github/workflows/release.yml
vendored
@@ -1,15 +1,18 @@
|
|||||||
name: Release job
|
name: Stable CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs: {}
|
inputs:
|
||||||
|
is_bugfix_release:
|
||||||
|
description: "Bug fix release: If checked, this will increment the third number for only Stable, and leave the Major version alone for both Stable and Canary."
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
|
||||||
concurrency: release
|
concurrency: release
|
||||||
|
|
||||||
env:
|
env:
|
||||||
POWERSHELL_TELEMETRY_OPTOUT: 1
|
POWERSHELL_TELEMETRY_OPTOUT: 1
|
||||||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||||
RYUJINX_BASE_VERSION: "1.3"
|
|
||||||
RYUJINX_TARGET_RELEASE_CHANNEL_NAME: "release"
|
RYUJINX_TARGET_RELEASE_CHANNEL_NAME: "release"
|
||||||
RELEASE: 1
|
RELEASE: 1
|
||||||
|
|
||||||
@@ -20,8 +23,8 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
platform:
|
platform:
|
||||||
- { name: win-x64, os: windows-latest, zip_os_name: win_x64 }
|
- { name: win-x64, os: ubuntu-latest, zip_os_name: win_x64 }
|
||||||
#- { name: win-arm64, os: windows-latest, zip_os_name: win_arm64 }
|
#- { name: win-arm64, os: ubuntu-latest, zip_os_name: win_arm64 }
|
||||||
- { name: linux-x64, os: ubuntu-latest, zip_os_name: linux_x64 }
|
- { name: linux-x64, os: ubuntu-latest, zip_os_name: linux_x64 }
|
||||||
- { name: linux-arm64, os: ubuntu-latest, zip_os_name: linux_arm64 }
|
- { name: linux-arm64, os: ubuntu-latest, zip_os_name: linux_arm64 }
|
||||||
steps:
|
steps:
|
||||||
@@ -33,12 +36,30 @@ jobs:
|
|||||||
|
|
||||||
- name: Overwrite csc problem matcher
|
- name: Overwrite csc problem matcher
|
||||||
run: echo "::add-matcher::.github/csc.json"
|
run: echo "::add-matcher::.github/csc.json"
|
||||||
|
|
||||||
|
- name: Install 7zip
|
||||||
|
run: |
|
||||||
|
sudo apt install -y 7zip
|
||||||
|
|
||||||
|
- name: Install gli
|
||||||
|
run: |
|
||||||
|
mkdir -p $HOME/.bin
|
||||||
|
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
|
||||||
|
chmod +x gli
|
||||||
|
mv gli $HOME/.bin/
|
||||||
|
echo "$HOME/.bin" >> $GITHUB_PATH
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Get version info
|
- name: Get version info
|
||||||
id: version_info
|
id: version_info
|
||||||
run: |
|
run: |
|
||||||
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT
|
if [ '${{ inputs.is_bugfix_release }}' == 'false' ]; then
|
||||||
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT
|
echo "build_version=$(gli get-next-version -m -c Stable -R)" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "build_version=$(gli get-next-version -c Stable -R)" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
echo "prev_build_version=$(gli get-current-version -c Stable -R)" >> $GITHUB_OUTPUT
|
||||||
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
|
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
@@ -58,47 +79,34 @@ jobs:
|
|||||||
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx --self-contained
|
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx --self-contained
|
||||||
|
|
||||||
- name: Packing Windows builds
|
- name: Packing Windows builds
|
||||||
if: matrix.platform.os == 'windows-latest'
|
if: contains(matrix.platform.name, 'win')
|
||||||
run: |
|
run: |
|
||||||
pushd publish
|
pushd publish
|
||||||
rm libarmeilleure-jitsupport.dylib
|
rm libarmeilleure-jitsupport.dylib
|
||||||
7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish
|
7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish
|
||||||
popd
|
popd
|
||||||
|
|
||||||
gh release download -R GreemDev/GLI -O gli.exe -p 'GitLabCli-win_x64.exe'
|
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -p release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip
|
||||||
|
|
||||||
./gli.exe --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip"
|
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Install GitLabCli
|
|
||||||
if: matrix.platform.os == 'ubuntu-latest'
|
|
||||||
run: |
|
|
||||||
mkdir -p $HOME/.bin
|
|
||||||
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64'
|
|
||||||
chmod +x gli
|
|
||||||
mv gli $HOME/.bin/
|
|
||||||
echo "$HOME/.bin" >> $GITHUB_PATH
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Packing Linux builds
|
- name: Packing Linux builds
|
||||||
if: matrix.platform.os == 'ubuntu-latest'
|
if: contains(matrix.platform.name, 'linux')
|
||||||
run: |
|
run: |
|
||||||
pushd publish
|
pushd publish
|
||||||
rm libarmeilleure-jitsupport.dylib
|
rm libarmeilleure-jitsupport.dylib
|
||||||
chmod +x Ryujinx.sh Ryujinx
|
chmod +x Ryujinx.sh Ryujinx
|
||||||
tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish
|
tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish
|
||||||
popd
|
popd
|
||||||
|
|
||||||
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz"
|
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -p release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build AppImage (Linux)
|
- name: Build AppImage (Linux)
|
||||||
if: matrix.platform.os == 'ubuntu-latest'
|
if: contains(matrix.platform.name, 'linux')
|
||||||
run: |
|
run: |
|
||||||
BUILD_VERSION="${{ steps.version_info.outputs.build_version }}"
|
BUILD_VERSION="${{ steps.version_info.outputs.build_version }}"
|
||||||
PLATFORM_NAME="${{ matrix.platform.name }}"
|
PLATFORM_NAME="${{ matrix.platform.name }}"
|
||||||
@@ -131,7 +139,7 @@ jobs:
|
|||||||
mv Ryujinx.AppImage ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage
|
mv Ryujinx.AppImage ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage
|
||||||
popd
|
popd
|
||||||
|
|
||||||
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage"
|
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -p release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
macos_release:
|
macos_release:
|
||||||
@@ -150,10 +158,10 @@ jobs:
|
|||||||
chmod +x llvm.sh
|
chmod +x llvm.sh
|
||||||
sudo ./llvm.sh 17
|
sudo ./llvm.sh 17
|
||||||
|
|
||||||
- name: Install GitLabCli
|
- name: Install gli
|
||||||
run: |
|
run: |
|
||||||
mkdir -p $HOME/.bin
|
mkdir -p $HOME/.bin
|
||||||
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64'
|
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
|
||||||
chmod +x gli
|
chmod +x gli
|
||||||
mv gli $HOME/.bin/
|
mv gli $HOME/.bin/
|
||||||
echo "$HOME/.bin" >> $GITHUB_PATH
|
echo "$HOME/.bin" >> $GITHUB_PATH
|
||||||
@@ -174,9 +182,14 @@ jobs:
|
|||||||
- name: Get version info
|
- name: Get version info
|
||||||
id: version_info
|
id: version_info
|
||||||
run: |
|
run: |
|
||||||
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT
|
if [ '${{ inputs.is_bugfix_release }}' == 'false' ]; then
|
||||||
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT
|
echo "build_version=$(gli get-next-version -m -c Stable -R)" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "build_version=$(gli get-next-version -c Stable -R)" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
echo "prev_build_version=$(gli get-current-version -c Stable -R)" >> $GITHUB_OUTPUT
|
||||||
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
|
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Configure for release
|
- name: Configure for release
|
||||||
run: |
|
run: |
|
||||||
@@ -189,7 +202,8 @@ jobs:
|
|||||||
- name: Publish macOS Ryujinx
|
- name: Publish macOS Ryujinx
|
||||||
run: |
|
run: |
|
||||||
./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 0
|
./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 0
|
||||||
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|publish/ryujinx-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz"
|
|
||||||
|
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -p publish/ryujinx-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz
|
||||||
|
|
||||||
create_gitlab_release:
|
create_gitlab_release:
|
||||||
name: Create GitLab Release
|
name: Create GitLab Release
|
||||||
@@ -200,32 +214,45 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Get version info
|
- name: Install gli
|
||||||
id: version_info
|
|
||||||
run: |
|
|
||||||
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT
|
|
||||||
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Install GitLabCli
|
|
||||||
run: |
|
run: |
|
||||||
mkdir -p $HOME/.bin
|
mkdir -p $HOME/.bin
|
||||||
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64'
|
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
|
||||||
chmod +x gli
|
chmod +x gli
|
||||||
mv gli $HOME/.bin/
|
mv gli $HOME/.bin/
|
||||||
echo "$HOME/.bin" >> $GITHUB_PATH
|
echo "$HOME/.bin" >> $GITHUB_PATH
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Get version info
|
||||||
|
id: version_info
|
||||||
|
run: |
|
||||||
|
if [ '${{ inputs.is_bugfix_release }}' == 'false' ]; then
|
||||||
|
echo "build_version=$(gli get-next-version -m -c Stable -R)" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "build_version=$(gli get-next-version -c Stable -R)" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
echo "prev_build_version=$(gli get-current-version -c Stable -R)" >> $GITHUB_OUTPUT
|
||||||
|
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
|
||||||
|
echo "commit_message=$(git log -1 --pretty=%B)" >> $GITHUB_OUTPUT
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
run: |
|
run: |
|
||||||
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=CreateReleaseFromGenericPackageFiles "Ryubing|${{ steps.version_info.outputs.build_version }}|${{ steps.version_info.outputs.git_short_hash }}|${{ steps.version_info.outputs.build_version }}|msd:${{ steps.version_info.outputs.build_version }}"
|
gli create-release-from-generic-package-files -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -r ${{ steps.version_info.outputs.git_short_hash }} -t "${{ steps.version_info.outputs.build_version }}" -b "msd:${{ steps.version_info.outputs.build_version }}"
|
||||||
|
|
||||||
- name: Send notification webhook
|
- name: Send notification webhook
|
||||||
run: |
|
run: |
|
||||||
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=SendUpdateMessage "${{ steps.version_info.outputs.build_version }}|32cd32|${{ secrets.STABLE_DISCORD_WEBHOOK }}|https://avatars.githubusercontent.com/u/192939710?s=200&v=4|false"
|
gli send-update-message -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -t ${{ steps.version_info.outputs.build_version }} -c 32cd32 -w ${{ secrets.STABLE_DISCORD_WEBHOOK }} -i https://avatars.githubusercontent.com/u/192939710?s=200&v=4
|
||||||
|
|
||||||
- name: Notify update server of new builds
|
- name: Notify update server of new builds
|
||||||
run: |
|
run: |
|
||||||
curl 'https://update.ryujinx.app/api/v1/admin/refresh_cache?rc=stable' -X PATCH -H 'accept: */*' -H 'Authorization: ${{ secrets.UPDATE_SERVER_ADMIN_TOKEN }}'
|
gli refresh-version-cache -T ${{ secrets.UPDATE_SERVER_ADMIN_TOKEN }} -c Stable
|
||||||
|
|
||||||
|
- name: Advance to the next version
|
||||||
|
run: |
|
||||||
|
if [ '${{ inputs.is_bugfix_release }}' == 'false' ]; then
|
||||||
|
gli advance-version -T ${{ secrets.UPDATE_SERVER_ADMIN_TOKEN }}
|
||||||
|
else
|
||||||
|
gli increment-version -T ${{ secrets.UPDATE_SERVER_ADMIN_TOKEN }} -c Stable
|
||||||
|
fi
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -100,6 +100,7 @@ DocProject/Help/html
|
|||||||
|
|
||||||
# Click-Once directory
|
# Click-Once directory
|
||||||
publish/
|
publish/
|
||||||
|
RyubingMaintainerTools/
|
||||||
|
|
||||||
# Publish Web Output
|
# Publish Web Output
|
||||||
*.Publish.xml
|
*.Publish.xml
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
<PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.126" />
|
<PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.126" />
|
||||||
<PackageVersion Include="Ryujinx.UpdateClient" Version="1.0.44" />
|
<PackageVersion Include="Ryujinx.UpdateClient" Version="1.0.44" />
|
||||||
<PackageVersion Include="Ryujinx.Systems.Update.Common" Version="1.0.44" />
|
<PackageVersion Include="Ryujinx.Systems.Update.Common" Version="1.0.44" />
|
||||||
<PackageVersion Include="Gommon" Version="2.8.0.1" />
|
<PackageVersion Include="Gommon" Version="2.8.0.2" />
|
||||||
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
|
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
|
||||||
<PackageVersion Include="Sep" Version="0.11.1" />
|
<PackageVersion Include="Sep" Version="0.11.1" />
|
||||||
<PackageVersion Include="shaderc.net" Version="0.1.0" />
|
<PackageVersion Include="shaderc.net" Version="0.1.0" />
|
||||||
@@ -59,4 +59,4 @@
|
|||||||
<PackageVersion Include="System.Management" Version="9.0.2" />
|
<PackageVersion Include="System.Management" Version="9.0.2" />
|
||||||
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" />
|
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -24841,6 +24841,781 @@
|
|||||||
"zh_CN": "如果您在设置中有某些 LDN 口令则可加入此游戏。",
|
"zh_CN": "如果您在设置中有某些 LDN 口令则可加入此游戏。",
|
||||||
"zh_TW": "你只能加入與 LDN 網路密碼片語 (passphrase) 設定相同的遊戲。"
|
"zh_TW": "你只能加入與 LDN 網路密碼片語 (passphrase) 設定相同的遊戲。"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": "SetupWizardOpen",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Setup Wizard",
|
||||||
|
"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": "SetupWizardActionBack",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Back",
|
||||||
|
"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": "SetupWizardActionNext",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Next",
|
||||||
|
"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": "SetupWizardFirstPageTitle",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Welcome to Ryubing!",
|
||||||
|
"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": "SetupWizardFirstPageContent",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Ryubing is a fork of the discontinued Nintendo Switch emulator, Ryujinx.\n\nThis setup wizard will guide you through the necessary steps needed for Ryubing to play your Switch games on PC.",
|
||||||
|
"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": "SetupWizardFirstPageAction",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Start Setup",
|
||||||
|
"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": "SetupWizardKeysPageTitle",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Key Files",
|
||||||
|
"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": "SetupWizardKeysPageDescription",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Please select the folder containing your prod/title .keys files:",
|
||||||
|
"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": "SetupWizardKeysPageFolderPopupTitle",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Please select the folder containing your prod/title .keys files",
|
||||||
|
"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": "SetupWizardKeysPageHelpText",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Not sure how to get your keys?",
|
||||||
|
"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": "SetupWizardKeysPageSkipText",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Skipped setting up keys as you already have a valid key installation and did not choose a folder to install from.\nClick '{0}' if you wish to reinstall your keys.",
|
||||||
|
"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": "SetupWizardFirmwarePageTitle",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Switch Firmware",
|
||||||
|
"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": "SetupWizardFirmwarePageDescription",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Please select the folder or .zip/.xci containing your dumped Nintendo Switch firmware:",
|
||||||
|
"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": "SetupWizardFirmwarePageFolderPopupTitle",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Please select the folder containing your dumped & extracted Switch firmware.",
|
||||||
|
"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": "SetupWizardFirmwarePageFilePopupTitle",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Please select the file containing your dumped Switch firmware.",
|
||||||
|
"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": "SetupWizardFirmwarePageFileBrowse",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Select .zip or .xci",
|
||||||
|
"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": "SetupWizardFirmwarePageFolderBrowse",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Select Extracted Folder",
|
||||||
|
"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": "SetupWizardFirmwarePageInstallSuccessNotificationTitle",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Firmware installed",
|
||||||
|
"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": "SetupWizardFirmwarePageInstallSuccessNotificationText",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Installed firmware version {0}.",
|
||||||
|
"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": "SetupWizardFirmwarePageInstallFailNotificationTitle",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Firmware not installed",
|
||||||
|
"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": "SetupWizardFirmwarePageInstallFailNotificationText",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "It seems some error occurred when trying to install the firmware at path '{0}'.\nDid that folder contain a firmware dump?",
|
||||||
|
"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": "SetupWizardFirmwarePageHelpText",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Not sure how to get your firmware off of your Switch?",
|
||||||
|
"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": "SetupWizardFirmwarePageSkipText",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Skipped setting up firmware as you already have a valid firmware installation and did not choose a folder or file to install from.\nClick '{0}' if you wish to overwrite your firmware.",
|
||||||
|
"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": "SetupWizardGameDirsPageTitle",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Game, Update, and DLC Paths",
|
||||||
|
"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": "SetupWizardGameDirsPageDescription",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "{0} can be pointed at any number of folders to look for your games, updates, and DLC content.\nAt least one folder must be specified in game directories before continuing.",
|
||||||
|
"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": "SetupWizardGameDirsPageNoFoldersSelectedError",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "At least one folder for games must be selected; otherwise the UI will be empty.",
|
||||||
|
"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": "SetupWizardGameDirsPageHelpText",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Not sure how to get your games, updates, and/or DLC onto your PC?",
|
||||||
|
"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": "SetupWizardFinalPageTitle",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Setup Complete",
|
||||||
|
"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": "SetupWizardFinalPageDescription",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Your installation of Ryubing (aka Ryujinx) has been completed.\n\nIf you require assistance, feel free to join our Discord server and ask for help,\nafter verifying your possession of a modded Nintendo Switch.",
|
||||||
|
"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": "SetupWizardFinalPageAction",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Finish",
|
||||||
|
"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": "SetupWizardHelpLinkButton",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Click here to view a guide.",
|
||||||
|
"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": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ namespace ARMeilleure.Instructions
|
|||||||
|
|
||||||
context.LoadFromContext();
|
context.LoadFromContext();
|
||||||
|
|
||||||
context.Return(Const(op.Address));
|
InstEmitFlowHelper.EmitReturn(context, Const(op.Address));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Svc(ArmEmitterContext context)
|
public static void Svc(ArmEmitterContext context)
|
||||||
@@ -49,7 +49,7 @@ namespace ARMeilleure.Instructions
|
|||||||
|
|
||||||
context.LoadFromContext();
|
context.LoadFromContext();
|
||||||
|
|
||||||
context.Return(Const(op.Address));
|
InstEmitFlowHelper.EmitReturn(context, Const(op.Address));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ namespace ARMeilleure.Instructions
|
|||||||
|
|
||||||
context.LoadFromContext();
|
context.LoadFromContext();
|
||||||
|
|
||||||
context.Return(Const(context.CurrOp.Address));
|
InstEmitFlowHelper.EmitReturn(context, Const(context.CurrOp.Address));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ namespace ARMeilleure.Instructions
|
|||||||
{
|
{
|
||||||
OpCodeBReg op = (OpCodeBReg)context.CurrOp;
|
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);
|
public static void Tbnz(ArmEmitterContext context) => EmitTb(context, onNotZero: true);
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ namespace ARMeilleure.Instructions
|
|||||||
{
|
{
|
||||||
static class InstEmitFlowHelper
|
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)
|
public static void EmitCondBranch(ArmEmitterContext context, Operand target, Condition cond)
|
||||||
{
|
{
|
||||||
if (cond != Condition.Al)
|
if (cond != Condition.Al)
|
||||||
@@ -182,12 +186,7 @@ namespace ARMeilleure.Instructions
|
|||||||
{
|
{
|
||||||
if (isReturn || context.IsSingleStep)
|
if (isReturn || context.IsSingleStep)
|
||||||
{
|
{
|
||||||
if (target.Type == OperandType.I32)
|
EmitReturn(context, target);
|
||||||
{
|
|
||||||
target = context.ZeroExtend32(OperandType.I64, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Return(target);
|
|
||||||
}
|
}
|
||||||
else
|
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)
|
private static void EmitTableBranch(ArmEmitterContext context, Operand guestAddress, bool isJump)
|
||||||
{
|
{
|
||||||
context.StoreToContext();
|
context.StoreToContext();
|
||||||
@@ -257,6 +269,8 @@ namespace ARMeilleure.Instructions
|
|||||||
|
|
||||||
if (isJump)
|
if (isJump)
|
||||||
{
|
{
|
||||||
|
DecreaseCallDepth(context, nativeContext);
|
||||||
|
|
||||||
context.Tailcall(hostAddress, nativeContext);
|
context.Tailcall(hostAddress, nativeContext);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -278,8 +292,42 @@ namespace ARMeilleure.Instructions
|
|||||||
Operand lblContinue = context.GetLabel(nextAddr.Value);
|
Operand lblContinue = context.GetLabel(nextAddr.Value);
|
||||||
context.BranchIf(lblContinue, returnAddress, nextAddr, Comparison.Equal, BasicBlockFrequency.Cold);
|
context.BranchIf(lblContinue, returnAddress, nextAddr, Comparison.Equal, BasicBlockFrequency.Cold);
|
||||||
|
|
||||||
|
DecreaseCallDepth(context, nativeContext);
|
||||||
|
|
||||||
context.Return(returnAddress);
|
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)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace ARMeilleure
|
|||||||
public static bool AllowLcqInFunctionTable { get; set; } = true;
|
public static bool AllowLcqInFunctionTable { get; set; } = true;
|
||||||
public static bool UseUnmanagedDispatchLoop { get; set; } = true;
|
public static bool UseUnmanagedDispatchLoop { get; set; } = true;
|
||||||
public static bool EnableDebugging { get; set; } = false;
|
public static bool EnableDebugging { get; set; } = false;
|
||||||
|
public static bool EnableDeepCallRecursionProtection { get; set; } = true;
|
||||||
|
|
||||||
public static bool UseAdvSimdIfAvailable { get; set; } = true;
|
public static bool UseAdvSimdIfAvailable { get; set; } = true;
|
||||||
public static bool UseArm64AesIfAvailable { get; set; } = true;
|
public static bool UseArm64AesIfAvailable { get; set; } = true;
|
||||||
|
|||||||
@@ -134,6 +134,11 @@ namespace ARMeilleure.State
|
|||||||
public bool GetFPstateFlag(FPState flag) => _nativeContext.GetFPStateFlag(flag);
|
public bool GetFPstateFlag(FPState flag) => _nativeContext.GetFPStateFlag(flag);
|
||||||
public void SetFPstateFlag(FPState flag, bool value) => _nativeContext.SetFPStateFlag(flag, value);
|
public void SetFPstateFlag(FPState flag, bool value) => _nativeContext.SetFPStateFlag(flag, value);
|
||||||
|
|
||||||
|
internal void ResetCallDepth()
|
||||||
|
{
|
||||||
|
_nativeContext.ResetCallDepth();
|
||||||
|
}
|
||||||
|
|
||||||
internal void CheckInterrupt()
|
internal void CheckInterrupt()
|
||||||
{
|
{
|
||||||
if (Interrupted)
|
if (Interrupted)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ namespace ARMeilleure.State
|
|||||||
public ulong ExclusiveValueHigh;
|
public ulong ExclusiveValueHigh;
|
||||||
public int Running;
|
public int Running;
|
||||||
public long Tpidr2El0;
|
public long Tpidr2El0;
|
||||||
|
public int CallDepth;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Precise PC value used for debugging.
|
/// Precise PC value used for debugging.
|
||||||
@@ -199,6 +200,8 @@ namespace ARMeilleure.State
|
|||||||
public bool GetRunning() => GetStorage().Running != 0;
|
public bool GetRunning() => GetStorage().Running != 0;
|
||||||
public void SetRunning(bool value) => GetStorage().Running = value ? 1 : 0;
|
public void SetRunning(bool value) => GetStorage().Running = value ? 1 : 0;
|
||||||
|
|
||||||
|
public void ResetCallDepth() => GetStorage().CallDepth = 0;
|
||||||
|
|
||||||
public unsafe static int GetRegisterOffset(Register reg)
|
public unsafe static int GetRegisterOffset(Register reg)
|
||||||
{
|
{
|
||||||
if (reg.Type == RegisterType.Integer)
|
if (reg.Type == RegisterType.Integer)
|
||||||
@@ -284,6 +287,11 @@ namespace ARMeilleure.State
|
|||||||
return StorageOffset(ref _dummyStorage, ref _dummyStorage.DebugPrecisePc);
|
return StorageOffset(ref _dummyStorage, ref _dummyStorage.DebugPrecisePc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int GetCallDepthOffset()
|
||||||
|
{
|
||||||
|
return StorageOffset(ref _dummyStorage, ref _dummyStorage.CallDepth);
|
||||||
|
}
|
||||||
|
|
||||||
private static int StorageOffset<T>(ref NativeCtxStorage storage, ref T target)
|
private static int StorageOffset<T>(ref NativeCtxStorage storage, ref T target)
|
||||||
{
|
{
|
||||||
return (int)Unsafe.ByteOffset(ref Unsafe.As<NativeCtxStorage, T>(ref storage), ref target);
|
return (int)Unsafe.ByteOffset(ref Unsafe.As<NativeCtxStorage, T>(ref storage), ref target);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ namespace ARMeilleure.Translation.PTC
|
|||||||
private const string OuterHeaderMagicString = "PTCohd\0\0";
|
private const string OuterHeaderMagicString = "PTCohd\0\0";
|
||||||
private const string InnerHeaderMagicString = "PTCihd\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 ActualDir = "0";
|
||||||
private const string BackupDir = "1";
|
private const string BackupDir = "1";
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ namespace ARMeilleure.Translation
|
|||||||
|
|
||||||
Statistics.StartTimer();
|
Statistics.StartTimer();
|
||||||
|
|
||||||
|
context.ResetCallDepth();
|
||||||
ulong nextAddr = func.Execute(Stubs.ContextWrapper, context);
|
ulong nextAddr = func.Execute(Stubs.ContextWrapper, context);
|
||||||
|
|
||||||
Statistics.StopTimer(address);
|
Statistics.StopTimer(address);
|
||||||
@@ -260,6 +261,7 @@ namespace ARMeilleure.Translation
|
|||||||
|
|
||||||
Logger.StartPass(PassName.Translation);
|
Logger.StartPass(PassName.Translation);
|
||||||
|
|
||||||
|
InstEmitFlowHelper.EmitCallDepthCheckAndIncrement(context, Const(address));
|
||||||
EmitSynchronization(context);
|
EmitSynchronization(context);
|
||||||
|
|
||||||
if (blocks[0].Address != address)
|
if (blocks[0].Address != address)
|
||||||
|
|||||||
@@ -262,10 +262,18 @@ namespace ARMeilleure.Translation
|
|||||||
|
|
||||||
Operand runningAddress = context.Add(nativeContext, Const((ulong)NativeContext.GetRunningOffset()));
|
Operand runningAddress = context.Add(nativeContext, Const((ulong)NativeContext.GetRunningOffset()));
|
||||||
Operand dispatchAddress = context.Add(nativeContext, Const((ulong)NativeContext.GetDispatchAddressOffset()));
|
Operand dispatchAddress = context.Add(nativeContext, Const((ulong)NativeContext.GetDispatchAddressOffset()));
|
||||||
|
Operand callDepthAddress = context.Add(nativeContext, Const((ulong)NativeContext.GetCallDepthOffset()));
|
||||||
|
|
||||||
EmitSyncFpContext(context, nativeContext, true);
|
EmitSyncFpContext(context, nativeContext, true);
|
||||||
|
|
||||||
context.MarkLabel(beginLbl);
|
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.Store(dispatchAddress, guestAddress);
|
||||||
context.Copy(guestAddress, context.Call(Const((ulong)DispatchStub), OperandType.I64, nativeContext));
|
context.Copy(guestAddress, context.Call(Const((ulong)DispatchStub), OperandType.I64, nativeContext));
|
||||||
context.BranchIfFalse(endLbl, guestAddress);
|
context.BranchIfFalse(endLbl, guestAddress);
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ namespace Ryujinx.Audio.Renderer.Parameter.Effect
|
|||||||
/// The output channel indices that will be used by the <see cref="Dsp.AudioProcessor"/>.
|
/// The output channel indices that will be used by the <see cref="Dsp.AudioProcessor"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Array6<byte> Output;
|
public Array6<byte> Output;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reserved/unused.
|
||||||
|
/// </summary>
|
||||||
|
private readonly uint _padding;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Biquad filter numerator (b0, b1, b2).
|
/// Biquad filter numerator (b0, b1, b2).
|
||||||
|
|||||||
@@ -91,7 +91,11 @@ namespace Ryujinx.Common
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_queue.CompleteAdding();
|
try
|
||||||
|
{
|
||||||
|
_queue.CompleteAdding();
|
||||||
|
} catch (ObjectDisposedException) {}
|
||||||
|
|
||||||
_cts.Cancel();
|
_cts.Cancel();
|
||||||
_workerThread.Join();
|
_workerThread.Join();
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ namespace Ryujinx.Common.Configuration
|
|||||||
public static string KeysDirPath { get; private set; }
|
public static string KeysDirPath { get; private set; }
|
||||||
public static string KeysDirPathUser { get; }
|
public static string KeysDirPathUser { get; }
|
||||||
|
|
||||||
|
public static string GetKeysDir() =>
|
||||||
|
Mode is LaunchMode.UserProfile && Directory.Exists(KeysDirPathUser)
|
||||||
|
? KeysDirPathUser
|
||||||
|
: KeysDirPath;
|
||||||
|
|
||||||
public static string LogsDirPath { get; private set; }
|
public static string LogsDirPath { get; private set; }
|
||||||
|
|
||||||
public const string DefaultNandDir = "bis";
|
public const string DefaultNandDir = "bis";
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ namespace Ryujinx.Common.Memory
|
|||||||
{
|
{
|
||||||
private static readonly RecyclableMemoryStreamManager _shared = new();
|
private static readonly RecyclableMemoryStreamManager _shared = new();
|
||||||
|
|
||||||
|
private static readonly ObjectPool<RecyclableMemoryStream> _streamPool =
|
||||||
|
new(() => new RecyclableMemoryStream(_shared, Guid.NewGuid(), null, 0));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// We don't expose the <c>RecyclableMemoryStreamManager</c> directly because version 2.x
|
/// We don't expose the <c>RecyclableMemoryStreamManager</c> directly because version 2.x
|
||||||
/// returns them as <c>MemoryStream</c>. This Shared class is here to a) offer only the GetStream() versions we use
|
/// returns them as <c>MemoryStream</c>. This Shared class is here to a) offer only the GetStream() versions we use
|
||||||
@@ -19,7 +22,12 @@ namespace Ryujinx.Common.Memory
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A <c>RecyclableMemoryStream</c></returns>
|
/// <returns>A <c>RecyclableMemoryStream</c></returns>
|
||||||
public static RecyclableMemoryStream GetStream()
|
public static RecyclableMemoryStream GetStream()
|
||||||
=> new(_shared);
|
{
|
||||||
|
RecyclableMemoryStream stream = _streamPool.Allocate();
|
||||||
|
stream.SetLength(0);
|
||||||
|
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieve a new <c>MemoryStream</c> object with the contents copied from the provided
|
/// Retrieve a new <c>MemoryStream</c> object with the contents copied from the provided
|
||||||
@@ -55,7 +63,8 @@ namespace Ryujinx.Common.Memory
|
|||||||
RecyclableMemoryStream stream = null;
|
RecyclableMemoryStream stream = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
stream = new RecyclableMemoryStream(_shared, id, tag, buffer.Length);
|
stream = _streamPool.Allocate();
|
||||||
|
stream.SetLength(0);
|
||||||
stream.Write(buffer);
|
stream.Write(buffer);
|
||||||
stream.Position = 0;
|
stream.Position = 0;
|
||||||
return stream;
|
return stream;
|
||||||
@@ -83,7 +92,8 @@ namespace Ryujinx.Common.Memory
|
|||||||
RecyclableMemoryStream stream = null;
|
RecyclableMemoryStream stream = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
stream = new RecyclableMemoryStream(_shared, id, tag, count);
|
stream = _streamPool.Allocate();
|
||||||
|
stream.SetLength(0);
|
||||||
stream.Write(buffer, offset, count);
|
stream.Write(buffer, offset, count);
|
||||||
stream.Position = 0;
|
stream.Position = 0;
|
||||||
return stream;
|
return stream;
|
||||||
@@ -94,6 +104,11 @@ namespace Ryujinx.Common.Memory
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void ReleaseStream(RecyclableMemoryStream stream)
|
||||||
|
{
|
||||||
|
_streamPool.Release(stream);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,15 @@ namespace Ryujinx.Common
|
|||||||
public const string SetupGuideWikiUrl =
|
public const string SetupGuideWikiUrl =
|
||||||
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Setup-&-Configuration-Guide";
|
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Setup-&-Configuration-Guide";
|
||||||
|
|
||||||
|
public const string DumpKeysWikiUrl =
|
||||||
|
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Dumping/Keys";
|
||||||
|
|
||||||
|
public const string DumpFirmwareWikiUrl =
|
||||||
|
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Dumping/Firmware";
|
||||||
|
|
||||||
|
public const string DumpContentWikiUrl =
|
||||||
|
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Dumping/Games,-Updates-&-DLC";
|
||||||
|
|
||||||
public const string MultiplayerWikiUrl =
|
public const string MultiplayerWikiUrl =
|
||||||
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Multiplayer-(LDN-Local-Wireless)-Guide";
|
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Multiplayer-(LDN-Local-Wireless)-Guide";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,5 +20,21 @@ namespace Ryujinx.Common.Utilities
|
|||||||
Debug.Assert(res != -1);
|
Debug.Assert(res != -1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "dumpable" attribute of the calling process
|
||||||
|
private const int PR_SET_DUMPABLE = 4;
|
||||||
|
|
||||||
|
[DllImport("libc", SetLastError = true)]
|
||||||
|
private static extern int prctl(int option, int arg2);
|
||||||
|
|
||||||
|
public static void SetCoreDumpable(bool dumpable)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
int dumpableInt = dumpable ? 1 : 0;
|
||||||
|
int result = prctl(PR_SET_DUMPABLE, dumpableInt);
|
||||||
|
Debug.Assert(result == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ namespace Ryujinx.Graphics.GAL
|
|||||||
void SetRasterizerDiscard(bool discard);
|
void SetRasterizerDiscard(bool discard);
|
||||||
|
|
||||||
void SetRenderTargetColorMasks(ReadOnlySpan<uint> componentMask);
|
void SetRenderTargetColorMasks(ReadOnlySpan<uint> componentMask);
|
||||||
void SetRenderTargets(ITexture[] colors, ITexture depthStencil);
|
void SetRenderTargets(Span<ITexture> colors, ITexture depthStencil);
|
||||||
|
|
||||||
void SetScissors(ReadOnlySpan<Rectangle<int>> regions);
|
void SetScissors(ReadOnlySpan<Rectangle<int>> regions);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Ryujinx.Graphics.GAL.Multithreading.Model;
|
using Ryujinx.Graphics.GAL.Multithreading.Model;
|
||||||
using Ryujinx.Graphics.GAL.Multithreading.Resources;
|
using Ryujinx.Graphics.GAL.Multithreading.Resources;
|
||||||
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
|
|
||||||
namespace Ryujinx.Graphics.GAL.Multithreading.Commands
|
namespace Ryujinx.Graphics.GAL.Multithreading.Commands
|
||||||
@@ -8,11 +9,13 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands
|
|||||||
{
|
{
|
||||||
public static readonly ArrayPool<ITexture> ArrayPool = ArrayPool<ITexture>.Create(512, 50);
|
public static readonly ArrayPool<ITexture> ArrayPool = ArrayPool<ITexture>.Create(512, 50);
|
||||||
public readonly CommandType CommandType => CommandType.SetRenderTargets;
|
public readonly CommandType CommandType => CommandType.SetRenderTargets;
|
||||||
|
private int _colorsCount;
|
||||||
private TableRef<ITexture[]> _colors;
|
private TableRef<ITexture[]> _colors;
|
||||||
private TableRef<ITexture> _depthStencil;
|
private TableRef<ITexture> _depthStencil;
|
||||||
|
|
||||||
public void Set(TableRef<ITexture[]> colors, TableRef<ITexture> depthStencil)
|
public void Set(int colorsCount, TableRef<ITexture[]> colors, TableRef<ITexture> depthStencil)
|
||||||
{
|
{
|
||||||
|
_colorsCount = colorsCount;
|
||||||
_colors = colors;
|
_colors = colors;
|
||||||
_depthStencil = depthStencil;
|
_depthStencil = depthStencil;
|
||||||
}
|
}
|
||||||
@@ -20,16 +23,15 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands
|
|||||||
public static void Run(ref SetRenderTargetsCommand command, ThreadedRenderer threaded, IRenderer renderer)
|
public static void Run(ref SetRenderTargetsCommand command, ThreadedRenderer threaded, IRenderer renderer)
|
||||||
{
|
{
|
||||||
ITexture[] colors = command._colors.Get(threaded);
|
ITexture[] colors = command._colors.Get(threaded);
|
||||||
ITexture[] colorsCopy = ArrayPool.Rent(colors.Length);
|
Span<ITexture> colorsSpan = colors.AsSpan(0, command._colorsCount);
|
||||||
|
|
||||||
for (int i = 0; i < colors.Length; i++)
|
for (int i = 0; i < colorsSpan.Length; i++)
|
||||||
{
|
{
|
||||||
colorsCopy[i] = ((ThreadedTexture)colors[i])?.Base;
|
colorsSpan[i] = ((ThreadedTexture)colorsSpan[i])?.Base;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.Pipeline.SetRenderTargets(colorsCopy, command._depthStencil.GetAs<ThreadedTexture>(threaded)?.Base);
|
renderer.Pipeline.SetRenderTargets(colorsSpan, command._depthStencil.GetAs<ThreadedTexture>(threaded)?.Base);
|
||||||
|
|
||||||
ArrayPool.Return(colorsCopy);
|
|
||||||
ArrayPool.Return(colors);
|
ArrayPool.Return(colors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -267,12 +267,12 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
|||||||
_renderer.QueueCommand();
|
_renderer.QueueCommand();
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe void SetRenderTargets(ITexture[] colors, ITexture depthStencil)
|
public unsafe void SetRenderTargets(Span<ITexture> colors, ITexture depthStencil)
|
||||||
{
|
{
|
||||||
ITexture[] colorsCopy = SetRenderTargetsCommand.ArrayPool.Rent(colors.Length);
|
ITexture[] colorsCopy = SetRenderTargetsCommand.ArrayPool.Rent(colors.Length);
|
||||||
colors.CopyTo(colorsCopy, 0);
|
colors.CopyTo(colorsCopy.AsSpan());
|
||||||
|
|
||||||
_renderer.New<SetRenderTargetsCommand>()->Set(Ref(colorsCopy), Ref(depthStencil));
|
_renderer.New<SetRenderTargetsCommand>()->Set(colors.Length, Ref(colorsCopy), Ref(depthStencil));
|
||||||
_renderer.QueueCommand();
|
_renderer.QueueCommand();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ThreadedRenderer : IRenderer
|
public class ThreadedRenderer : IRenderer
|
||||||
{
|
{
|
||||||
private const int SpanPoolBytes = 4 * 1024 * 1024;
|
private const int SpanPoolBytes = 8 * 1024 * 1024;
|
||||||
private const int MaxRefsPerCommand = 2;
|
private const int MaxRefsPerCommand = 2;
|
||||||
private const int QueueCount = 10000;
|
private const int QueueCount = 10000;
|
||||||
|
|
||||||
|
|||||||
@@ -404,9 +404,12 @@ namespace Ryujinx.Graphics.Gpu
|
|||||||
|
|
||||||
if (force || _pendingSync || (syncPoint && SyncpointActions.Count > 0))
|
if (force || _pendingSync || (syncPoint && SyncpointActions.Count > 0))
|
||||||
{
|
{
|
||||||
foreach (ISyncActionHandler action in SyncActions)
|
for (int i = 0; i < SyncActions.Count; i++)
|
||||||
{
|
{
|
||||||
action.SyncPreAction(syncPoint);
|
if (SyncActions[i].SyncPreAction(syncPoint))
|
||||||
|
{
|
||||||
|
SyncActions.RemoveAt(i--);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (ISyncActionHandler action in SyncpointActions)
|
foreach (ISyncActionHandler action in SyncpointActions)
|
||||||
|
|||||||
@@ -411,7 +411,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// flushes often enough, which is determined by the flush balance.
|
/// flushes often enough, which is determined by the flush balance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void SyncPreAction(bool syncpoint)
|
public bool SyncPreAction(bool syncpoint)
|
||||||
{
|
{
|
||||||
if (syncpoint || NextSyncCopies())
|
if (syncpoint || NextSyncCopies())
|
||||||
{
|
{
|
||||||
@@ -421,6 +421,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
_registeredBufferSync = _modifiedSync;
|
_registeredBufferSync = _modifiedSync;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Buffer, used to store vertex and index data, uniform and storage buffers, and others.
|
/// Buffer, used to store vertex and index data, uniform and storage buffers, and others.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class Buffer : INonOverlappingRange, ISyncActionHandler, IDisposable
|
class Buffer : INonOverlappingRange<Buffer>, ISyncActionHandler, IDisposable
|
||||||
{
|
{
|
||||||
private const ulong GranularBufferThreshold = 4096;
|
private const ulong GranularBufferThreshold = 4096;
|
||||||
|
|
||||||
@@ -41,6 +41,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
/// End address of the buffer in guest memory.
|
/// End address of the buffer in guest memory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ulong EndAddress => Address + Size;
|
public ulong EndAddress => Address + Size;
|
||||||
|
|
||||||
|
public Buffer Next { get; set; }
|
||||||
|
public Buffer Previous { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Increments when the buffer is (partially) unmapped or disposed.
|
/// Increments when the buffer is (partially) unmapped or disposed.
|
||||||
@@ -87,6 +90,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
|
|
||||||
private readonly bool _useGranular;
|
private readonly bool _useGranular;
|
||||||
private bool _syncActionRegistered;
|
private bool _syncActionRegistered;
|
||||||
|
private bool _bufferInherited;
|
||||||
|
|
||||||
private int _referenceCount = 1;
|
private int _referenceCount = 1;
|
||||||
|
|
||||||
@@ -113,7 +117,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
ulong size,
|
ulong size,
|
||||||
BufferStage stage,
|
BufferStage stage,
|
||||||
bool sparseCompatible,
|
bool sparseCompatible,
|
||||||
RangeItem<Buffer>[] baseBuffers)
|
Buffer[] baseBuffers)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_physicalMemory = physicalMemory;
|
_physicalMemory = physicalMemory;
|
||||||
@@ -134,15 +138,15 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
if (baseBuffers.Length != 0)
|
if (baseBuffers.Length != 0)
|
||||||
{
|
{
|
||||||
baseHandles = new List<IRegionHandle>();
|
baseHandles = new List<IRegionHandle>();
|
||||||
foreach (RangeItem<Buffer> item in baseBuffers)
|
foreach (Buffer item in baseBuffers)
|
||||||
{
|
{
|
||||||
if (item.Value._useGranular)
|
if (item._useGranular)
|
||||||
{
|
{
|
||||||
baseHandles.AddRange(item.Value._memoryTrackingGranular.Handles);
|
baseHandles.AddRange(item._memoryTrackingGranular.Handles);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
baseHandles.Add(item.Value._memoryTracking);
|
baseHandles.Add(item._memoryTracking);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,14 +251,14 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
/// Checks if a given range overlaps with the buffer.
|
/// Checks if a given range overlaps with the buffer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="address">Start address of the range</param>
|
/// <param name="address">Start address of the range</param>
|
||||||
/// <param name="size">Size in bytes of the range</param>
|
/// <param name="endAddress">End address of the range</param>
|
||||||
/// <returns>True if the range overlaps, false otherwise</returns>
|
/// <returns>True if the range overlaps, false otherwise</returns>
|
||||||
public bool OverlapsWith(ulong address, ulong size)
|
public bool OverlapsWith(ulong address, ulong endAddress)
|
||||||
{
|
{
|
||||||
return Address < address + size && address < EndAddress;
|
return Address < endAddress && address < EndAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
public INonOverlappingRange Split(ulong splitAddress)
|
public INonOverlappingRange<Buffer> Split(ulong splitAddress)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@@ -389,11 +393,16 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
/// This will copy any buffer ranges designated for pre-flushing.
|
/// This will copy any buffer ranges designated for pre-flushing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="syncpoint">True if the action is a guest syncpoint</param>
|
/// <param name="syncpoint">True if the action is a guest syncpoint</param>
|
||||||
public void SyncPreAction(bool syncpoint)
|
public bool SyncPreAction(bool syncpoint)
|
||||||
{
|
{
|
||||||
|
if (_bufferInherited)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (_referenceCount == 0)
|
if (_referenceCount == 0)
|
||||||
{
|
{
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BackingState.ShouldChangeBacking())
|
if (BackingState.ShouldChangeBacking())
|
||||||
@@ -410,6 +419,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
_modifiedRanges?.GetRangesAtSync(Address, Size, _context.SyncNumber, _syncPreRangeAction);
|
_modifiedRanges?.GetRangesAtSync(Address, Size, _context.SyncNumber, _syncPreRangeAction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SyncPreRangeAction(ulong address, ulong size)
|
void SyncPreRangeAction(ulong address, ulong size)
|
||||||
@@ -426,10 +437,13 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
{
|
{
|
||||||
_syncActionRegistered = false;
|
_syncActionRegistered = false;
|
||||||
|
|
||||||
|
if (_bufferInherited)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (_useGranular)
|
if (_useGranular)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
_modifiedRanges?.GetRanges(Address, Size, _syncRangeAction);
|
_modifiedRanges?.GetRanges(Address, Size, _syncRangeAction);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -453,6 +467,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
/// <param name="from">The buffer to inherit from</param>
|
/// <param name="from">The buffer to inherit from</param>
|
||||||
public void InheritModifiedRanges(Buffer from)
|
public void InheritModifiedRanges(Buffer from)
|
||||||
{
|
{
|
||||||
|
from._bufferInherited = true;
|
||||||
|
|
||||||
if (from._modifiedRanges is { HasRanges: true })
|
if (from._modifiedRanges is { HasRanges: true })
|
||||||
{
|
{
|
||||||
if (from._syncActionRegistered && !_syncActionRegistered)
|
if (from._syncActionRegistered && !_syncActionRegistered)
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
/// <param name="parent">Parent buffer</param>
|
/// <param name="parent">Parent buffer</param>
|
||||||
/// <param name="stage">Initial buffer stage</param>
|
/// <param name="stage">Initial buffer stage</param>
|
||||||
/// <param name="baseBuffers">Buffers to inherit state from</param>
|
/// <param name="baseBuffers">Buffers to inherit state from</param>
|
||||||
public BufferBackingState(GpuContext context, Buffer parent, BufferStage stage, RangeItem<Buffer>[] baseBuffers)
|
public BufferBackingState(GpuContext context, Buffer parent, BufferStage stage, Buffer[] baseBuffers)
|
||||||
{
|
{
|
||||||
_size = (int)parent.Size;
|
_size = (int)parent.Size;
|
||||||
_systemMemoryType = context.Capabilities.MemoryType;
|
_systemMemoryType = context.Capabilities.MemoryType;
|
||||||
@@ -102,9 +102,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
|
|
||||||
if (baseBuffers.Length != 0)
|
if (baseBuffers.Length != 0)
|
||||||
{
|
{
|
||||||
foreach (RangeItem<Buffer> item in baseBuffers)
|
foreach (Buffer item in baseBuffers)
|
||||||
{
|
{
|
||||||
CombineState(item.Value.BackingState);
|
CombineState(item.BackingState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,16 +79,13 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
for (int index = 0; index < range.Count; index++)
|
for (int index = 0; index < range.Count; index++)
|
||||||
{
|
{
|
||||||
MemoryRange subRange = range.GetSubRange(index);
|
MemoryRange subRange = range.GetSubRange(index);
|
||||||
|
|
||||||
_buffers.Lock.EnterReadLock();
|
ReadOnlySpan<Buffer> overlaps = _buffers.FindOverlapsAsSpan(subRange.Address, subRange.Size);
|
||||||
Span<RangeItem<Buffer>> overlaps = _buffers.FindOverlapsAsSpan(subRange.Address, subRange.Size);
|
|
||||||
|
|
||||||
for (int i = 0; i < overlaps.Length; i++)
|
for (int i = 0; i < overlaps.Length; i++)
|
||||||
{
|
{
|
||||||
overlaps[i].Value.Unmapped(subRange.Address, subRange.Size);
|
overlaps[i].Unmapped(subRange.Address, subRange.Size);
|
||||||
}
|
}
|
||||||
|
|
||||||
_buffers.Lock.ExitReadLock();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,7 +325,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
ulong alignedEndAddress = (endAddress + alignmentMask) & ~alignmentMask;
|
ulong alignedEndAddress = (endAddress + alignmentMask) & ~alignmentMask;
|
||||||
ulong alignedSize = alignedEndAddress - alignedAddress;
|
ulong alignedSize = alignedEndAddress - alignedAddress;
|
||||||
|
|
||||||
Buffer buffer = _buffers.FindOverlap(alignedAddress, alignedSize).Value;
|
Buffer buffer = _buffers.FindOverlap(alignedAddress, alignedSize);
|
||||||
BufferRange bufferRange = buffer.GetRange(alignedAddress, alignedSize, false);
|
BufferRange bufferRange = buffer.GetRange(alignedAddress, alignedSize, false);
|
||||||
|
|
||||||
alignedSubRanges[i] = new MemoryRange(alignedAddress, alignedSize);
|
alignedSubRanges[i] = new MemoryRange(alignedAddress, alignedSize);
|
||||||
@@ -395,7 +392,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
|
|
||||||
if (subRange.Address != MemoryManager.PteUnmapped)
|
if (subRange.Address != MemoryManager.PteUnmapped)
|
||||||
{
|
{
|
||||||
Buffer buffer = _buffers.FindOverlap(subRange.Address, subRange.Size).Value;
|
Buffer buffer = _buffers.FindOverlap(subRange.Address, subRange.Size);
|
||||||
|
|
||||||
virtualBuffer.AddPhysicalDependency(buffer, subRange.Address, dstOffset, subRange.Size);
|
virtualBuffer.AddPhysicalDependency(buffer, subRange.Address, dstOffset, subRange.Size);
|
||||||
physicalBuffers.Add(buffer);
|
physicalBuffers.Add(buffer);
|
||||||
@@ -487,10 +484,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
/// <param name="stage">The type of usage that created the buffer</param>
|
/// <param name="stage">The type of usage that created the buffer</param>
|
||||||
private void CreateBufferAligned(ulong address, ulong size, BufferStage stage)
|
private void CreateBufferAligned(ulong address, ulong size, BufferStage stage)
|
||||||
{
|
{
|
||||||
Buffer newBuffer = null;
|
ReadOnlySpan<Buffer> overlaps = _buffers.FindOverlapsAsSpan(address, size);
|
||||||
|
|
||||||
_buffers.Lock.EnterWriteLock();
|
|
||||||
Span<RangeItem<Buffer>> overlaps = _buffers.FindOverlapsAsSpan(address, size);
|
|
||||||
|
|
||||||
if (overlaps.Length != 0)
|
if (overlaps.Length != 0)
|
||||||
{
|
{
|
||||||
@@ -521,7 +515,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
{
|
{
|
||||||
// Try to grow the buffer by 1.5x of its current size.
|
// Try to grow the buffer by 1.5x of its current size.
|
||||||
// This improves performance in the cases where the buffer is resized often by small amounts.
|
// This improves performance in the cases where the buffer is resized often by small amounts.
|
||||||
ulong existingSize = overlaps[0].Value.Size;
|
ulong existingSize = overlaps[0].Size;
|
||||||
ulong growthSize = (existingSize + Math.Min(existingSize >> 1, MaxDynamicGrowthSize)) & ~BufferAlignmentMask;
|
ulong growthSize = (existingSize + Math.Min(existingSize >> 1, MaxDynamicGrowthSize)) & ~BufferAlignmentMask;
|
||||||
|
|
||||||
size = Math.Max(size, growthSize);
|
size = Math.Max(size, growthSize);
|
||||||
@@ -535,39 +529,22 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
|
|
||||||
for (int i = 0; i < overlaps.Length; i++)
|
for (int i = 0; i < overlaps.Length; i++)
|
||||||
{
|
{
|
||||||
anySparseCompatible |= overlaps[i].Value.SparseCompatible;
|
anySparseCompatible |= overlaps[i].SparseCompatible;
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeItem<Buffer>[] overlapsArray = overlaps.ToArray();
|
Buffer[] overlapsArray = overlaps.ToArray();
|
||||||
|
|
||||||
_buffers.RemoveRange(overlaps[0], overlaps[^1]);
|
_buffers.RemoveRange(overlaps[0], overlaps[^1]);
|
||||||
|
|
||||||
_buffers.Lock.ExitWriteLock();
|
|
||||||
|
|
||||||
ulong newSize = endAddress - address;
|
ulong newSize = endAddress - address;
|
||||||
|
|
||||||
newBuffer = CreateBufferAligned(address, newSize, stage, anySparseCompatible, overlapsArray);
|
_buffers.Add(CreateBufferAligned(address, newSize, stage, anySparseCompatible, overlapsArray));
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_buffers.Lock.ExitWriteLock();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_buffers.Lock.ExitWriteLock();
|
|
||||||
|
|
||||||
// No overlap, just create a new buffer.
|
// No overlap, just create a new buffer.
|
||||||
newBuffer = new(_context, _physicalMemory, address, size, stage, sparseCompatible: false, []);
|
_buffers.Add(new(_context, _physicalMemory, address, size, stage, sparseCompatible: false, []));
|
||||||
}
|
|
||||||
|
|
||||||
if (newBuffer is not null)
|
|
||||||
{
|
|
||||||
_buffers.Lock.EnterWriteLock();
|
|
||||||
|
|
||||||
_buffers.Add(newBuffer);
|
|
||||||
|
|
||||||
_buffers.Lock.ExitWriteLock();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,10 +560,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
private void CreateBufferAligned(ulong address, ulong size, BufferStage stage, ulong alignment)
|
private void CreateBufferAligned(ulong address, ulong size, BufferStage stage, ulong alignment)
|
||||||
{
|
{
|
||||||
bool sparseAligned = alignment >= SparseBufferAlignmentSize;
|
bool sparseAligned = alignment >= SparseBufferAlignmentSize;
|
||||||
Buffer newBuffer = null;
|
|
||||||
|
|
||||||
_buffers.Lock.EnterWriteLock();
|
ReadOnlySpan<Buffer> overlaps = _buffers.FindOverlapsAsSpan(address, size);
|
||||||
Span<RangeItem<Buffer>> overlaps = _buffers.FindOverlapsAsSpan(address, size);
|
|
||||||
|
|
||||||
if (overlaps.Length != 0)
|
if (overlaps.Length != 0)
|
||||||
{
|
{
|
||||||
@@ -598,7 +573,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
if (overlaps[0].Address > address ||
|
if (overlaps[0].Address > address ||
|
||||||
overlaps[0].EndAddress < endAddress ||
|
overlaps[0].EndAddress < endAddress ||
|
||||||
(overlaps[0].Address & (alignment - 1)) != 0 ||
|
(overlaps[0].Address & (alignment - 1)) != 0 ||
|
||||||
(!overlaps[0].Value.SparseCompatible && sparseAligned))
|
(!overlaps[0].SparseCompatible && sparseAligned))
|
||||||
{
|
{
|
||||||
// We need to make sure the new buffer is properly aligned.
|
// We need to make sure the new buffer is properly aligned.
|
||||||
// However, after the range is aligned, it is possible that it
|
// However, after the range is aligned, it is possible that it
|
||||||
@@ -622,35 +597,18 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
|
|
||||||
ulong newSize = endAddress - address;
|
ulong newSize = endAddress - address;
|
||||||
|
|
||||||
RangeItem<Buffer>[] overlapsArray = overlaps.ToArray();
|
Buffer[] overlapsArray = overlaps.ToArray();
|
||||||
|
|
||||||
_buffers.RemoveRange(overlaps[0], overlaps[^1]);
|
_buffers.RemoveRange(overlaps[0], overlaps[^1]);
|
||||||
|
|
||||||
_buffers.Lock.ExitWriteLock();
|
_buffers.Add(CreateBufferAligned(address, newSize, stage, sparseAligned, overlapsArray));
|
||||||
|
|
||||||
newBuffer = CreateBufferAligned(address, newSize, stage, sparseAligned, overlapsArray);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_buffers.Lock.ExitWriteLock();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_buffers.Lock.ExitWriteLock();
|
|
||||||
|
|
||||||
// No overlap, just create a new buffer.
|
// No overlap, just create a new buffer.
|
||||||
newBuffer = new(_context, _physicalMemory, address, size, stage, sparseAligned, []);
|
_buffers.Add(new(_context, _physicalMemory, address, size, stage, sparseAligned, []));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newBuffer is not null)
|
|
||||||
{
|
|
||||||
_buffers.Lock.EnterWriteLock();
|
|
||||||
|
|
||||||
_buffers.Add(newBuffer);
|
|
||||||
|
|
||||||
_buffers.Lock.ExitWriteLock();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -663,13 +621,13 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
/// <param name="stage">The type of usage that created the buffer</param>
|
/// <param name="stage">The type of usage that created the buffer</param>
|
||||||
/// <param name="sparseCompatible">Indicates if the buffer can be used in a sparse buffer mapping</param>
|
/// <param name="sparseCompatible">Indicates if the buffer can be used in a sparse buffer mapping</param>
|
||||||
/// <param name="overlaps">Buffers overlapping the range</param>
|
/// <param name="overlaps">Buffers overlapping the range</param>
|
||||||
private Buffer CreateBufferAligned(ulong address, ulong size, BufferStage stage, bool sparseCompatible, RangeItem<Buffer>[] overlaps)
|
private Buffer CreateBufferAligned(ulong address, ulong size, BufferStage stage, bool sparseCompatible, Buffer[] overlaps)
|
||||||
{
|
{
|
||||||
Buffer newBuffer = new(_context, _physicalMemory, address, size, stage, sparseCompatible, overlaps);
|
Buffer newBuffer = new(_context, _physicalMemory, address, size, stage, sparseCompatible, overlaps);
|
||||||
|
|
||||||
for (int index = 0; index < overlaps.Length; index++)
|
for (int index = 0; index < overlaps.Length; index++)
|
||||||
{
|
{
|
||||||
Buffer buffer = overlaps[index].Value;
|
Buffer buffer = overlaps[index];
|
||||||
|
|
||||||
int dstOffset = (int)(buffer.Address - newBuffer.Address);
|
int dstOffset = (int)(buffer.Address - newBuffer.Address);
|
||||||
|
|
||||||
@@ -897,7 +855,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
{
|
{
|
||||||
MemoryRange subRange = range.GetSubRange(i);
|
MemoryRange subRange = range.GetSubRange(i);
|
||||||
|
|
||||||
Buffer subBuffer = _buffers.FindOverlap(subRange.Address, subRange.Size).Value;
|
Buffer subBuffer = _buffers.FindOverlap(subRange.Address, subRange.Size);
|
||||||
|
|
||||||
subBuffer.SynchronizeMemory(subRange.Address, subRange.Size);
|
subBuffer.SynchronizeMemory(subRange.Address, subRange.Size);
|
||||||
|
|
||||||
@@ -945,7 +903,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
|
|
||||||
if (size != 0)
|
if (size != 0)
|
||||||
{
|
{
|
||||||
buffer = _buffers.FindOverlap(address, size).Value;
|
buffer = _buffers.FindOverlap(address, size);
|
||||||
|
|
||||||
buffer.CopyFromDependantVirtualBuffers();
|
buffer.CopyFromDependantVirtualBuffers();
|
||||||
buffer.SynchronizeMemory(address, size);
|
buffer.SynchronizeMemory(address, size);
|
||||||
@@ -957,7 +915,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
buffer = _buffers.FindOverlapFast(address, 1).Value;
|
buffer = _buffers.FindOverlapFast(address, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return buffer;
|
return buffer;
|
||||||
@@ -995,7 +953,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
{
|
{
|
||||||
if (size != 0)
|
if (size != 0)
|
||||||
{
|
{
|
||||||
Buffer buffer = _buffers.FindOverlap(address, size).Value;
|
Buffer buffer = _buffers.FindOverlap(address, size);
|
||||||
|
|
||||||
if (copyBackVirtual)
|
if (copyBackVirtual)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A range within a buffer that has been modified by the GPU.
|
/// A range within a buffer that has been modified by the GPU.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class BufferModifiedRange : INonOverlappingRange
|
class BufferModifiedRange : INonOverlappingRange<BufferModifiedRange>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Start address of the range in guest memory.
|
/// Start address of the range in guest memory.
|
||||||
@@ -24,6 +24,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
/// End address of the range in guest memory.
|
/// End address of the range in guest memory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ulong EndAddress => Address + Size;
|
public ulong EndAddress => Address + Size;
|
||||||
|
|
||||||
|
public BufferModifiedRange Next { get; set; }
|
||||||
|
public BufferModifiedRange Previous { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The GPU sync number at the time of the last modification.
|
/// The GPU sync number at the time of the last modification.
|
||||||
@@ -54,14 +57,14 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
/// Checks if a given range overlaps with the modified range.
|
/// Checks if a given range overlaps with the modified range.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="address">Start address of the range</param>
|
/// <param name="address">Start address of the range</param>
|
||||||
/// <param name="size">Size in bytes of the range</param>
|
/// <param name="endAddress">End address of the range</param>
|
||||||
/// <returns>True if the range overlaps, false otherwise</returns>
|
/// <returns>True if the range overlaps, false otherwise</returns>
|
||||||
public bool OverlapsWith(ulong address, ulong size)
|
public bool OverlapsWith(ulong address, ulong endAddress)
|
||||||
{
|
{
|
||||||
return Address < address + size && address < EndAddress;
|
return Address < endAddress && address < EndAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
public INonOverlappingRange Split(ulong splitAddress)
|
public INonOverlappingRange<BufferModifiedRange> Split(ulong splitAddress)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@@ -119,11 +122,11 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
// Slices a given region using the modified regions in the list. Calls the action for the new slices.
|
// Slices a given region using the modified regions in the list. Calls the action for the new slices.
|
||||||
Lock.EnterReadLock();
|
Lock.EnterReadLock();
|
||||||
|
|
||||||
Span<RangeItem<BufferModifiedRange>> overlaps = FindOverlapsAsSpan(address, size);
|
ReadOnlySpan<BufferModifiedRange> overlaps = FindOverlapsAsSpan(address, size);
|
||||||
|
|
||||||
for (int i = 0; i < overlaps.Length; i++)
|
for (int i = 0; i < overlaps.Length; i++)
|
||||||
{
|
{
|
||||||
BufferModifiedRange overlap = overlaps[i].Value;
|
BufferModifiedRange overlap = overlaps[i];
|
||||||
|
|
||||||
if (overlap.Address > address)
|
if (overlap.Address > address)
|
||||||
{
|
{
|
||||||
@@ -157,7 +160,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
ulong syncNumber = _context.SyncNumber;
|
ulong syncNumber = _context.SyncNumber;
|
||||||
// We may overlap with some existing modified regions. They must be cut into by the new entry.
|
// We may overlap with some existing modified regions. They must be cut into by the new entry.
|
||||||
Lock.EnterWriteLock();
|
Lock.EnterWriteLock();
|
||||||
(RangeItem<BufferModifiedRange> first, RangeItem<BufferModifiedRange> last) = FindOverlapsAsNodes(address, size);
|
(BufferModifiedRange first, BufferModifiedRange last) = FindOverlapsAsNodes(address, size);
|
||||||
|
|
||||||
if (first is null)
|
if (first is null)
|
||||||
{
|
{
|
||||||
@@ -170,34 +173,39 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
{
|
{
|
||||||
if (first.Address == address && first.EndAddress == endAddress)
|
if (first.Address == address && first.EndAddress == endAddress)
|
||||||
{
|
{
|
||||||
first.Value.SyncNumber = syncNumber;
|
first.SyncNumber = syncNumber;
|
||||||
first.Value.Parent = this;
|
first.Parent = this;
|
||||||
Lock.ExitWriteLock();
|
Lock.ExitWriteLock();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (first.Address < address)
|
if (first.Address < address)
|
||||||
{
|
{
|
||||||
first.Value.Size = address - first.Address;
|
|
||||||
Update(first);
|
|
||||||
|
|
||||||
if (first.EndAddress > endAddress)
|
if (first.EndAddress > endAddress)
|
||||||
{
|
{
|
||||||
Add(new BufferModifiedRange(endAddress, first.EndAddress - endAddress,
|
Add(new BufferModifiedRange(endAddress, first.EndAddress - endAddress,
|
||||||
first.Value.SyncNumber, first.Value.Parent));
|
first.SyncNumber, first.Parent));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
first.Size = address - first.Address;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (first.EndAddress > endAddress)
|
if (first.EndAddress > endAddress)
|
||||||
{
|
{
|
||||||
first.Value.Size = first.EndAddress - endAddress;
|
first.Size = first.EndAddress - endAddress;
|
||||||
first.Value.Address = endAddress;
|
first.Address = endAddress;
|
||||||
Update(first);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Remove(first.Value);
|
first.Address = address;
|
||||||
|
first.Size = size;
|
||||||
|
first.SyncNumber = syncNumber;
|
||||||
|
first.Parent = this;
|
||||||
|
|
||||||
|
Lock.ExitWriteLock();
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,38 +215,39 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BufferModifiedRange buffPre = null;
|
|
||||||
BufferModifiedRange buffPost = null;
|
|
||||||
bool extendsPost = false;
|
|
||||||
bool extendsPre = false;
|
|
||||||
|
|
||||||
if (first.Address < address)
|
if (first.Address < address)
|
||||||
{
|
{
|
||||||
buffPre = new BufferModifiedRange(first.Address, address - first.Address,
|
first.Size = address - first.Address;
|
||||||
first.Value.SyncNumber, first.Value.Parent);
|
first = first.Next;
|
||||||
extendsPre = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (last.EndAddress > endAddress)
|
if (last.EndAddress > endAddress)
|
||||||
{
|
{
|
||||||
buffPost = new BufferModifiedRange(endAddress, last.EndAddress - endAddress,
|
last.Size = last.EndAddress - endAddress;
|
||||||
last.Value.SyncNumber, last.Value.Parent);
|
last.Address = endAddress;
|
||||||
extendsPost = true;
|
last = last.Previous;
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoveRange(first, last);
|
if (first.Address < last.Address)
|
||||||
|
|
||||||
if (extendsPre)
|
|
||||||
{
|
{
|
||||||
Add(buffPre);
|
RemoveRange(first.Next, last);
|
||||||
|
first.Address = address;
|
||||||
|
first.Size = size;
|
||||||
|
first.SyncNumber = syncNumber;
|
||||||
|
first.Parent = this;
|
||||||
}
|
}
|
||||||
|
else if (first.Address == last.Address)
|
||||||
if (extendsPost)
|
|
||||||
{
|
{
|
||||||
Add(buffPost);
|
first.Address = address;
|
||||||
|
first.Size = size;
|
||||||
|
first.SyncNumber = syncNumber;
|
||||||
|
first.Parent = this;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
Add(new BufferModifiedRange(address, size, syncNumber, this));
|
{
|
||||||
|
Add(new BufferModifiedRange(address, size, syncNumber, this));
|
||||||
|
}
|
||||||
|
|
||||||
Lock.ExitWriteLock();
|
Lock.ExitWriteLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,11 +261,11 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
public void GetRangesAtSync(ulong address, ulong size, ulong syncNumber, Action<ulong, ulong> rangeAction)
|
public void GetRangesAtSync(ulong address, ulong size, ulong syncNumber, Action<ulong, ulong> rangeAction)
|
||||||
{
|
{
|
||||||
Lock.EnterReadLock();
|
Lock.EnterReadLock();
|
||||||
Span<RangeItem<BufferModifiedRange>> overlaps = FindOverlapsAsSpan(address, size);
|
ReadOnlySpan<BufferModifiedRange> overlaps = FindOverlapsAsSpan(address, size);
|
||||||
|
|
||||||
for (int i = 0; i < overlaps.Length; i++)
|
for (int i = 0; i < overlaps.Length; i++)
|
||||||
{
|
{
|
||||||
BufferModifiedRange overlap = overlaps[i].Value;
|
BufferModifiedRange overlap = overlaps[i];
|
||||||
|
|
||||||
if (overlap.SyncNumber == syncNumber)
|
if (overlap.SyncNumber == syncNumber)
|
||||||
{
|
{
|
||||||
@@ -277,18 +286,18 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
{
|
{
|
||||||
// We use the non-span method here because keeping the lock will cause a deadlock.
|
// We use the non-span method here because keeping the lock will cause a deadlock.
|
||||||
Lock.EnterReadLock();
|
Lock.EnterReadLock();
|
||||||
RangeItem<BufferModifiedRange>[] overlaps = FindOverlapsAsArray(address, size, out int length);
|
BufferModifiedRange[] overlaps = FindOverlapsAsArray(address, size, out int length);
|
||||||
Lock.ExitReadLock();
|
Lock.ExitReadLock();
|
||||||
|
|
||||||
if (length != 0)
|
if (length != 0)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < length; i++)
|
for (int i = 0; i < length; i++)
|
||||||
{
|
{
|
||||||
BufferModifiedRange overlap = overlaps[i].Value;
|
BufferModifiedRange overlap = overlaps[i];
|
||||||
rangeAction(overlap.Address, overlap.Size);
|
rangeAction(overlap.Address, overlap.Size);
|
||||||
}
|
}
|
||||||
|
|
||||||
ArrayPool<RangeItem<BufferModifiedRange>>.Shared.Return(overlaps);
|
ArrayPool<BufferModifiedRange>.Shared.Return(overlaps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +310,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
public bool HasRange(ulong address, ulong size)
|
public bool HasRange(ulong address, ulong size)
|
||||||
{
|
{
|
||||||
Lock.EnterReadLock();
|
Lock.EnterReadLock();
|
||||||
RangeItem<BufferModifiedRange> first = FindOverlapFast(address, size);
|
BufferModifiedRange first = FindOverlapFast(address, size);
|
||||||
bool result = first is not null;
|
bool result = first is not null;
|
||||||
Lock.ExitReadLock();
|
Lock.ExitReadLock();
|
||||||
return result;
|
return result;
|
||||||
@@ -336,7 +345,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
/// <param name="address">The start address of the flush range</param>
|
/// <param name="address">The start address of the flush range</param>
|
||||||
/// <param name="endAddress">The end address of the flush range</param>
|
/// <param name="endAddress">The end address of the flush range</param>
|
||||||
private void RemoveRangesAndFlush(
|
private void RemoveRangesAndFlush(
|
||||||
RangeItem<BufferModifiedRange>[] overlaps,
|
BufferModifiedRange[] overlaps,
|
||||||
int rangeCount,
|
int rangeCount,
|
||||||
long highestDiff,
|
long highestDiff,
|
||||||
ulong currentSync,
|
ulong currentSync,
|
||||||
@@ -349,7 +358,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
|
|
||||||
for (int i = 0; i < rangeCount; i++)
|
for (int i = 0; i < rangeCount; i++)
|
||||||
{
|
{
|
||||||
BufferModifiedRange overlap = overlaps[i].Value;
|
BufferModifiedRange overlap = overlaps[i];
|
||||||
|
|
||||||
long diff = (long)(overlap.SyncNumber - currentSync);
|
long diff = (long)(overlap.SyncNumber - currentSync);
|
||||||
|
|
||||||
@@ -358,7 +367,14 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
ulong clampAddress = Math.Max(address, overlap.Address);
|
ulong clampAddress = Math.Max(address, overlap.Address);
|
||||||
ulong clampEnd = Math.Min(endAddress, overlap.EndAddress);
|
ulong clampEnd = Math.Min(endAddress, overlap.EndAddress);
|
||||||
|
|
||||||
ClearPart(overlap, clampAddress, clampEnd);
|
if (i == 0 || i == rangeCount - 1)
|
||||||
|
{
|
||||||
|
ClearPart(overlap, clampAddress, clampEnd);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Remove(overlap);
|
||||||
|
}
|
||||||
|
|
||||||
RangeActionWithMigration(clampAddress, clampEnd - clampAddress, waitSync, _flushAction);
|
RangeActionWithMigration(clampAddress, clampEnd - clampAddress, waitSync, _flushAction);
|
||||||
}
|
}
|
||||||
@@ -398,7 +414,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
|
|
||||||
Lock.EnterWriteLock();
|
Lock.EnterWriteLock();
|
||||||
// We use the non-span method here because the array is partially modified by the code, which would invalidate a span.
|
// We use the non-span method here because the array is partially modified by the code, which would invalidate a span.
|
||||||
RangeItem<BufferModifiedRange>[] overlaps = FindOverlapsAsArray(address, size, out int rangeCount);
|
BufferModifiedRange[] overlaps = FindOverlapsAsArray(address, size, out int rangeCount);
|
||||||
|
|
||||||
if (rangeCount == 0)
|
if (rangeCount == 0)
|
||||||
{
|
{
|
||||||
@@ -414,7 +430,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
|
|
||||||
for (int i = 0; i < rangeCount; i++)
|
for (int i = 0; i < rangeCount; i++)
|
||||||
{
|
{
|
||||||
BufferModifiedRange overlap = overlaps![i].Value;
|
BufferModifiedRange overlap = overlaps![i];
|
||||||
|
|
||||||
long diff = (long)(overlap.SyncNumber - currentSync);
|
long diff = (long)(overlap.SyncNumber - currentSync);
|
||||||
|
|
||||||
@@ -436,7 +452,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
|
|
||||||
RemoveRangesAndFlush(overlaps, rangeCount, highestDiff, currentSync, address, endAddress);
|
RemoveRangesAndFlush(overlaps, rangeCount, highestDiff, currentSync, address, endAddress);
|
||||||
|
|
||||||
ArrayPool<RangeItem<BufferModifiedRange>>.Shared.Return(overlaps!);
|
ArrayPool<BufferModifiedRange>.Shared.Return(overlaps!);
|
||||||
|
|
||||||
Lock.ExitWriteLock();
|
Lock.ExitWriteLock();
|
||||||
}
|
}
|
||||||
@@ -452,7 +468,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
public void InheritRanges(BufferModifiedRangeList ranges, Action<ulong, ulong> registerRangeAction)
|
public void InheritRanges(BufferModifiedRangeList ranges, Action<ulong, ulong> registerRangeAction)
|
||||||
{
|
{
|
||||||
ranges.Lock.EnterReadLock();
|
ranges.Lock.EnterReadLock();
|
||||||
BufferModifiedRange[] inheritRanges = ranges.ToArray();
|
int rangesCount = ranges.Count;
|
||||||
|
BufferModifiedRange[] inheritRanges = ArrayPool<BufferModifiedRange>.Shared.Rent(ranges.Count);
|
||||||
|
ranges.Items.AsSpan(0, ranges.Count).CopyTo(inheritRanges);
|
||||||
ranges.Lock.ExitReadLock();
|
ranges.Lock.ExitReadLock();
|
||||||
|
|
||||||
// Copy over the migration from the previous range list
|
// Copy over the migration from the previous range list
|
||||||
@@ -478,22 +496,26 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
ranges._migrationTarget = this;
|
ranges._migrationTarget = this;
|
||||||
|
|
||||||
Lock.EnterWriteLock();
|
Lock.EnterWriteLock();
|
||||||
|
|
||||||
foreach (BufferModifiedRange range in inheritRanges)
|
for (int i = 0; i < rangesCount; i++)
|
||||||
{
|
{
|
||||||
|
BufferModifiedRange range = inheritRanges[i];
|
||||||
Add(range);
|
Add(range);
|
||||||
}
|
}
|
||||||
|
|
||||||
Lock.ExitWriteLock();
|
Lock.ExitWriteLock();
|
||||||
|
|
||||||
ulong currentSync = _context.SyncNumber;
|
ulong currentSync = _context.SyncNumber;
|
||||||
foreach (BufferModifiedRange range in inheritRanges)
|
for (int i = 0; i < rangesCount; i++)
|
||||||
{
|
{
|
||||||
|
BufferModifiedRange range = inheritRanges[i];
|
||||||
if (range.SyncNumber != currentSync)
|
if (range.SyncNumber != currentSync)
|
||||||
{
|
{
|
||||||
registerRangeAction(range.Address, range.Size);
|
registerRangeAction(range.Address, range.Size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ArrayPool<BufferModifiedRange>.Shared.Return(inheritRanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -534,18 +556,25 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
|
|
||||||
private void ClearPart(BufferModifiedRange overlap, ulong address, ulong endAddress)
|
private void ClearPart(BufferModifiedRange overlap, ulong address, ulong endAddress)
|
||||||
{
|
{
|
||||||
Remove(overlap);
|
|
||||||
|
|
||||||
// If the overlap extends outside of the clear range, make sure those parts still exist.
|
// If the overlap extends outside of the clear range, make sure those parts still exist.
|
||||||
|
|
||||||
if (overlap.Address < address)
|
if (overlap.Address < address)
|
||||||
{
|
{
|
||||||
Add(new BufferModifiedRange(overlap.Address, address - overlap.Address, overlap.SyncNumber, overlap.Parent));
|
if (overlap.EndAddress > endAddress)
|
||||||
|
{
|
||||||
|
Add(new BufferModifiedRange(endAddress, overlap.EndAddress - endAddress, overlap.SyncNumber, overlap.Parent));
|
||||||
|
}
|
||||||
|
|
||||||
|
overlap.Size = address - overlap.Address;
|
||||||
}
|
}
|
||||||
|
else if (overlap.EndAddress > endAddress)
|
||||||
if (overlap.EndAddress > endAddress)
|
|
||||||
{
|
{
|
||||||
Add(new BufferModifiedRange(endAddress, overlap.EndAddress - endAddress, overlap.SyncNumber, overlap.Parent));
|
overlap.Size = overlap.EndAddress - endAddress;
|
||||||
|
overlap.Address = endAddress;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Remove(overlap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,7 +587,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
{
|
{
|
||||||
ulong endAddress = address + size;
|
ulong endAddress = address + size;
|
||||||
Lock.EnterWriteLock();
|
Lock.EnterWriteLock();
|
||||||
(RangeItem<BufferModifiedRange> first, RangeItem<BufferModifiedRange> last) = FindOverlapsAsNodes(address, size);
|
(BufferModifiedRange first, BufferModifiedRange last) = FindOverlapsAsNodes(address, size);
|
||||||
|
|
||||||
if (first is null)
|
if (first is null)
|
||||||
{
|
{
|
||||||
@@ -570,26 +599,24 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
{
|
{
|
||||||
if (first.Address < address)
|
if (first.Address < address)
|
||||||
{
|
{
|
||||||
first.Value.Size = address - first.Address;
|
first.Size = address - first.Address;
|
||||||
Update(first);
|
|
||||||
|
|
||||||
if (first.EndAddress > endAddress)
|
if (first.EndAddress > endAddress)
|
||||||
{
|
{
|
||||||
Add(new BufferModifiedRange(endAddress, first.EndAddress - endAddress,
|
Add(new BufferModifiedRange(endAddress, first.EndAddress - endAddress,
|
||||||
first.Value.SyncNumber, first.Value.Parent));
|
first.SyncNumber, first.Parent));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (first.EndAddress > endAddress)
|
if (first.EndAddress > endAddress)
|
||||||
{
|
{
|
||||||
first.Value.Size = first.EndAddress - endAddress;
|
first.Size = first.EndAddress - endAddress;
|
||||||
first.Value.Address = endAddress;
|
first.Address = endAddress;
|
||||||
Update(first);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Remove(first.Value);
|
Remove(first);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,14 +632,14 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
if (first.Address < address)
|
if (first.Address < address)
|
||||||
{
|
{
|
||||||
buffPre = new BufferModifiedRange(first.Address, address - first.Address,
|
buffPre = new BufferModifiedRange(first.Address, address - first.Address,
|
||||||
first.Value.SyncNumber, first.Value.Parent);
|
first.SyncNumber, first.Parent);
|
||||||
extendsPre = true;
|
extendsPre = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (last.EndAddress > endAddress)
|
if (last.EndAddress > endAddress)
|
||||||
{
|
{
|
||||||
buffPost = new BufferModifiedRange(endAddress, last.EndAddress - endAddress,
|
buffPost = new BufferModifiedRange(endAddress, last.EndAddress - endAddress,
|
||||||
last.Value.SyncNumber, last.Value.Parent);
|
last.SyncNumber, last.Parent);
|
||||||
extendsPost = true;
|
extendsPost = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a GPU virtual memory range.
|
/// Represents a GPU virtual memory range.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private class VirtualRange : INonOverlappingRange
|
private class VirtualRange : INonOverlappingRange<VirtualRange>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// GPU virtual address where the range starts.
|
/// GPU virtual address where the range starts.
|
||||||
@@ -32,6 +32,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ulong EndAddress => Address + Size;
|
public ulong EndAddress => Address + Size;
|
||||||
|
|
||||||
|
public VirtualRange Next { get; set; }
|
||||||
|
public VirtualRange Previous { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Physical regions where the GPU virtual region is mapped.
|
/// Physical regions where the GPU virtual region is mapped.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -54,14 +57,14 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
/// Checks if a given range overlaps with the buffer.
|
/// Checks if a given range overlaps with the buffer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="address">Start address of the range</param>
|
/// <param name="address">Start address of the range</param>
|
||||||
/// <param name="size">Size in bytes of the range</param>
|
/// <param name="endAddress">End address of the range</param>
|
||||||
/// <returns>True if the range overlaps, false otherwise</returns>
|
/// <returns>True if the range overlaps, false otherwise</returns>
|
||||||
public bool OverlapsWith(ulong address, ulong size)
|
public bool OverlapsWith(ulong address, ulong endAddress)
|
||||||
{
|
{
|
||||||
return Address < address + size && address < EndAddress;
|
return Address < endAddress && address < EndAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
public INonOverlappingRange Split(ulong splitAddress)
|
public INonOverlappingRange<VirtualRange> Split(ulong splitAddress)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@@ -122,7 +125,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
ulong originalVa = gpuVa;
|
ulong originalVa = gpuVa;
|
||||||
|
|
||||||
_virtualRanges.Lock.EnterWriteLock();
|
_virtualRanges.Lock.EnterWriteLock();
|
||||||
(RangeItem<VirtualRange> first, RangeItem<VirtualRange> last) = _virtualRanges.FindOverlapsAsNodes(gpuVa, size);
|
(VirtualRange first, VirtualRange last) = _virtualRanges.FindOverlapsAsNodes(gpuVa, size);
|
||||||
|
|
||||||
if (first is not null)
|
if (first is not null)
|
||||||
{
|
{
|
||||||
@@ -147,8 +150,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
found = first.Value.Range.Count == 1 || IsSparseAligned(first.Value.Range);
|
found = first.Range.Count == 1 || IsSparseAligned(first.Range);
|
||||||
range = first.Value.Range.Slice(gpuVa - first.Address, size);
|
range = first.Range.Slice(gpuVa - first.Address, size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -17,6 +17,6 @@ namespace Ryujinx.Graphics.Gpu.Synchronization
|
|||||||
/// Action to be performed immediately before sync is created.
|
/// Action to be performed immediately before sync is created.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="syncpoint">True if the action is a guest syncpoint</param>
|
/// <param name="syncpoint">True if the action is a guest syncpoint</param>
|
||||||
void SyncPreAction(bool syncpoint) { }
|
bool SyncPreAction(bool syncpoint) { return true; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1166,7 +1166,7 @@ namespace Ryujinx.Graphics.OpenGL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetRenderTargets(ITexture[] colors, ITexture depthStencil)
|
public void SetRenderTargets(Span<ITexture> colors, ITexture depthStencil)
|
||||||
{
|
{
|
||||||
EnsureFramebuffer();
|
EnsureFramebuffer();
|
||||||
|
|
||||||
|
|||||||
@@ -71,17 +71,31 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
HasDepthStencil = isDepthStencil;
|
HasDepthStencil = isDepthStencil;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FramebufferParams(Device device, ITexture[] colors, ITexture depthStencil)
|
public FramebufferParams(Device device, ReadOnlySpan<ITexture> colors, ITexture depthStencil)
|
||||||
{
|
{
|
||||||
_device = device;
|
_device = device;
|
||||||
|
|
||||||
int colorsCount = colors.Count(IsValidTextureView);
|
int colorsCount = 0;
|
||||||
|
_colorsCanonical = new TextureView[colors.Length];
|
||||||
|
|
||||||
|
for (int i = 0; i < colors.Length; i++)
|
||||||
|
{
|
||||||
|
ITexture color = colors[i];
|
||||||
|
if (color is TextureView { Valid: true } view)
|
||||||
|
{
|
||||||
|
colorsCount++;
|
||||||
|
_colorsCanonical[i] = view;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_colorsCanonical[i] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int count = colorsCount + (IsValidTextureView(depthStencil) ? 1 : 0);
|
int count = colorsCount + (IsValidTextureView(depthStencil) ? 1 : 0);
|
||||||
|
|
||||||
_attachments = new Auto<DisposableImageView>[count];
|
_attachments = new Auto<DisposableImageView>[count];
|
||||||
_colors = new TextureView[colorsCount];
|
_colors = new TextureView[colorsCount];
|
||||||
_colorsCanonical = colors.Select(color => color is TextureView view && view.Valid ? view : null).ToArray();
|
|
||||||
|
|
||||||
AttachmentSamples = new uint[count];
|
AttachmentSamples = new uint[count];
|
||||||
AttachmentFormats = new VkFormat[count];
|
AttachmentFormats = new VkFormat[count];
|
||||||
@@ -165,9 +179,17 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
_totalCount = colors.Length;
|
_totalCount = colors.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FramebufferParams Update(ITexture[] colors, ITexture depthStencil)
|
public FramebufferParams Update(ReadOnlySpan<ITexture> colors, ITexture depthStencil)
|
||||||
{
|
{
|
||||||
int colorsCount = colors.Count(IsValidTextureView);
|
int colorsCount = 0;
|
||||||
|
|
||||||
|
foreach (ITexture color in colors)
|
||||||
|
{
|
||||||
|
if (IsValidTextureView(color))
|
||||||
|
{
|
||||||
|
colorsCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int count = colorsCount + (IsValidTextureView(depthStencil) ? 1 : 0);
|
int count = colorsCount + (IsValidTextureView(depthStencil) ? 1 : 0);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
using Ryujinx.Common;
|
||||||
using Ryujinx.Common.Memory;
|
using Ryujinx.Common.Memory;
|
||||||
using Silk.NET.Vulkan;
|
using Silk.NET.Vulkan;
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
|
||||||
|
|
||||||
namespace Ryujinx.Graphics.Vulkan
|
namespace Ryujinx.Graphics.Vulkan
|
||||||
{
|
{
|
||||||
@@ -10,6 +10,8 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
class MultiFenceHolder
|
class MultiFenceHolder
|
||||||
{
|
{
|
||||||
|
public static readonly ObjectPool<FenceHolder[]> FencePool = new(() => new FenceHolder[CommandBufferPool.MaxCommandBuffers]);
|
||||||
|
|
||||||
private const int BufferUsageTrackingGranularity = 4096;
|
private const int BufferUsageTrackingGranularity = 4096;
|
||||||
|
|
||||||
public FenceHolder[] Fences { get; }
|
public FenceHolder[] Fences { get; }
|
||||||
@@ -20,7 +22,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public MultiFenceHolder()
|
public MultiFenceHolder()
|
||||||
{
|
{
|
||||||
Fences = ArrayPool<FenceHolder>.Shared.Rent(CommandBufferPool.MaxCommandBuffers);
|
Fences = FencePool.Allocate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -29,7 +31,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
/// <param name="size">Size of the buffer</param>
|
/// <param name="size">Size of the buffer</param>
|
||||||
public MultiFenceHolder(int size)
|
public MultiFenceHolder(int size)
|
||||||
{
|
{
|
||||||
Fences = ArrayPool<FenceHolder>.Shared.Rent(CommandBufferPool.MaxCommandBuffers);
|
Fences = FencePool.Allocate();
|
||||||
_bufferUsageBitmap = new BufferUsageBitmap(size, BufferUsageTrackingGranularity);
|
_bufferUsageBitmap = new BufferUsageBitmap(size, BufferUsageTrackingGranularity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1035,7 +1035,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetRenderTargetsInternal(ITexture[] colors, ITexture depthStencil, bool filterWriteMasked)
|
private void SetRenderTargetsInternal(Span<ITexture> colors, ITexture depthStencil, bool filterWriteMasked)
|
||||||
{
|
{
|
||||||
CreateFramebuffer(colors, depthStencil, filterWriteMasked);
|
CreateFramebuffer(colors, depthStencil, filterWriteMasked);
|
||||||
CreateRenderPass();
|
CreateRenderPass();
|
||||||
@@ -1043,7 +1043,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
SignalAttachmentChange();
|
SignalAttachmentChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetRenderTargets(ITexture[] colors, ITexture depthStencil)
|
public void SetRenderTargets(Span<ITexture> colors, ITexture depthStencil)
|
||||||
{
|
{
|
||||||
_framebufferUsingColorWriteMask = false;
|
_framebufferUsingColorWriteMask = false;
|
||||||
SetRenderTargetsInternal(colors, depthStencil, Gd.IsTBDR);
|
SetRenderTargetsInternal(colors, depthStencil, Gd.IsTBDR);
|
||||||
@@ -1389,7 +1389,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
_currentPipelineHandle = 0;
|
_currentPipelineHandle = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreateFramebuffer(ITexture[] colors, ITexture depthStencil, bool filterWriteMasked)
|
private void CreateFramebuffer(Span<ITexture> colors, ITexture depthStencil, bool filterWriteMasked)
|
||||||
{
|
{
|
||||||
if (filterWriteMasked)
|
if (filterWriteMasked)
|
||||||
{
|
{
|
||||||
@@ -1399,7 +1399,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
// Just try to remove duplicate attachments.
|
// Just try to remove duplicate attachments.
|
||||||
// Save a copy of the array to rebind when mask changes.
|
// Save a copy of the array to rebind when mask changes.
|
||||||
|
|
||||||
void MaskOut()
|
void MaskOut(ReadOnlySpan<ITexture> colors)
|
||||||
{
|
{
|
||||||
if (!_framebufferUsingColorWriteMask)
|
if (!_framebufferUsingColorWriteMask)
|
||||||
{
|
{
|
||||||
@@ -1436,12 +1436,12 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
if (vkBlend.ColorWriteMask == 0)
|
if (vkBlend.ColorWriteMask == 0)
|
||||||
{
|
{
|
||||||
colors[i] = null;
|
colors[i] = null;
|
||||||
MaskOut();
|
MaskOut(colors);
|
||||||
}
|
}
|
||||||
else if (vkBlend2.ColorWriteMask == 0)
|
else if (vkBlend2.ColorWriteMask == 0)
|
||||||
{
|
{
|
||||||
colors[j] = null;
|
colors[j] = null;
|
||||||
MaskOut();
|
MaskOut(colors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Silk.NET.Vulkan;
|
using Silk.NET.Vulkan;
|
||||||
using System.Buffers;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -193,7 +193,8 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
{
|
{
|
||||||
_firstHandle = first.ID + 1;
|
_firstHandle = first.ID + 1;
|
||||||
_handles.RemoveAt(0);
|
_handles.RemoveAt(0);
|
||||||
ArrayPool<FenceHolder>.Shared.Return(first.Waitable.Fences);
|
Array.Clear(first.Waitable.Fences);
|
||||||
|
MultiFenceHolder.FencePool.Release(first.Waitable.Fences);
|
||||||
first.Waitable = null;
|
first.Waitable = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ using System;
|
|||||||
|
|
||||||
namespace Ryujinx.HLE.Exceptions
|
namespace Ryujinx.HLE.Exceptions
|
||||||
{
|
{
|
||||||
class InvalidFirmwarePackageException : Exception
|
public class InvalidFirmwarePackageException : Exception
|
||||||
{
|
{
|
||||||
public InvalidFirmwarePackageException(string message) : base(message) { }
|
public InvalidFirmwarePackageException(string message) : base(message) { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -483,10 +483,29 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
{
|
{
|
||||||
if (Directory.Exists(keysSource))
|
if (Directory.Exists(keysSource))
|
||||||
{
|
{
|
||||||
foreach (string filePath in Directory.EnumerateFiles(keysSource, "*.keys"))
|
string[] keyPaths = Directory.EnumerateFiles(keysSource, "*.keys").ToArray();
|
||||||
|
|
||||||
|
if (keyPaths.Length is 0)
|
||||||
|
throw new FileNotFoundException($"Directory '{keysSource}' contained no '.keys' files.");
|
||||||
|
|
||||||
|
foreach (string filePath in keyPaths)
|
||||||
{
|
{
|
||||||
VerifyKeysFile(filePath);
|
try
|
||||||
File.Copy(filePath, Path.Combine(installDirectory, Path.GetFileName(filePath)), true);
|
{
|
||||||
|
VerifyKeysFile(filePath);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Error?.Print(LogClass.Application, e.Message);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string destPath = Path.Combine(installDirectory, Path.GetFileName(filePath));
|
||||||
|
|
||||||
|
if (File.Exists(destPath))
|
||||||
|
File.Delete(destPath);
|
||||||
|
|
||||||
|
File.Copy(filePath, destPath, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -501,13 +520,25 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
|
|
||||||
using FileStream file = File.OpenRead(keysSource);
|
using FileStream file = File.OpenRead(keysSource);
|
||||||
|
|
||||||
if (info.Extension is ".keys")
|
if (info.Extension is not ".keys")
|
||||||
|
throw new InvalidFirmwarePackageException("Input file extension is not .keys");
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
VerifyKeysFile(keysSource);
|
VerifyKeysFile(keysSource);
|
||||||
File.Copy(keysSource, Path.Combine(installDirectory, info.Name), true);
|
}
|
||||||
}
|
catch
|
||||||
else
|
{
|
||||||
throw new InvalidFirmwarePackageException("Input file is not a valid key package");
|
throw new InvalidFirmwarePackageException("Input file is not a valid key package");
|
||||||
|
}
|
||||||
|
|
||||||
|
string dest = Path.Combine(installDirectory, info.Name);
|
||||||
|
|
||||||
|
if (File.Exists(dest))
|
||||||
|
File.Delete(dest);
|
||||||
|
|
||||||
|
// overwrite: true seems to not work on its own? https://github.com/Ryubing/Issues/issues/189
|
||||||
|
File.Copy(keysSource, dest, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FinishInstallation(string temporaryDirectory, string registeredDirectory)
|
private void FinishInstallation(string temporaryDirectory, string registeredDirectory)
|
||||||
@@ -517,6 +548,9 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
new DirectoryInfo(registeredDirectory).Delete(true);
|
new DirectoryInfo(registeredDirectory).Delete(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!Directory.Exists(temporaryDirectory))
|
||||||
|
return; // nothing to move
|
||||||
|
|
||||||
Directory.Move(temporaryDirectory, registeredDirectory);
|
Directory.Move(temporaryDirectory, registeredDirectory);
|
||||||
|
|
||||||
LoadEntries();
|
LoadEntries();
|
||||||
@@ -985,8 +1019,8 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
public static void VerifyKeysFile(string filePath)
|
public static void VerifyKeysFile(string filePath)
|
||||||
{
|
{
|
||||||
// Verify the keys file format refers to https://github.com/Thealexbarney/LibHac/blob/master/KEYS.md
|
// Verify the keys file format refers to https://github.com/Thealexbarney/LibHac/blob/master/KEYS.md
|
||||||
string genericPattern = @"^[a-z0-9_]+ = [a-z0-9]+$";
|
string genericPattern = "^[a-z0-9_]+ = [a-z0-9]+$";
|
||||||
string titlePattern = @"^[a-z0-9]{32} = [a-z0-9]{32}$";
|
string titlePattern = "^[a-z0-9]{32} = [a-z0-9]{32}$";
|
||||||
|
|
||||||
if (File.Exists(filePath))
|
if (File.Exists(filePath))
|
||||||
{
|
{
|
||||||
@@ -994,24 +1028,13 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
string fileName = Path.GetFileName(filePath);
|
string fileName = Path.GetFileName(filePath);
|
||||||
string[] lines = File.ReadAllLines(filePath);
|
string[] lines = File.ReadAllLines(filePath);
|
||||||
|
|
||||||
bool verified;
|
bool verified = fileName switch
|
||||||
switch (fileName)
|
|
||||||
{
|
{
|
||||||
case "prod.keys":
|
"prod.keys" or "console.keys" or "dev.keys" => VerifyKeys(lines, genericPattern),
|
||||||
verified = VerifyKeys(lines, genericPattern);
|
"title.keys" => VerifyKeys(lines, titlePattern),
|
||||||
break;
|
_ => throw new FormatException(
|
||||||
case "title.keys":
|
$"Keys file name \"{fileName}\" not supported. Only \"prod.keys\", \"title.keys\", \"console.keys\", \"dev.keys\" are supported.")
|
||||||
verified = VerifyKeys(lines, titlePattern);
|
};
|
||||||
break;
|
|
||||||
case "console.keys":
|
|
||||||
verified = VerifyKeys(lines, genericPattern);
|
|
||||||
break;
|
|
||||||
case "dev.keys":
|
|
||||||
verified = VerifyKeys(lines, genericPattern);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new FormatException($"Keys file name \"{fileName}\" not supported. Only \"prod.keys\", \"title.keys\", \"console.keys\", \"dev.keys\" are supported.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!verified)
|
if (!verified)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -219,6 +219,8 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
FileSystemServerInitializer.InitializeWithConfig(fsServerClient, fsServer, fsServerConfig);
|
FileSystemServerInitializer.InitializeWithConfig(fsServerClient, fsServer, fsServerConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool HasKeySet { get; private set; }
|
||||||
|
|
||||||
public void ReloadKeySet()
|
public void ReloadKeySet()
|
||||||
{
|
{
|
||||||
KeySet ??= KeySet.CreateDefaultKeySet();
|
KeySet ??= KeySet.CreateDefaultKeySet();
|
||||||
@@ -228,12 +230,19 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
string consoleKeyFile = null;
|
string consoleKeyFile = null;
|
||||||
string devKeyFile = null;
|
string devKeyFile = null;
|
||||||
|
|
||||||
if (AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile)
|
LoadSetAtPath(AppDataManager.GetKeysDir());
|
||||||
{
|
|
||||||
LoadSetAtPath(AppDataManager.KeysDirPathUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
LoadSetAtPath(AppDataManager.KeysDirPath);
|
HasKeySet = (prodKeyFile != null && titleKeyFile != null) || prodKeyFile != null;
|
||||||
|
|
||||||
|
|
||||||
|
ExternalKeyReader.ReadKeyFile(
|
||||||
|
KeySet,
|
||||||
|
prodKeyFile,
|
||||||
|
devKeyFile,
|
||||||
|
titleKeyFile,
|
||||||
|
consoleKeyFile);
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
void LoadSetAtPath(string basePath)
|
void LoadSetAtPath(string basePath)
|
||||||
{
|
{
|
||||||
@@ -262,8 +271,6 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
devKeyFile = localDevKeyFile;
|
devKeyFile = localDevKeyFile;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ExternalKeyReader.ReadKeyFile(KeySet, prodKeyFile, devKeyFile, titleKeyFile, consoleKeyFile, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ImportTickets(IFileSystem fs)
|
public void ImportTickets(IFileSystem fs)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Microsoft.IO;
|
|||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
using Ryujinx.Common.Memory;
|
using Ryujinx.Common.Memory;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -37,7 +38,7 @@ namespace Ryujinx.HLE.HOS.Ipc
|
|||||||
|
|
||||||
public IpcMessage(ReadOnlySpan<byte> data, long cmdPtr)
|
public IpcMessage(ReadOnlySpan<byte> data, long cmdPtr)
|
||||||
{
|
{
|
||||||
using RecyclableMemoryStream ms = MemoryStreamManager.Shared.GetStream(data);
|
RecyclableMemoryStream ms = MemoryStreamManager.Shared.GetStream(data);
|
||||||
|
|
||||||
BinaryReader reader = new(ms);
|
BinaryReader reader = new(ms);
|
||||||
|
|
||||||
@@ -123,6 +124,8 @@ namespace Ryujinx.HLE.HOS.Ipc
|
|||||||
}
|
}
|
||||||
|
|
||||||
ObjectIds = [];
|
ObjectIds = [];
|
||||||
|
|
||||||
|
MemoryStreamManager.Shared.ReleaseStream(ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecyclableMemoryStream GetStream(long cmdPtr, ulong recvListAddr)
|
public RecyclableMemoryStream GetStream(long cmdPtr, ulong recvListAddr)
|
||||||
|
|||||||
@@ -20,6 +20,15 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
|
|||||||
_exchangeBufferDescriptors = new List<KBufferDescriptor>(MaxInternalBuffersCount);
|
_exchangeBufferDescriptors = new List<KBufferDescriptor>(MaxInternalBuffersCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public KBufferDescriptorTable Clear()
|
||||||
|
{
|
||||||
|
_sendBufferDescriptors.Clear();
|
||||||
|
_receiveBufferDescriptors.Clear();
|
||||||
|
_exchangeBufferDescriptors.Clear();
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Result AddSendBuffer(ulong src, ulong dst, ulong size, MemoryState state)
|
public Result AddSendBuffer(ulong src, ulong dst, ulong size, MemoryState state)
|
||||||
{
|
{
|
||||||
return Add(_sendBufferDescriptors, src, dst, size, state);
|
return Add(_sendBufferDescriptors, src, dst, size, state);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Ryujinx.Common;
|
||||||
using Ryujinx.HLE.HOS.Kernel.Common;
|
using Ryujinx.HLE.HOS.Kernel.Common;
|
||||||
using Ryujinx.HLE.HOS.Kernel.Process;
|
using Ryujinx.HLE.HOS.Kernel.Process;
|
||||||
using Ryujinx.HLE.HOS.Kernel.Threading;
|
using Ryujinx.HLE.HOS.Kernel.Threading;
|
||||||
@@ -32,7 +33,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
|
|||||||
{
|
{
|
||||||
KThread currentThread = KernelStatic.GetCurrentThread();
|
KThread currentThread = KernelStatic.GetCurrentThread();
|
||||||
|
|
||||||
KSessionRequest request = new(currentThread, customCmdBuffAddr, customCmdBuffSize);
|
KSessionRequest request = _parent.ServerSession.RequestPool.Allocate().Set(currentThread, customCmdBuffAddr, customCmdBuffSize);
|
||||||
|
|
||||||
KernelContext.CriticalSection.Enter();
|
KernelContext.CriticalSection.Enter();
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
|
|||||||
{
|
{
|
||||||
KThread currentThread = KernelStatic.GetCurrentThread();
|
KThread currentThread = KernelStatic.GetCurrentThread();
|
||||||
|
|
||||||
KSessionRequest request = new(currentThread, customCmdBuffAddr, customCmdBuffSize, asyncEvent);
|
KSessionRequest request = _parent.ServerSession.RequestPool.Allocate().Set(currentThread, customCmdBuffAddr, customCmdBuffSize, asyncEvent);
|
||||||
|
|
||||||
KernelContext.CriticalSection.Enter();
|
KernelContext.CriticalSection.Enter();
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
|
|||||||
{
|
{
|
||||||
class KServerSession : KSynchronizationObject
|
class KServerSession : KSynchronizationObject
|
||||||
{
|
{
|
||||||
|
public readonly ObjectPool<KSessionRequest> RequestPool = new(() => new KSessionRequest());
|
||||||
|
|
||||||
private static readonly MemoryState[] _ipcMemoryStates =
|
private static readonly MemoryState[] _ipcMemoryStates =
|
||||||
[
|
[
|
||||||
MemoryState.IpcBuffer3,
|
MemoryState.IpcBuffer3,
|
||||||
@@ -274,6 +276,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
|
|||||||
KernelContext.CriticalSection.Leave();
|
KernelContext.CriticalSection.Leave();
|
||||||
|
|
||||||
WakeClientThread(request, clientResult);
|
WakeClientThread(request, clientResult);
|
||||||
|
|
||||||
|
RequestPool.Release(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clientHeader.ReceiveListType < 2 &&
|
if (clientHeader.ReceiveListType < 2 &&
|
||||||
@@ -627,6 +631,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
|
|||||||
CloseAllHandles(clientMsg, serverHeader, clientProcess);
|
CloseAllHandles(clientMsg, serverHeader, clientProcess);
|
||||||
|
|
||||||
FinishRequest(request, clientResult);
|
FinishRequest(request, clientResult);
|
||||||
|
|
||||||
|
RequestPool.Release(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clientHeader.ReceiveListType < 2 &&
|
if (clientHeader.ReceiveListType < 2 &&
|
||||||
@@ -865,6 +871,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
|
|||||||
|
|
||||||
// Unmap buffers from server.
|
// Unmap buffers from server.
|
||||||
FinishRequest(request, clientResult);
|
FinishRequest(request, clientResult);
|
||||||
|
|
||||||
|
RequestPool.Release(request);
|
||||||
|
|
||||||
return serverResult;
|
return serverResult;
|
||||||
}
|
}
|
||||||
@@ -1098,6 +1106,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
|
|||||||
foreach (KSessionRequest request in IterateWithRemovalOfAllRequests())
|
foreach (KSessionRequest request in IterateWithRemovalOfAllRequests())
|
||||||
{
|
{
|
||||||
FinishRequest(request, KernelResult.PortRemoteClosed);
|
FinishRequest(request, KernelResult.PortRemoteClosed);
|
||||||
|
|
||||||
|
RequestPool.Release(request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1117,6 +1127,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
|
|||||||
{
|
{
|
||||||
SendResultToAsyncRequestClient(request, KernelResult.PortRemoteClosed);
|
SendResultToAsyncRequestClient(request, KernelResult.PortRemoteClosed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RequestPool.Release(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
WakeServerThreads(KernelResult.PortRemoteClosed);
|
WakeServerThreads(KernelResult.PortRemoteClosed);
|
||||||
|
|||||||
@@ -5,18 +5,18 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
|
|||||||
{
|
{
|
||||||
class KSessionRequest
|
class KSessionRequest
|
||||||
{
|
{
|
||||||
public KBufferDescriptorTable BufferDescriptorTable { get; }
|
public KBufferDescriptorTable BufferDescriptorTable { get; private set; }
|
||||||
|
|
||||||
public KThread ClientThread { get; }
|
public KThread ClientThread { get; private set; }
|
||||||
|
|
||||||
public KProcess ServerProcess { get; set; }
|
public KProcess ServerProcess { get; set; }
|
||||||
|
|
||||||
public KWritableEvent AsyncEvent { get; }
|
public KWritableEvent AsyncEvent { get; private set; }
|
||||||
|
|
||||||
public ulong CustomCmdBuffAddr { get; }
|
public ulong CustomCmdBuffAddr { get; private set; }
|
||||||
public ulong CustomCmdBuffSize { get; }
|
public ulong CustomCmdBuffSize { get; private set; }
|
||||||
|
|
||||||
public KSessionRequest(
|
public KSessionRequest Set(
|
||||||
KThread clientThread,
|
KThread clientThread,
|
||||||
ulong customCmdBuffAddr,
|
ulong customCmdBuffAddr,
|
||||||
ulong customCmdBuffSize,
|
ulong customCmdBuffSize,
|
||||||
@@ -27,7 +27,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
|
|||||||
CustomCmdBuffSize = customCmdBuffSize;
|
CustomCmdBuffSize = customCmdBuffSize;
|
||||||
AsyncEvent = asyncEvent;
|
AsyncEvent = asyncEvent;
|
||||||
|
|
||||||
BufferDescriptorTable = new KBufferDescriptorTable();
|
BufferDescriptorTable = BufferDescriptorTable?.Clear() ?? new KBufferDescriptorTable();
|
||||||
|
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
using Ryujinx.Common;
|
|
||||||
using Ryujinx.HLE.HOS.Kernel.Common;
|
using Ryujinx.HLE.HOS.Kernel.Common;
|
||||||
using Ryujinx.HLE.HOS.Kernel.Process;
|
using Ryujinx.HLE.HOS.Kernel.Process;
|
||||||
using Ryujinx.Horizon.Common;
|
using Ryujinx.Horizon.Common;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Kernel.Threading
|
namespace Ryujinx.HLE.HOS.Kernel.Threading
|
||||||
@@ -12,12 +10,12 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
|
|||||||
class KAddressArbiter
|
class KAddressArbiter
|
||||||
{
|
{
|
||||||
private const int HasListenersMask = 0x40000000;
|
private const int HasListenersMask = 0x40000000;
|
||||||
private static readonly ObjectPool<KThread[]> _threadArrayPool = new(() => []);
|
|
||||||
|
|
||||||
private readonly KernelContext _context;
|
private readonly KernelContext _context;
|
||||||
|
|
||||||
private readonly List<KThread> _condVarThreads;
|
private readonly Dictionary<ulong, List<KThread>> _condVarThreads;
|
||||||
private readonly List<KThread> _arbiterThreads;
|
private readonly Dictionary<ulong, List<KThread>> _arbiterThreads;
|
||||||
|
private readonly ByDynamicPriority _byDynamicPriority;
|
||||||
|
|
||||||
public KAddressArbiter(KernelContext context)
|
public KAddressArbiter(KernelContext context)
|
||||||
{
|
{
|
||||||
@@ -25,6 +23,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
|
|||||||
|
|
||||||
_condVarThreads = [];
|
_condVarThreads = [];
|
||||||
_arbiterThreads = [];
|
_arbiterThreads = [];
|
||||||
|
_byDynamicPriority = new ByDynamicPriority();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result ArbitrateLock(int ownerHandle, ulong mutexAddress, int requesterHandle)
|
public Result ArbitrateLock(int ownerHandle, ulong mutexAddress, int requesterHandle)
|
||||||
@@ -140,9 +139,23 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
|
|||||||
|
|
||||||
currentThread.MutexAddress = mutexAddress;
|
currentThread.MutexAddress = mutexAddress;
|
||||||
currentThread.ThreadHandleForUserMutex = threadHandle;
|
currentThread.ThreadHandleForUserMutex = threadHandle;
|
||||||
currentThread.CondVarAddress = condVarAddress;
|
|
||||||
|
|
||||||
_condVarThreads.Add(currentThread);
|
if (_condVarThreads.TryGetValue(condVarAddress, out List<KThread> threads))
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
if (threads.Count > 0)
|
||||||
|
{
|
||||||
|
i = threads.BinarySearch(currentThread, _byDynamicPriority);
|
||||||
|
if (i < 0) i = ~i;
|
||||||
|
}
|
||||||
|
|
||||||
|
threads.Insert(i, currentThread);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_condVarThreads.Add(condVarAddress, [currentThread]);
|
||||||
|
}
|
||||||
|
|
||||||
if (timeout != 0)
|
if (timeout != 0)
|
||||||
{
|
{
|
||||||
@@ -165,7 +178,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
|
|||||||
|
|
||||||
currentThread.MutexOwner?.RemoveMutexWaiter(currentThread);
|
currentThread.MutexOwner?.RemoveMutexWaiter(currentThread);
|
||||||
|
|
||||||
_condVarThreads.Remove(currentThread);
|
_condVarThreads[condVarAddress].Remove(currentThread);
|
||||||
|
|
||||||
_context.CriticalSection.Leave();
|
_context.CriticalSection.Leave();
|
||||||
|
|
||||||
@@ -200,13 +213,14 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
|
|||||||
{
|
{
|
||||||
_context.CriticalSection.Enter();
|
_context.CriticalSection.Enter();
|
||||||
|
|
||||||
static bool SignalProcessWideKeyPredicate(KThread thread, ulong address)
|
int validThreads = 0;
|
||||||
|
_condVarThreads.TryGetValue(address, out List<KThread> threads);
|
||||||
|
|
||||||
|
if (threads is not null && threads.Count > 0)
|
||||||
{
|
{
|
||||||
return thread.CondVarAddress == address;
|
validThreads = WakeThreads(threads, count, TryAcquireMutex);
|
||||||
}
|
}
|
||||||
|
|
||||||
int validThreads = WakeThreads(_condVarThreads, count, TryAcquireMutex, SignalProcessWideKeyPredicate, address);
|
|
||||||
|
|
||||||
if (validThreads == 0)
|
if (validThreads == 0)
|
||||||
{
|
{
|
||||||
KernelTransfer.KernelToUser(address, 0);
|
KernelTransfer.KernelToUser(address, 0);
|
||||||
@@ -315,9 +329,24 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
|
|||||||
|
|
||||||
currentThread.MutexAddress = address;
|
currentThread.MutexAddress = address;
|
||||||
currentThread.WaitingInArbitration = true;
|
currentThread.WaitingInArbitration = true;
|
||||||
|
|
||||||
|
if (_arbiterThreads.TryGetValue(address, out List<KThread> threads))
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
_arbiterThreads.Add(currentThread);
|
if (threads.Count > 0)
|
||||||
|
{
|
||||||
|
i = threads.BinarySearch(currentThread, _byDynamicPriority);
|
||||||
|
if (i < 0) i = ~i;
|
||||||
|
}
|
||||||
|
|
||||||
|
threads.Insert(i, currentThread);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_arbiterThreads.Add(address, [currentThread]);
|
||||||
|
}
|
||||||
|
|
||||||
currentThread.Reschedule(ThreadSchedState.Paused);
|
currentThread.Reschedule(ThreadSchedState.Paused);
|
||||||
|
|
||||||
if (timeout > 0)
|
if (timeout > 0)
|
||||||
@@ -336,7 +365,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
|
|||||||
|
|
||||||
if (currentThread.WaitingInArbitration)
|
if (currentThread.WaitingInArbitration)
|
||||||
{
|
{
|
||||||
_arbiterThreads.Remove(currentThread);
|
_arbiterThreads[address].Remove(currentThread);
|
||||||
|
|
||||||
currentThread.WaitingInArbitration = false;
|
currentThread.WaitingInArbitration = false;
|
||||||
}
|
}
|
||||||
@@ -392,9 +421,24 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
|
|||||||
|
|
||||||
currentThread.MutexAddress = address;
|
currentThread.MutexAddress = address;
|
||||||
currentThread.WaitingInArbitration = true;
|
currentThread.WaitingInArbitration = true;
|
||||||
|
|
||||||
|
if (_arbiterThreads.TryGetValue(address, out List<KThread> threads))
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
_arbiterThreads.Add(currentThread);
|
if (threads.Count > 0)
|
||||||
|
{
|
||||||
|
i = threads.BinarySearch(currentThread, _byDynamicPriority);
|
||||||
|
if (i < 0) i = ~i;
|
||||||
|
}
|
||||||
|
|
||||||
|
threads.Insert(i, currentThread);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_arbiterThreads.Add(address, [currentThread]);
|
||||||
|
}
|
||||||
|
|
||||||
currentThread.Reschedule(ThreadSchedState.Paused);
|
currentThread.Reschedule(ThreadSchedState.Paused);
|
||||||
|
|
||||||
if (timeout > 0)
|
if (timeout > 0)
|
||||||
@@ -413,7 +457,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
|
|||||||
|
|
||||||
if (currentThread.WaitingInArbitration)
|
if (currentThread.WaitingInArbitration)
|
||||||
{
|
{
|
||||||
_arbiterThreads.Remove(currentThread);
|
_arbiterThreads[address].Remove(currentThread);
|
||||||
|
|
||||||
currentThread.WaitingInArbitration = false;
|
currentThread.WaitingInArbitration = false;
|
||||||
}
|
}
|
||||||
@@ -486,15 +530,12 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
|
|||||||
// or equal to the Count of threads to be signaled, or Count is zero
|
// or equal to the Count of threads to be signaled, or Count is zero
|
||||||
// or negative. It is incremented if there are no threads waiting.
|
// or negative. It is incremented if there are no threads waiting.
|
||||||
int waitingCount = 0;
|
int waitingCount = 0;
|
||||||
|
|
||||||
foreach (KThread thread in _arbiterThreads)
|
if (_arbiterThreads.TryGetValue(address, out List<KThread> threads))
|
||||||
{
|
{
|
||||||
if (thread.MutexAddress == address &&
|
waitingCount = threads.Count;
|
||||||
++waitingCount >= count)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (waitingCount > 0)
|
if (waitingCount > 0)
|
||||||
{
|
{
|
||||||
@@ -561,55 +602,38 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
|
|||||||
thread.WaitingInArbitration = false;
|
thread.WaitingInArbitration = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool ArbiterThreadPredecate(KThread thread, ulong address)
|
_arbiterThreads.TryGetValue(address, out List<KThread> threads);
|
||||||
{
|
|
||||||
return thread.MutexAddress == address;
|
|
||||||
}
|
|
||||||
|
|
||||||
WakeThreads(_arbiterThreads, count, RemoveArbiterThread, ArbiterThreadPredecate, address);
|
if (threads is not null && threads.Count > 0)
|
||||||
|
{
|
||||||
|
WakeThreads(threads, count, RemoveArbiterThread);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int WakeThreads(
|
private static int WakeThreads(
|
||||||
List<KThread> threads,
|
List<KThread> threads,
|
||||||
int count,
|
int count,
|
||||||
Action<KThread> removeCallback,
|
Action<KThread> removeCallback)
|
||||||
Func<KThread, ulong, bool> predicate,
|
|
||||||
ulong address = 0)
|
|
||||||
{
|
{
|
||||||
KThread[] candidates = _threadArrayPool.Allocate();
|
int validCount = count > 0 ? Math.Min(count, threads.Count) : threads.Count;
|
||||||
if (candidates.Length < threads.Count)
|
|
||||||
{
|
|
||||||
Array.Resize(ref candidates, threads.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
int validCount = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < threads.Count; i++)
|
|
||||||
{
|
|
||||||
if (predicate(threads[i], address))
|
|
||||||
{
|
|
||||||
candidates[validCount++] = threads[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Span<KThread> candidatesSpan = candidates.AsSpan(..validCount);
|
|
||||||
|
|
||||||
candidatesSpan.Sort((x, y) => (x.DynamicPriority.CompareTo(y.DynamicPriority)));
|
|
||||||
|
|
||||||
if (count > 0)
|
for (int i = 0; i < validCount; i++)
|
||||||
{
|
|
||||||
candidatesSpan = candidatesSpan[..Math.Min(count, candidatesSpan.Length)];
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (KThread thread in candidatesSpan)
|
|
||||||
{
|
{
|
||||||
|
KThread thread = threads[i];
|
||||||
removeCallback(thread);
|
removeCallback(thread);
|
||||||
threads.Remove(thread);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_threadArrayPool.Release(candidates);
|
threads.RemoveRange(0, validCount);
|
||||||
|
|
||||||
return validCount;
|
return validCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ByDynamicPriority : IComparer<KThread>
|
||||||
|
{
|
||||||
|
public int Compare(KThread x, KThread y)
|
||||||
|
{
|
||||||
|
return x!.DynamicPriority.CompareTo(y!.DynamicPriority);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,8 +61,6 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
|
|||||||
|
|
||||||
public KSynchronizationObject SignaledObj { get; set; }
|
public KSynchronizationObject SignaledObj { get; set; }
|
||||||
|
|
||||||
public ulong CondVarAddress { get; set; }
|
|
||||||
|
|
||||||
private ulong _entrypoint;
|
private ulong _entrypoint;
|
||||||
private ThreadStart _customThreadStart;
|
private ThreadStart _customThreadStart;
|
||||||
private bool _forcedUnschedulable;
|
private bool _forcedUnschedulable;
|
||||||
|
|||||||
@@ -416,7 +416,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
|
|||||||
return ResultCode.InvalidParameters;
|
return ResultCode.InvalidParameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Stub?.PrintStub(LogClass.ServiceAm, new { albumReportOption });
|
context.Device.UIHandler.TakeScreenshot();
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ namespace Ryujinx.HLE.HOS.Services
|
|||||||
private int _selfId;
|
private int _selfId;
|
||||||
private bool _isDomain;
|
private bool _isDomain;
|
||||||
|
|
||||||
|
// cache array so we don't recreate it all the time
|
||||||
|
private object[] _parameters = [null];
|
||||||
|
|
||||||
public IpcService(ServerBase server = null, bool registerTipc = false)
|
public IpcService(ServerBase server = null, bool registerTipc = false)
|
||||||
{
|
{
|
||||||
Stopwatch sw = Stopwatch.StartNew();
|
Stopwatch sw = Stopwatch.StartNew();
|
||||||
@@ -146,7 +149,9 @@ namespace Ryujinx.HLE.HOS.Services
|
|||||||
{
|
{
|
||||||
Logger.Trace?.Print(LogClass.KernelIpc, $"{service.GetType().Name}: {processRequest.Name}");
|
Logger.Trace?.Print(LogClass.KernelIpc, $"{service.GetType().Name}: {processRequest.Name}");
|
||||||
|
|
||||||
result = (ResultCode)processRequest.Invoke(service, [context]);
|
_parameters[0] = context;
|
||||||
|
|
||||||
|
result = (ResultCode)processRequest.Invoke(service, _parameters);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -196,7 +201,9 @@ namespace Ryujinx.HLE.HOS.Services
|
|||||||
{
|
{
|
||||||
Logger.Debug?.Print(LogClass.KernelIpc, $"{GetType().Name}: {processRequest.Name}");
|
Logger.Debug?.Print(LogClass.KernelIpc, $"{GetType().Name}: {processRequest.Name}");
|
||||||
|
|
||||||
result = (ResultCode)processRequest.Invoke(this, [context]);
|
_parameters[0] = context;
|
||||||
|
|
||||||
|
result = (ResultCode)processRequest.Invoke(this, _parameters);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption
|
|||||||
|
|
||||||
private static string GetKeyRetailBinPath()
|
private static string GetKeyRetailBinPath()
|
||||||
{
|
{
|
||||||
return Path.Combine(AppDataManager.KeysDirPath, "key_retail.bin");
|
return Path.Combine(AppDataManager.GetKeysDir(), "key_retail.bin");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool HasAmiiboKeyFile => File.Exists(GetKeyRetailBinPath());
|
public static bool HasAmiiboKeyFile => File.Exists(GetKeyRetailBinPath());
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ namespace Ryujinx.HLE.HOS.Services.Nv
|
|||||||
|
|
||||||
// TODO: This should call set:sys::GetDebugModeFlag
|
// TODO: This should call set:sys::GetDebugModeFlag
|
||||||
private readonly bool _debugModeEnabled = false;
|
private readonly bool _debugModeEnabled = false;
|
||||||
|
|
||||||
|
private byte[] _ioctl2Buffer = [];
|
||||||
|
private byte[] _ioctlArgumentBuffer = [];
|
||||||
|
private byte[] _ioctl3Buffer = [];
|
||||||
|
|
||||||
public INvDrvServices(ServiceCtx context) : base(context.Device.System.NvDrvServer)
|
public INvDrvServices(ServiceCtx context) : base(context.Device.System.NvDrvServer)
|
||||||
{
|
{
|
||||||
@@ -128,27 +132,38 @@ namespace Ryujinx.HLE.HOS.Services.Nv
|
|||||||
|
|
||||||
if (!context.Memory.TryReadUnsafe(inputDataPosition, (int)inputDataSize, out arguments))
|
if (!context.Memory.TryReadUnsafe(inputDataPosition, (int)inputDataSize, out arguments))
|
||||||
{
|
{
|
||||||
arguments = new byte[inputDataSize];
|
if (_ioctlArgumentBuffer.Length < (int)inputDataSize)
|
||||||
|
{
|
||||||
|
Array.Resize(ref _ioctlArgumentBuffer, (int)inputDataSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
arguments = _ioctlArgumentBuffer.AsSpan(0, (int)inputDataSize);
|
||||||
|
|
||||||
context.Memory.Read(inputDataPosition, arguments);
|
context.Memory.Read(inputDataPosition, arguments);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
arguments = arguments.ToArray();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (isWrite)
|
else if (isWrite)
|
||||||
{
|
{
|
||||||
byte[] outputData = new byte[outputDataSize];
|
if (_ioctlArgumentBuffer.Length < (int)outputDataSize)
|
||||||
|
{
|
||||||
arguments = new Span<byte>(outputData);
|
Array.Resize(ref _ioctlArgumentBuffer, (int)outputDataSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
arguments = _ioctlArgumentBuffer.AsSpan(0, (int)outputDataSize);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
byte[] temp = new byte[inputDataSize];
|
if (!context.Memory.TryReadUnsafe(inputDataPosition, (int)inputDataSize, out arguments))
|
||||||
|
{
|
||||||
|
if (_ioctlArgumentBuffer.Length < (int)inputDataSize)
|
||||||
|
{
|
||||||
|
Array.Resize(ref _ioctlArgumentBuffer, (int)inputDataSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
arguments = _ioctlArgumentBuffer.AsSpan(0, (int)inputDataSize);
|
||||||
|
|
||||||
context.Memory.Read(inputDataPosition, temp);
|
context.Memory.Read(inputDataPosition, arguments);
|
||||||
|
}
|
||||||
arguments = new Span<byte>(temp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return NvResult.Success;
|
return NvResult.Success;
|
||||||
@@ -270,7 +285,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv
|
|||||||
|
|
||||||
if ((ioctlCommand.DirectionValue & NvIoctl.Direction.Write) != 0)
|
if ((ioctlCommand.DirectionValue & NvIoctl.Direction.Write) != 0)
|
||||||
{
|
{
|
||||||
context.Memory.Write(context.Request.GetBufferType0x22(0).Position, arguments.ToArray());
|
context.Memory.Write(context.Request.GetBufferType0x22(0).Position, arguments);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -474,13 +489,15 @@ namespace Ryujinx.HLE.HOS.Services.Nv
|
|||||||
|
|
||||||
errorCode = GetIoctlArgument(context, ioctlCommand, out Span<byte> arguments);
|
errorCode = GetIoctlArgument(context, ioctlCommand, out Span<byte> arguments);
|
||||||
|
|
||||||
byte[] inlineInBuffer = null;
|
|
||||||
|
|
||||||
if (!context.Memory.TryReadUnsafe(inlineInBufferPosition, (int)inlineInBufferSize, out Span<byte> inlineInBufferSpan))
|
if (!context.Memory.TryReadUnsafe(inlineInBufferPosition, (int)inlineInBufferSize, out Span<byte> inlineInBufferSpan))
|
||||||
{
|
{
|
||||||
inlineInBuffer = _byteArrayPool.Rent((int)inlineInBufferSize);
|
if (_ioctl2Buffer.Length < (int)inlineInBufferSize)
|
||||||
inlineInBufferSpan = inlineInBuffer;
|
{
|
||||||
context.Memory.Read(inlineInBufferPosition, inlineInBufferSpan[..(int)inlineInBufferSize]);
|
Array.Resize(ref _ioctl2Buffer, (int)inlineInBufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
inlineInBufferSpan = _ioctl2Buffer.AsSpan(0, (int)inlineInBufferSize);
|
||||||
|
context.Memory.Read(inlineInBufferPosition, inlineInBufferSpan);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorCode == NvResult.Success)
|
if (errorCode == NvResult.Success)
|
||||||
@@ -489,7 +506,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv
|
|||||||
|
|
||||||
if (errorCode == NvResult.Success)
|
if (errorCode == NvResult.Success)
|
||||||
{
|
{
|
||||||
NvInternalResult internalResult = deviceFile.Ioctl2(ioctlCommand, arguments, inlineInBufferSpan[..(int)inlineInBufferSize]);
|
NvInternalResult internalResult = deviceFile.Ioctl2(ioctlCommand, arguments, inlineInBufferSpan);
|
||||||
|
|
||||||
if (internalResult == NvInternalResult.NotImplemented)
|
if (internalResult == NvInternalResult.NotImplemented)
|
||||||
{
|
{
|
||||||
@@ -500,15 +517,10 @@ namespace Ryujinx.HLE.HOS.Services.Nv
|
|||||||
|
|
||||||
if ((ioctlCommand.DirectionValue & NvIoctl.Direction.Write) != 0)
|
if ((ioctlCommand.DirectionValue & NvIoctl.Direction.Write) != 0)
|
||||||
{
|
{
|
||||||
context.Memory.Write(context.Request.GetBufferType0x22(0).Position, arguments.ToArray());
|
context.Memory.Write(context.Request.GetBufferType0x22(0).Position, arguments);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inlineInBuffer is not null)
|
|
||||||
{
|
|
||||||
_byteArrayPool.Return(inlineInBuffer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
context.ResponseData.Write((uint)errorCode);
|
context.ResponseData.Write((uint)errorCode);
|
||||||
@@ -531,13 +543,15 @@ namespace Ryujinx.HLE.HOS.Services.Nv
|
|||||||
|
|
||||||
errorCode = GetIoctlArgument(context, ioctlCommand, out Span<byte> arguments);
|
errorCode = GetIoctlArgument(context, ioctlCommand, out Span<byte> arguments);
|
||||||
|
|
||||||
byte[] inlineOutBuffer = null;
|
|
||||||
|
|
||||||
if (!context.Memory.TryReadUnsafe(inlineOutBufferPosition, (int)inlineOutBufferSize, out Span<byte> inlineOutBufferSpan))
|
if (!context.Memory.TryReadUnsafe(inlineOutBufferPosition, (int)inlineOutBufferSize, out Span<byte> inlineOutBufferSpan))
|
||||||
{
|
{
|
||||||
inlineOutBuffer = _byteArrayPool.Rent((int)inlineOutBufferSize);
|
if (_ioctl3Buffer.Length < (int)inlineOutBufferSize)
|
||||||
inlineOutBufferSpan = inlineOutBuffer;
|
{
|
||||||
context.Memory.Read(inlineOutBufferPosition, inlineOutBufferSpan[..(int)inlineOutBufferSize]);
|
Array.Resize(ref _ioctl3Buffer, (int)inlineOutBufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
inlineOutBufferSpan = _ioctl3Buffer.AsSpan(0, (int)inlineOutBufferSize);
|
||||||
|
context.Memory.Read(inlineOutBufferPosition, inlineOutBufferSpan);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorCode == NvResult.Success)
|
if (errorCode == NvResult.Success)
|
||||||
@@ -546,7 +560,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv
|
|||||||
|
|
||||||
if (errorCode == NvResult.Success)
|
if (errorCode == NvResult.Success)
|
||||||
{
|
{
|
||||||
NvInternalResult internalResult = deviceFile.Ioctl3(ioctlCommand, arguments, inlineOutBufferSpan[..(int)inlineOutBufferSize]);
|
NvInternalResult internalResult = deviceFile.Ioctl3(ioctlCommand, arguments, inlineOutBufferSpan);
|
||||||
|
|
||||||
if (internalResult == NvInternalResult.NotImplemented)
|
if (internalResult == NvInternalResult.NotImplemented)
|
||||||
{
|
{
|
||||||
@@ -557,16 +571,11 @@ namespace Ryujinx.HLE.HOS.Services.Nv
|
|||||||
|
|
||||||
if ((ioctlCommand.DirectionValue & NvIoctl.Direction.Write) != 0)
|
if ((ioctlCommand.DirectionValue & NvIoctl.Direction.Write) != 0)
|
||||||
{
|
{
|
||||||
context.Memory.Write(context.Request.GetBufferType0x22(0).Position, arguments.ToArray());
|
context.Memory.Write(context.Request.GetBufferType0x22(0).Position, arguments);
|
||||||
context.Memory.Write(inlineOutBufferPosition, inlineOutBufferSpan[..(int)inlineOutBufferSize].ToArray());
|
context.Memory.Write(inlineOutBufferPosition, inlineOutBufferSpan);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inlineOutBuffer is not null)
|
|
||||||
{
|
|
||||||
_byteArrayPool.Return(inlineOutBuffer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
context.ResponseData.Write((uint)errorCode);
|
context.ResponseData.Write((uint)errorCode);
|
||||||
|
|||||||
@@ -454,8 +454,9 @@ namespace Ryujinx.HLE.HOS.Services
|
|||||||
|
|
||||||
response.RawData = _responseDataStream.ToArray();
|
response.RawData = _responseDataStream.ToArray();
|
||||||
|
|
||||||
using RecyclableMemoryStream responseStream = response.GetStreamTipc();
|
RecyclableMemoryStream responseStream = response.GetStreamTipc();
|
||||||
_selfProcess.CpuMemory.Write(_selfThread.TlsAddress, responseStream.GetReadOnlySequence());
|
_selfProcess.CpuMemory.Write(_selfThread.TlsAddress, responseStream.GetReadOnlySequence());
|
||||||
|
MemoryStreamManager.Shared.ReleaseStream(responseStream);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -464,8 +465,9 @@ namespace Ryujinx.HLE.HOS.Services
|
|||||||
|
|
||||||
if (!isTipcCommunication)
|
if (!isTipcCommunication)
|
||||||
{
|
{
|
||||||
using RecyclableMemoryStream responseStream = response.GetStream((long)_selfThread.TlsAddress, recvListAddr | ((ulong)PointerBufferSize << 48));
|
RecyclableMemoryStream responseStream = response.GetStream((long)_selfThread.TlsAddress, recvListAddr | ((ulong)PointerBufferSize << 48));
|
||||||
_selfProcess.CpuMemory.Write(_selfThread.TlsAddress, responseStream.GetReadOnlySequence());
|
_selfProcess.CpuMemory.Write(_selfThread.TlsAddress, responseStream.GetReadOnlySequence());
|
||||||
|
MemoryStreamManager.Shared.ReleaseStream(responseStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
return shouldReply;
|
return shouldReply;
|
||||||
|
|||||||
@@ -68,5 +68,10 @@ namespace Ryujinx.HLE.UI
|
|||||||
/// Displays the player select dialog and returns the selected profile.
|
/// Displays the player select dialog and returns the selected profile.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
UserProfile ShowPlayerSelectDialog();
|
UserProfile ShowPlayerSelectDialog();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Takes a screenshot from the current renderer and saves it in the screenshots folder.
|
||||||
|
/// </summary>
|
||||||
|
void TakeScreenshot();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ namespace Ryujinx.Horizon.Sdk.OsTypes.Impl
|
|||||||
private int _waitingThreadHandle;
|
private int _waitingThreadHandle;
|
||||||
|
|
||||||
private MultiWaitHolderBase _signaledHolder;
|
private MultiWaitHolderBase _signaledHolder;
|
||||||
|
|
||||||
|
ObjectPool<int[]> _objectHandlePool = new(() => new int[64]);
|
||||||
|
ObjectPool<MultiWaitHolderBase[]> _objectPool = new(() => new MultiWaitHolderBase[64]);
|
||||||
|
|
||||||
public long CurrentTime { get; private set; }
|
public long CurrentTime { get; private set; }
|
||||||
|
|
||||||
@@ -76,11 +79,15 @@ namespace Ryujinx.Horizon.Sdk.OsTypes.Impl
|
|||||||
|
|
||||||
private MultiWaitHolderBase WaitAnyHandleImpl(bool infinite, long timeout)
|
private MultiWaitHolderBase WaitAnyHandleImpl(bool infinite, long timeout)
|
||||||
{
|
{
|
||||||
Span<int> objectHandles = new int[64];
|
int[] objectHandles = _objectHandlePool.Allocate();
|
||||||
|
Span<int> objectHandlesSpan = objectHandles;
|
||||||
|
objectHandlesSpan.Clear();
|
||||||
|
|
||||||
Span<MultiWaitHolderBase> objects = new MultiWaitHolderBase[64];
|
MultiWaitHolderBase[] objects = _objectPool.Allocate();
|
||||||
|
Span<MultiWaitHolderBase> objectsSpan = objects;
|
||||||
|
objectsSpan.Clear();
|
||||||
|
|
||||||
int count = FillObjectsArray(objectHandles, objects);
|
int count = FillObjectsArray(objectHandlesSpan, objectsSpan);
|
||||||
|
|
||||||
long endTime = infinite ? long.MaxValue : PerformanceCounter.ElapsedMilliseconds * 1000000;
|
long endTime = infinite ? long.MaxValue : PerformanceCounter.ElapsedMilliseconds * 1000000;
|
||||||
|
|
||||||
@@ -98,7 +105,7 @@ namespace Ryujinx.Horizon.Sdk.OsTypes.Impl
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
index = WaitSynchronization(objectHandles[..count], minTimeout);
|
index = WaitSynchronization(objectHandlesSpan[..count], minTimeout);
|
||||||
|
|
||||||
DebugUtil.Assert(index != WaitInvalid);
|
DebugUtil.Assert(index != WaitInvalid);
|
||||||
}
|
}
|
||||||
@@ -116,12 +123,18 @@ namespace Ryujinx.Horizon.Sdk.OsTypes.Impl
|
|||||||
{
|
{
|
||||||
_signaledHolder = minTimeoutObject;
|
_signaledHolder = minTimeoutObject;
|
||||||
|
|
||||||
|
_objectHandlePool.Release(objectHandles);
|
||||||
|
_objectPool.Release(objects);
|
||||||
|
|
||||||
return _signaledHolder;
|
return _signaledHolder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
_objectHandlePool.Release(objectHandles);
|
||||||
|
_objectPool.Release(objects);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +144,9 @@ namespace Ryujinx.Horizon.Sdk.OsTypes.Impl
|
|||||||
{
|
{
|
||||||
if (_signaledHolder != null)
|
if (_signaledHolder != null)
|
||||||
{
|
{
|
||||||
|
_objectHandlePool.Release(objectHandles);
|
||||||
|
_objectPool.Release(objects);
|
||||||
|
|
||||||
return _signaledHolder;
|
return _signaledHolder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,8 +155,11 @@ namespace Ryujinx.Horizon.Sdk.OsTypes.Impl
|
|||||||
default:
|
default:
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
_signaledHolder = objects[index];
|
_signaledHolder = objectsSpan[index];
|
||||||
|
|
||||||
|
_objectHandlePool.Release(objectHandles);
|
||||||
|
_objectPool.Release(objects);
|
||||||
|
|
||||||
return _signaledHolder;
|
return _signaledHolder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -532,8 +532,6 @@ namespace Ryujinx.Input.HLE
|
|||||||
|
|
||||||
hidKeyboard.Modifier |= value << entry.Target;
|
hidKeyboard.Modifier |= value << entry.Target;
|
||||||
}
|
}
|
||||||
|
|
||||||
ArrayPool<bool>.Shared.Return(keyboardState.KeysState);
|
|
||||||
|
|
||||||
return hidKeyboard;
|
return hidKeyboard;
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ namespace Ryujinx.Input.HLE
|
|||||||
{
|
{
|
||||||
public class NpadManager : IDisposable
|
public class NpadManager : IDisposable
|
||||||
{
|
{
|
||||||
private static readonly ObjectPool<List<SixAxisInput>> _hleMotionStatesPool = new (() => new List<SixAxisInput>(NpadDevices.MaxControllers));
|
|
||||||
private readonly CemuHookClient _cemuHookClient;
|
private readonly CemuHookClient _cemuHookClient;
|
||||||
|
|
||||||
private readonly Lock _lock = new();
|
private readonly Lock _lock = new();
|
||||||
@@ -40,6 +39,9 @@ namespace Ryujinx.Input.HLE
|
|||||||
private bool _enableKeyboard;
|
private bool _enableKeyboard;
|
||||||
private bool _enableMouse;
|
private bool _enableMouse;
|
||||||
private Switch _device;
|
private Switch _device;
|
||||||
|
|
||||||
|
private readonly List<GamepadInput> _hleInputStates = [];
|
||||||
|
private readonly List<SixAxisInput> _hleMotionStates = new(NpadDevices.MaxControllers);
|
||||||
|
|
||||||
public NpadManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver, IGamepadDriver mouseDriver)
|
public NpadManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver, IGamepadDriver mouseDriver)
|
||||||
{
|
{
|
||||||
@@ -217,8 +219,8 @@ namespace Ryujinx.Input.HLE
|
|||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
List<GamepadInput> hleInputStates = [];
|
_hleInputStates.Clear();
|
||||||
List<SixAxisInput> hleMotionStates = _hleMotionStatesPool.Allocate();
|
_hleMotionStates.Clear();
|
||||||
|
|
||||||
KeyboardInput? hleKeyboardInput = null;
|
KeyboardInput? hleKeyboardInput = null;
|
||||||
|
|
||||||
@@ -260,14 +262,14 @@ namespace Ryujinx.Input.HLE
|
|||||||
inputState.PlayerId = playerIndex;
|
inputState.PlayerId = playerIndex;
|
||||||
motionState.Item1.PlayerId = playerIndex;
|
motionState.Item1.PlayerId = playerIndex;
|
||||||
|
|
||||||
hleInputStates.Add(inputState);
|
_hleInputStates.Add(inputState);
|
||||||
hleMotionStates.Add(motionState.Item1);
|
_hleMotionStates.Add(motionState.Item1);
|
||||||
|
|
||||||
if (isJoyconPair && !motionState.Item2.Equals(default))
|
if (isJoyconPair && !motionState.Item2.Equals(default))
|
||||||
{
|
{
|
||||||
motionState.Item2.PlayerId = playerIndex;
|
motionState.Item2.PlayerId = playerIndex;
|
||||||
|
|
||||||
hleMotionStates.Add(motionState.Item2);
|
_hleMotionStates.Add(motionState.Item2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,8 +278,8 @@ namespace Ryujinx.Input.HLE
|
|||||||
hleKeyboardInput = NpadController.GetHLEKeyboardInput(_keyboardDriver);
|
hleKeyboardInput = NpadController.GetHLEKeyboardInput(_keyboardDriver);
|
||||||
}
|
}
|
||||||
|
|
||||||
_device.Hid.Npads.Update(hleInputStates);
|
_device.Hid.Npads.Update(_hleInputStates);
|
||||||
_device.Hid.Npads.UpdateSixAxis(hleMotionStates);
|
_device.Hid.Npads.UpdateSixAxis(_hleMotionStates);
|
||||||
|
|
||||||
if (hleKeyboardInput.HasValue)
|
if (hleKeyboardInput.HasValue)
|
||||||
{
|
{
|
||||||
@@ -328,10 +330,7 @@ namespace Ryujinx.Input.HLE
|
|||||||
_device.Hid.Mouse.Update(0, 0);
|
_device.Hid.Mouse.Update(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
_device.TamperMachine.UpdateInput(hleInputStates);
|
_device.TamperMachine.UpdateInput(_hleInputStates);
|
||||||
|
|
||||||
hleMotionStates.Clear();
|
|
||||||
_hleMotionStatesPool.Release(hleMotionStates);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ namespace Ryujinx.Input
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IKeyboard : IGamepad
|
public interface IKeyboard : IGamepad
|
||||||
{
|
{
|
||||||
|
private static bool[] _keyState;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Check if a given key is pressed on the keyboard.
|
/// Check if a given key is pressed on the keyboard.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -29,15 +31,17 @@ namespace Ryujinx.Input
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
static KeyboardStateSnapshot GetStateSnapshot(IKeyboard keyboard)
|
static KeyboardStateSnapshot GetStateSnapshot(IKeyboard keyboard)
|
||||||
{
|
{
|
||||||
|
if (_keyState is null)
|
||||||
|
{
|
||||||
|
_keyState = new bool[(int)Key.Count];
|
||||||
|
}
|
||||||
|
|
||||||
bool[] keysState = ArrayPool<bool>.Shared.Rent((int)Key.Count);
|
|
||||||
|
|
||||||
for (Key key = 0; key < Key.Count; key++)
|
for (Key key = 0; key < Key.Count; key++)
|
||||||
{
|
{
|
||||||
keysState[(int)key] = keyboard.IsPressed(key);
|
_keyState[(int)key] = keyboard.IsPressed(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new KeyboardStateSnapshot(keysState);
|
return new KeyboardStateSnapshot(_keyState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ namespace Ryujinx.Memory.Range
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Range of memory that can be split in two.
|
/// Range of memory that can be split in two.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface INonOverlappingRange : IRange
|
public interface INonOverlappingRange<T> : IRangeListRange<T> where T : class, IRangeListRange<T>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Split this region into two, around the specified address.
|
/// Split this region into two, around the specified address.
|
||||||
@@ -11,6 +11,6 @@ namespace Ryujinx.Memory.Range
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="splitAddress">Address to split the region around</param>
|
/// <param name="splitAddress">Address to split the region around</param>
|
||||||
/// <returns>The second part of the split region, with start address at the given split.</returns>
|
/// <returns>The second part of the split region, with start address at the given split.</returns>
|
||||||
public INonOverlappingRange Split(ulong splitAddress);
|
public INonOverlappingRange<T> Split(ulong splitAddress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ namespace Ryujinx.Memory.Range
|
|||||||
/// Check if this range overlaps with another.
|
/// Check if this range overlaps with another.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="address">Base address</param>
|
/// <param name="address">Base address</param>
|
||||||
/// <param name="size">Size of the range</param>
|
/// <param name="endAddress">EndAddress of the range</param>
|
||||||
/// <returns>True if overlapping, false otherwise</returns>
|
/// <returns>True if overlapping, false otherwise</returns>
|
||||||
bool OverlapsWith(ulong address, ulong size);
|
bool OverlapsWith(ulong address, ulong endAddress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace Ryujinx.Memory.Range
|
|||||||
/// A range list that assumes ranges are non-overlapping, with list items that can be split in two to avoid overlaps.
|
/// A range list that assumes ranges are non-overlapping, with list items that can be split in two to avoid overlaps.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">Type of the range.</typeparam>
|
/// <typeparam name="T">Type of the range.</typeparam>
|
||||||
public unsafe class NonOverlappingRangeList<T> : RangeListBase<T> where T : class, INonOverlappingRange
|
public class NonOverlappingRangeList<T> : RangeListBase<T> where T : class, INonOverlappingRange<T>
|
||||||
{
|
{
|
||||||
public readonly ReaderWriterLockSlim Lock = new();
|
public readonly ReaderWriterLockSlim Lock = new();
|
||||||
|
|
||||||
@@ -32,83 +32,18 @@ namespace Ryujinx.Memory.Range
|
|||||||
/// <param name="item">The item to be added</param>
|
/// <param name="item">The item to be added</param>
|
||||||
public override void Add(T item)
|
public override void Add(T item)
|
||||||
{
|
{
|
||||||
|
Debug.Assert(item.Address != item.EndAddress);
|
||||||
|
|
||||||
int index = BinarySearch(item.Address);
|
int index = BinarySearch(item.Address);
|
||||||
|
|
||||||
if (index < 0)
|
if (index < 0)
|
||||||
{
|
{
|
||||||
index = ~index;
|
index = ~index;
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeItem<T> rangeItem = _rangeItemPool.Allocate().Set(item);
|
|
||||||
|
|
||||||
Insert(index, rangeItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates an item's end address on the list. Address must be the same.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="item">The item to be updated</param>
|
|
||||||
/// <returns>True if the item was located and updated, false otherwise</returns>
|
|
||||||
protected override bool Update(T item)
|
|
||||||
{
|
|
||||||
int index = BinarySearch(item.Address);
|
|
||||||
|
|
||||||
if (index >= 0 && Items[index].Value.Equals(item))
|
|
||||||
{
|
|
||||||
RangeItem<T> rangeItem = new(item) { Previous = Items[index].Previous, Next = Items[index].Next };
|
|
||||||
|
|
||||||
if (index > 0)
|
|
||||||
{
|
|
||||||
Items[index - 1].Next = rangeItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index < Count - 1)
|
|
||||||
{
|
|
||||||
Items[index + 1].Previous = rangeItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
Items[index] = rangeItem;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates an item's end address on the list. Address must be the same.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="item">The RangeItem to be updated</param>
|
|
||||||
/// <returns>True if the item was located and updated, false otherwise</returns>
|
|
||||||
protected override bool Update(RangeItem<T> item)
|
|
||||||
{
|
|
||||||
int index = BinarySearch(item.Address);
|
|
||||||
|
|
||||||
RangeItem<T> rangeItem = new(item.Value) { Previous = item.Previous, Next = item.Next };
|
|
||||||
|
|
||||||
if (index > 0)
|
|
||||||
{
|
|
||||||
Items[index - 1].Next = rangeItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index < Count - 1)
|
|
||||||
{
|
|
||||||
Items[index + 1].Previous = rangeItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
Items[index] = rangeItem;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private void Insert(int index, RangeItem<T> item)
|
|
||||||
{
|
|
||||||
Debug.Assert(item.Address != item.EndAddress);
|
|
||||||
|
|
||||||
if (Count + 1 > Items.Length)
|
if (Count + 1 > Items.Length)
|
||||||
{
|
{
|
||||||
Array.Resize(ref Items, Items.Length + BackingGrowthSize);
|
Array.Resize(ref Items, (int)(Items.Length * 1.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index >= Count)
|
if (index >= Count)
|
||||||
@@ -145,8 +80,6 @@ namespace Ryujinx.Memory.Range
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private void RemoveAt(int index)
|
private void RemoveAt(int index)
|
||||||
{
|
{
|
||||||
_rangeItemPool.Release(Items[index]);
|
|
||||||
|
|
||||||
if (index < Count - 1)
|
if (index < Count - 1)
|
||||||
{
|
{
|
||||||
Items[index + 1].Previous = index > 0 ? Items[index - 1] : null;
|
Items[index + 1].Previous = index > 0 ? Items[index - 1] : null;
|
||||||
@@ -173,7 +106,7 @@ namespace Ryujinx.Memory.Range
|
|||||||
{
|
{
|
||||||
int index = BinarySearch(item.Address);
|
int index = BinarySearch(item.Address);
|
||||||
|
|
||||||
if (index >= 0 && Items[index].Value.Equals(item))
|
if (index >= 0 && Items[index] == item)
|
||||||
{
|
{
|
||||||
RemoveAt(index);
|
RemoveAt(index);
|
||||||
|
|
||||||
@@ -188,7 +121,7 @@ namespace Ryujinx.Memory.Range
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="startItem">The first item in the range of items to be removed</param>
|
/// <param name="startItem">The first item in the range of items to be removed</param>
|
||||||
/// <param name="endItem">The last item in the range of items to be removed</param>
|
/// <param name="endItem">The last item in the range of items to be removed</param>
|
||||||
public override void RemoveRange(RangeItem<T> startItem, RangeItem<T> endItem)
|
public override void RemoveRange(T startItem, T endItem)
|
||||||
{
|
{
|
||||||
if (startItem is null)
|
if (startItem is null)
|
||||||
{
|
{
|
||||||
@@ -197,7 +130,7 @@ namespace Ryujinx.Memory.Range
|
|||||||
|
|
||||||
if (startItem == endItem)
|
if (startItem == endItem)
|
||||||
{
|
{
|
||||||
Remove(startItem.Value);
|
Remove(startItem);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,42 +162,45 @@ namespace Ryujinx.Memory.Range
|
|||||||
/// <param name="size">Size of the range</param>
|
/// <param name="size">Size of the range</param>
|
||||||
public void RemoveRange(ulong address, ulong size)
|
public void RemoveRange(ulong address, ulong size)
|
||||||
{
|
{
|
||||||
int startIndex = BinarySearchLeftEdge(address, address + size);
|
(int startIndex, int endIndex) = BinarySearchEdges(address, address + size);
|
||||||
|
|
||||||
if (startIndex < 0)
|
if (startIndex < 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int endIndex = startIndex;
|
if (startIndex == endIndex - 1)
|
||||||
|
|
||||||
while (Items[endIndex] is not null && Items[endIndex].Address < address + size)
|
|
||||||
{
|
{
|
||||||
if (endIndex == Count - 1)
|
RemoveAt(startIndex);
|
||||||
{
|
return;
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
endIndex++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endIndex < Count - 1)
|
RemoveRangeInternal(startIndex, endIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a range of items from the item list
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index">Start index of the range</param>
|
||||||
|
/// <param name="endIndex">End index of the range (exclusive)</param>
|
||||||
|
private void RemoveRangeInternal(int index, int endIndex)
|
||||||
|
{
|
||||||
|
if (endIndex < Count)
|
||||||
{
|
{
|
||||||
Items[endIndex + 1].Previous = startIndex > 0 ? Items[startIndex - 1] : null;
|
Items[endIndex].Previous = index > 0 ? Items[index - 1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startIndex > 0)
|
if (index > 0)
|
||||||
{
|
{
|
||||||
Items[startIndex - 1].Next = endIndex < Count - 1 ? Items[endIndex + 1] : null;
|
Items[index - 1].Next = endIndex < Count ? Items[endIndex] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (endIndex < Count)
|
||||||
if (endIndex < Count - 1)
|
|
||||||
{
|
{
|
||||||
Array.Copy(Items, endIndex + 1, Items, startIndex, Count - endIndex - 1);
|
Array.Copy(Items, endIndex, Items, index, Count - endIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
Count -= endIndex - startIndex + 1;
|
Count -= endIndex - index;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -296,8 +232,8 @@ namespace Ryujinx.Memory.Range
|
|||||||
// So we need to return both the split 0-1 and 1-2 ranges.
|
// So we need to return both the split 0-1 and 1-2 ranges.
|
||||||
|
|
||||||
Lock.EnterWriteLock();
|
Lock.EnterWriteLock();
|
||||||
(RangeItem<T> first, RangeItem<T> last) = FindOverlapsAsNodes(address, size);
|
(T first, T last) = FindOverlapsAsNodes(address, size);
|
||||||
list = new List<T>();
|
list = [];
|
||||||
|
|
||||||
if (first is null)
|
if (first is null)
|
||||||
{
|
{
|
||||||
@@ -311,42 +247,41 @@ namespace Ryujinx.Memory.Range
|
|||||||
ulong lastAddress = address;
|
ulong lastAddress = address;
|
||||||
ulong endAddress = address + size;
|
ulong endAddress = address + size;
|
||||||
|
|
||||||
RangeItem<T> current = first;
|
T current = first;
|
||||||
while (last is not null && current is not null && current.Address < endAddress)
|
while (last is not null && current is not null && current.Address < endAddress)
|
||||||
{
|
{
|
||||||
T region = current.Value;
|
if (first == last && current.Address == address && current.Size == size)
|
||||||
if (first == last && region.Address == address && region.Size == size)
|
|
||||||
{
|
{
|
||||||
// Exact match, no splitting required.
|
// Exact match, no splitting required.
|
||||||
list.Add(region);
|
list.Add(current);
|
||||||
Lock.ExitWriteLock();
|
Lock.ExitWriteLock();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastAddress < region.Address)
|
if (lastAddress < current.Address)
|
||||||
{
|
{
|
||||||
// There is a gap between this region and the last. We need to fill it.
|
// There is a gap between this region and the last. We need to fill it.
|
||||||
T fillRegion = factory(lastAddress, region.Address - lastAddress);
|
T fillRegion = factory(lastAddress, current.Address - lastAddress);
|
||||||
list.Add(fillRegion);
|
list.Add(fillRegion);
|
||||||
Add(fillRegion);
|
Add(fillRegion);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (region.Address < address)
|
if (current.Address < address)
|
||||||
{
|
{
|
||||||
// Split the region around our base address and take the high half.
|
// Split the region around our base address and take the high half.
|
||||||
|
|
||||||
region = Split(region, address);
|
current = Split(current, address);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (region.EndAddress > address + size)
|
if (current.EndAddress > address + size)
|
||||||
{
|
{
|
||||||
// Split the region around our end address and take the low half.
|
// Split the region around our end address and take the low half.
|
||||||
|
|
||||||
Split(region, address + size);
|
Split(current, address + size);
|
||||||
}
|
}
|
||||||
|
|
||||||
list.Add(region);
|
list.Add(current);
|
||||||
lastAddress = region.EndAddress;
|
lastAddress = current.EndAddress;
|
||||||
current = current.Next;
|
current = current.Next;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +309,6 @@ namespace Ryujinx.Memory.Range
|
|||||||
private T Split(T region, ulong splitAddress)
|
private T Split(T region, ulong splitAddress)
|
||||||
{
|
{
|
||||||
T newRegion = (T)region.Split(splitAddress);
|
T newRegion = (T)region.Split(splitAddress);
|
||||||
Update(region);
|
|
||||||
Add(newRegion);
|
Add(newRegion);
|
||||||
return newRegion;
|
return newRegion;
|
||||||
}
|
}
|
||||||
@@ -386,16 +320,11 @@ namespace Ryujinx.Memory.Range
|
|||||||
/// <param name="size">Size in bytes of the range</param>
|
/// <param name="size">Size in bytes of the range</param>
|
||||||
/// <returns>The leftmost overlapping item, or null if none is found</returns>
|
/// <returns>The leftmost overlapping item, or null if none is found</returns>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public override RangeItem<T> FindOverlap(ulong address, ulong size)
|
public override T FindOverlap(ulong address, ulong size)
|
||||||
{
|
{
|
||||||
int index = BinarySearchLeftEdge(address, address + size);
|
int index = BinarySearchLeftEdge(address, address + size);
|
||||||
|
|
||||||
if (index < 0)
|
return index < 0 ? null : Items[index];
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Items[index];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -405,16 +334,11 @@ namespace Ryujinx.Memory.Range
|
|||||||
/// <param name="size">Size in bytes of the range</param>
|
/// <param name="size">Size in bytes of the range</param>
|
||||||
/// <returns>The overlapping item, or null if none is found</returns>
|
/// <returns>The overlapping item, or null if none is found</returns>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public override RangeItem<T> FindOverlapFast(ulong address, ulong size)
|
public override T FindOverlapFast(ulong address, ulong size)
|
||||||
{
|
{
|
||||||
int index = BinarySearch(address, address + size);
|
int index = BinarySearch(address, address + size);
|
||||||
|
|
||||||
if (index < 0)
|
return index < 0 ? null : Items[index];
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Items[index];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -424,23 +348,18 @@ namespace Ryujinx.Memory.Range
|
|||||||
/// <param name="size">Size in bytes of the range</param>
|
/// <param name="size">Size in bytes of the range</param>
|
||||||
/// <returns>The first and last overlapping items, or null if none are found</returns>
|
/// <returns>The first and last overlapping items, or null if none are found</returns>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public (RangeItem<T>, RangeItem<T>) FindOverlapsAsNodes(ulong address, ulong size)
|
public (T, T) FindOverlapsAsNodes(ulong address, ulong size)
|
||||||
{
|
{
|
||||||
(int index, int endIndex) = BinarySearchEdges(address, address + size);
|
(int index, int endIndex) = BinarySearchEdges(address, address + size);
|
||||||
|
|
||||||
if (index < 0)
|
return index < 0 ? (null, null) : (Items[index], Items[endIndex - 1]);
|
||||||
{
|
|
||||||
return (null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (Items[index], Items[endIndex - 1]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public RangeItem<T>[] FindOverlapsAsArray(ulong address, ulong size, out int length)
|
public T[] FindOverlapsAsArray(ulong address, ulong size, out int length)
|
||||||
{
|
{
|
||||||
(int index, int endIndex) = BinarySearchEdges(address, address + size);
|
(int index, int endIndex) = BinarySearchEdges(address, address + size);
|
||||||
|
|
||||||
RangeItem<T>[] result;
|
T[] result;
|
||||||
|
|
||||||
if (index < 0)
|
if (index < 0)
|
||||||
{
|
{
|
||||||
@@ -449,29 +368,20 @@ namespace Ryujinx.Memory.Range
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
result = ArrayPool<RangeItem<T>>.Shared.Rent(endIndex - index);
|
result = ArrayPool<T>.Shared.Rent(endIndex - index);
|
||||||
length = endIndex - index;
|
length = endIndex - index;
|
||||||
|
|
||||||
Array.Copy(Items, index, result, 0, endIndex - index);
|
Items.AsSpan(index, endIndex - index).CopyTo(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Span<RangeItem<T>> FindOverlapsAsSpan(ulong address, ulong size)
|
public ReadOnlySpan<T> FindOverlapsAsSpan(ulong address, ulong size)
|
||||||
{
|
{
|
||||||
(int index, int endIndex) = BinarySearchEdges(address, address + size);
|
(int index, int endIndex) = BinarySearchEdges(address, address + size);
|
||||||
|
|
||||||
Span<RangeItem<T>> result;
|
ReadOnlySpan<T> result = index < 0 ? [] : Items.AsSpan(index, endIndex - index);
|
||||||
|
|
||||||
if (index < 0)
|
|
||||||
{
|
|
||||||
result = [];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result = Items.AsSpan().Slice(index, endIndex - index);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -480,7 +390,7 @@ namespace Ryujinx.Memory.Range
|
|||||||
{
|
{
|
||||||
for (int i = 0; i < Count; i++)
|
for (int i = 0; i < Count; i++)
|
||||||
{
|
{
|
||||||
yield return Items[i].Value;
|
yield return Items[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ namespace Ryujinx.Memory.Range
|
|||||||
/// startIndex is inclusive.
|
/// startIndex is inclusive.
|
||||||
/// endIndex is exclusive.
|
/// endIndex is exclusive.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public readonly struct OverlapResult<T> where T : IRange
|
public readonly struct OverlapResult<T> where T : class, IRangeListRange<T>
|
||||||
{
|
{
|
||||||
public readonly int StartIndex = -1;
|
public readonly int StartIndex = -1;
|
||||||
public readonly int EndIndex = -1;
|
public readonly int EndIndex = -1;
|
||||||
public readonly RangeItem<T> QuickResult;
|
public readonly T QuickResult;
|
||||||
public int Count => EndIndex - StartIndex;
|
public int Count => EndIndex - StartIndex;
|
||||||
|
|
||||||
public OverlapResult(int startIndex, int endIndex, RangeItem<T> quickResult = null)
|
public OverlapResult(int startIndex, int endIndex, T quickResult = null)
|
||||||
{
|
{
|
||||||
this.StartIndex = startIndex;
|
this.StartIndex = startIndex;
|
||||||
this.EndIndex = endIndex;
|
this.EndIndex = endIndex;
|
||||||
@@ -33,7 +33,7 @@ namespace Ryujinx.Memory.Range
|
|||||||
/// Sorted list of ranges that supports binary search.
|
/// Sorted list of ranges that supports binary search.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">Type of the range.</typeparam>
|
/// <typeparam name="T">Type of the range.</typeparam>
|
||||||
public class RangeList<T> : RangeListBase<T> where T : IRange
|
public class RangeList<T> : RangeListBase<T> where T : class, IRangeListRange<T>
|
||||||
{
|
{
|
||||||
public readonly ReaderWriterLockSlim Lock = new();
|
public readonly ReaderWriterLockSlim Lock = new();
|
||||||
|
|
||||||
@@ -61,104 +61,6 @@ namespace Ryujinx.Memory.Range
|
|||||||
index = ~index;
|
index = ~index;
|
||||||
}
|
}
|
||||||
|
|
||||||
Insert(index, new RangeItem<T>(item));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates an item's end address on the list. Address must be the same.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="item">The item to be updated</param>
|
|
||||||
/// <returns>True if the item was located and updated, false otherwise</returns>
|
|
||||||
protected override bool Update(T item)
|
|
||||||
{
|
|
||||||
int index = BinarySearch(item.Address);
|
|
||||||
|
|
||||||
if (index >= 0)
|
|
||||||
{
|
|
||||||
while (index < Count)
|
|
||||||
{
|
|
||||||
if (Items[index].Value.Equals(item))
|
|
||||||
{
|
|
||||||
RangeItem<T> rangeItem = new(item) { Previous = Items[index].Previous, Next = Items[index].Next };
|
|
||||||
|
|
||||||
if (index > 0)
|
|
||||||
{
|
|
||||||
Items[index - 1].Next = rangeItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index < Count - 1)
|
|
||||||
{
|
|
||||||
Items[index + 1].Previous = rangeItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
Items[index] = rangeItem;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Items[index].Address > item.Address)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates an item's end address on the list. Address must be the same.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="item">The RangeItem to be updated</param>
|
|
||||||
/// <returns>True if the item was located and updated, false otherwise</returns>
|
|
||||||
protected override bool Update(RangeItem<T> item)
|
|
||||||
{
|
|
||||||
int index = BinarySearch(item.Address);
|
|
||||||
|
|
||||||
if (index >= 0)
|
|
||||||
{
|
|
||||||
while (index < Count)
|
|
||||||
{
|
|
||||||
if (Items[index].Equals(item))
|
|
||||||
{
|
|
||||||
RangeItem<T> rangeItem = new(item.Value) { Previous = item.Previous, Next = item.Next };
|
|
||||||
|
|
||||||
if (index > 0)
|
|
||||||
{
|
|
||||||
Items[index - 1].Next = rangeItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index < Count - 1)
|
|
||||||
{
|
|
||||||
Items[index + 1].Previous = rangeItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
Items[index] = rangeItem;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Items[index].Address > item.Address)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private void Insert(int index, RangeItem<T> item)
|
|
||||||
{
|
|
||||||
Debug.Assert(item.Address != item.EndAddress);
|
|
||||||
|
|
||||||
Debug.Assert(item.Address % 32 == 0);
|
|
||||||
|
|
||||||
if (Count + 1 > Items.Length)
|
if (Count + 1 > Items.Length)
|
||||||
{
|
{
|
||||||
Array.Resize(ref Items, Items.Length + BackingGrowthSize);
|
Array.Resize(ref Items, Items.Length + BackingGrowthSize);
|
||||||
@@ -220,7 +122,7 @@ namespace Ryujinx.Memory.Range
|
|||||||
/// <param name="startItem">The first item in the range of items to be removed</param>
|
/// <param name="startItem">The first item in the range of items to be removed</param>
|
||||||
/// <param name="endItem">The last item in the range of items to be removed</param>
|
/// <param name="endItem">The last item in the range of items to be removed</param>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public override void RemoveRange(RangeItem<T> startItem, RangeItem<T> endItem)
|
public override void RemoveRange(T startItem, T endItem)
|
||||||
{
|
{
|
||||||
if (startItem is null)
|
if (startItem is null)
|
||||||
{
|
{
|
||||||
@@ -229,30 +131,29 @@ namespace Ryujinx.Memory.Range
|
|||||||
|
|
||||||
if (startItem == endItem)
|
if (startItem == endItem)
|
||||||
{
|
{
|
||||||
Remove(startItem.Value);
|
Remove(startItem);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int startIndex = BinarySearch(startItem.Address);
|
(int index, int endIndex) = BinarySearchEdges(startItem.Address, endItem.EndAddress);
|
||||||
int endIndex = BinarySearch(endItem.Address);
|
|
||||||
|
|
||||||
if (endIndex < Count - 1)
|
if (endIndex < Count)
|
||||||
{
|
{
|
||||||
Items[endIndex + 1].Previous = startIndex > 0 ? Items[startIndex - 1] : null;
|
Items[endIndex].Previous = index > 0 ? Items[index - 1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startIndex > 0)
|
if (index > 0)
|
||||||
{
|
{
|
||||||
Items[startIndex - 1].Next = endIndex < Count - 1 ? Items[endIndex + 1] : null;
|
Items[index - 1].Next = endIndex < Count ? Items[endIndex] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (endIndex < Count - 1)
|
if (endIndex < Count)
|
||||||
{
|
{
|
||||||
Array.Copy(Items, endIndex + 1, Items, startIndex, Count - endIndex - 1);
|
Array.Copy(Items, endIndex, Items, index, Count - endIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
Count -= endIndex - startIndex + 1;
|
Count -= endIndex - index;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -268,7 +169,7 @@ namespace Ryujinx.Memory.Range
|
|||||||
{
|
{
|
||||||
while (index < Count)
|
while (index < Count)
|
||||||
{
|
{
|
||||||
if (Items[index].Value.Equals(item))
|
if (Items[index] == item)
|
||||||
{
|
{
|
||||||
RemoveAt(index);
|
RemoveAt(index);
|
||||||
|
|
||||||
@@ -298,7 +199,7 @@ namespace Ryujinx.Memory.Range
|
|||||||
/// <param name="size">Size in bytes of the range</param>
|
/// <param name="size">Size in bytes of the range</param>
|
||||||
/// <returns>The overlapping item, or the default value for the type if none found</returns>
|
/// <returns>The overlapping item, or the default value for the type if none found</returns>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public override RangeItem<T> FindOverlap(ulong address, ulong size)
|
public override T FindOverlap(ulong address, ulong size)
|
||||||
{
|
{
|
||||||
int index = BinarySearchLeftEdge(address, address + size);
|
int index = BinarySearchLeftEdge(address, address + size);
|
||||||
|
|
||||||
@@ -321,7 +222,7 @@ namespace Ryujinx.Memory.Range
|
|||||||
/// <param name="size">Size in bytes of the range</param>
|
/// <param name="size">Size in bytes of the range</param>
|
||||||
/// <returns>The overlapping item, or the default value for the type if none found</returns>
|
/// <returns>The overlapping item, or the default value for the type if none found</returns>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public override RangeItem<T> FindOverlapFast(ulong address, ulong size)
|
public override T FindOverlapFast(ulong address, ulong size)
|
||||||
{
|
{
|
||||||
int index = BinarySearch(address, address + size);
|
int index = BinarySearch(address, address + size);
|
||||||
|
|
||||||
@@ -340,7 +241,7 @@ namespace Ryujinx.Memory.Range
|
|||||||
/// <param name="size">Size in bytes of the range</param>
|
/// <param name="size">Size in bytes of the range</param>
|
||||||
/// <param name="output">Output array where matches will be written. It is automatically resized to fit the results</param>
|
/// <param name="output">Output array where matches will be written. It is automatically resized to fit the results</param>
|
||||||
/// <returns>Range information of overlapping items found</returns>
|
/// <returns>Range information of overlapping items found</returns>
|
||||||
private OverlapResult<T> FindOverlaps(ulong address, ulong size, ref RangeItem<T>[] output)
|
private OverlapResult<T> FindOverlaps(ulong address, ulong size, ref T[] output)
|
||||||
{
|
{
|
||||||
int outputCount = 0;
|
int outputCount = 0;
|
||||||
|
|
||||||
@@ -353,7 +254,7 @@ namespace Ryujinx.Memory.Range
|
|||||||
|
|
||||||
for (int i = startIndex; i < Count; i++)
|
for (int i = startIndex; i < Count; i++)
|
||||||
{
|
{
|
||||||
ref RangeItem<T> item = ref Items[i];
|
T item = Items[i];
|
||||||
|
|
||||||
if (item.Address >= endAddress)
|
if (item.Address >= endAddress)
|
||||||
{
|
{
|
||||||
@@ -398,7 +299,7 @@ namespace Ryujinx.Memory.Range
|
|||||||
{
|
{
|
||||||
for (int i = 0; i < Count; i++)
|
for (int i = 0; i < Count; i++)
|
||||||
{
|
{
|
||||||
yield return Items[i].Value;
|
yield return Items[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,22 @@
|
|||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Ryujinx.Memory.Range
|
namespace Ryujinx.Memory.Range
|
||||||
{
|
{
|
||||||
public class RangeItem<TValue> where TValue : IRange
|
public interface IRangeListRange<TValue> : IRange where TValue : class, IRangeListRange<TValue>
|
||||||
{
|
{
|
||||||
public RangeItem<TValue> Next;
|
public TValue Next { get; set; }
|
||||||
public RangeItem<TValue> Previous;
|
public TValue Previous { get; set; }
|
||||||
|
|
||||||
public ulong Address;
|
|
||||||
public ulong EndAddress;
|
|
||||||
|
|
||||||
public TValue Value;
|
|
||||||
|
|
||||||
public RangeItem()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public RangeItem(TValue value)
|
|
||||||
{
|
|
||||||
Address = value.Address;
|
|
||||||
EndAddress = value.Address + value.Size;
|
|
||||||
Value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public RangeItem<TValue> Set(TValue value)
|
|
||||||
{
|
|
||||||
Next = null;
|
|
||||||
Previous = null;
|
|
||||||
Address = value.Address;
|
|
||||||
EndAddress = value.Address + value.Size;
|
|
||||||
Value = value;
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public bool OverlapsWith(ulong address, ulong endAddress)
|
|
||||||
{
|
|
||||||
return Address < endAddress && address < EndAddress;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe abstract class RangeListBase<T> : IEnumerable<T> where T : IRange
|
public unsafe abstract class RangeListBase<T> : IEnumerable<T> where T : class, IRangeListRange<T>
|
||||||
{
|
{
|
||||||
protected static readonly ObjectPool<RangeItem<T>> _rangeItemPool = new(() => new RangeItem<T>());
|
|
||||||
private const int BackingInitialSize = 1024;
|
private const int BackingInitialSize = 1024;
|
||||||
|
|
||||||
protected RangeItem<T>[] Items;
|
protected T[] Items;
|
||||||
protected readonly int BackingGrowthSize;
|
protected readonly int BackingGrowthSize;
|
||||||
|
|
||||||
public int Count { get; protected set; }
|
public int Count { get; protected set; }
|
||||||
@@ -62,32 +28,18 @@ namespace Ryujinx.Memory.Range
|
|||||||
protected RangeListBase(int backingInitialSize = BackingInitialSize)
|
protected RangeListBase(int backingInitialSize = BackingInitialSize)
|
||||||
{
|
{
|
||||||
BackingGrowthSize = backingInitialSize;
|
BackingGrowthSize = backingInitialSize;
|
||||||
Items = new RangeItem<T>[backingInitialSize];
|
Items = new T[backingInitialSize];
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract void Add(T item);
|
public abstract void Add(T item);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates an item's end address on the list. Address must be the same.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="item">The item to be updated</param>
|
|
||||||
/// <returns>True if the item was located and updated, false otherwise</returns>
|
|
||||||
protected abstract bool Update(T item);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates an item's end address on the list. Address must be the same.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="item">The RangeItem to be updated</param>
|
|
||||||
/// <returns>True if the item was located and updated, false otherwise</returns>
|
|
||||||
protected abstract bool Update(RangeItem<T> item);
|
|
||||||
|
|
||||||
public abstract bool Remove(T item);
|
public abstract bool Remove(T item);
|
||||||
|
|
||||||
public abstract void RemoveRange(RangeItem<T> startItem, RangeItem<T> endItem);
|
public abstract void RemoveRange(T startItem, T endItem);
|
||||||
|
|
||||||
public abstract RangeItem<T> FindOverlap(ulong address, ulong size);
|
public abstract T FindOverlap(ulong address, ulong size);
|
||||||
|
|
||||||
public abstract RangeItem<T> FindOverlapFast(ulong address, ulong size);
|
public abstract T FindOverlapFast(ulong address, ulong size);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Performs binary search on the internal list of items.
|
/// Performs binary search on the internal list of items.
|
||||||
@@ -106,7 +58,7 @@ namespace Ryujinx.Memory.Range
|
|||||||
|
|
||||||
int middle = left + (range >> 1);
|
int middle = left + (range >> 1);
|
||||||
|
|
||||||
ref RangeItem<T> item = ref Items[middle];
|
T item = Items[middle];
|
||||||
|
|
||||||
if (item.Address == address)
|
if (item.Address == address)
|
||||||
{
|
{
|
||||||
@@ -144,7 +96,7 @@ namespace Ryujinx.Memory.Range
|
|||||||
|
|
||||||
int middle = left + (range >> 1);
|
int middle = left + (range >> 1);
|
||||||
|
|
||||||
ref RangeItem<T> item = ref Items[middle];
|
T item = Items[middle];
|
||||||
|
|
||||||
if (item.OverlapsWith(address, endAddress))
|
if (item.OverlapsWith(address, endAddress))
|
||||||
{
|
{
|
||||||
@@ -185,7 +137,7 @@ namespace Ryujinx.Memory.Range
|
|||||||
|
|
||||||
int middle = left + (range >> 1);
|
int middle = left + (range >> 1);
|
||||||
|
|
||||||
ref RangeItem<T> item = ref Items[middle];
|
T item = Items[middle];
|
||||||
|
|
||||||
bool match = item.OverlapsWith(address, endAddress);
|
bool match = item.OverlapsWith(address, endAddress);
|
||||||
|
|
||||||
@@ -237,7 +189,7 @@ namespace Ryujinx.Memory.Range
|
|||||||
|
|
||||||
int middle = right - (range >> 1);
|
int middle = right - (range >> 1);
|
||||||
|
|
||||||
ref RangeItem<T> item = ref Items[middle];
|
T item = Items[middle];
|
||||||
|
|
||||||
bool match = item.OverlapsWith(address, endAddress);
|
bool match = item.OverlapsWith(address, endAddress);
|
||||||
|
|
||||||
@@ -282,7 +234,7 @@ namespace Ryujinx.Memory.Range
|
|||||||
|
|
||||||
if (Count == 1)
|
if (Count == 1)
|
||||||
{
|
{
|
||||||
ref RangeItem<T> item = ref Items[0];
|
T item = Items[0];
|
||||||
|
|
||||||
if (item.OverlapsWith(address, endAddress))
|
if (item.OverlapsWith(address, endAddress))
|
||||||
{
|
{
|
||||||
@@ -312,7 +264,7 @@ namespace Ryujinx.Memory.Range
|
|||||||
|
|
||||||
int middle = left + (range >> 1);
|
int middle = left + (range >> 1);
|
||||||
|
|
||||||
ref RangeItem<T> item = ref Items[middle];
|
T item = Items[middle];
|
||||||
|
|
||||||
bool match = item.OverlapsWith(address, endAddress);
|
bool match = item.OverlapsWith(address, endAddress);
|
||||||
|
|
||||||
@@ -369,7 +321,7 @@ namespace Ryujinx.Memory.Range
|
|||||||
|
|
||||||
int middle = right - (range >> 1);
|
int middle = right - (range >> 1);
|
||||||
|
|
||||||
ref RangeItem<T> item = ref Items[middle];
|
T item = Items[middle];
|
||||||
|
|
||||||
bool match = item.OverlapsWith(address, endAddress);
|
bool match = item.OverlapsWith(address, endAddress);
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace Ryujinx.Memory.Tracking
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A region of memory.
|
/// A region of memory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
abstract class AbstractRegion : INonOverlappingRange
|
abstract class AbstractRegion<T> : INonOverlappingRange<T> where T : class, INonOverlappingRange<T>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base address.
|
/// Base address.
|
||||||
@@ -21,6 +21,9 @@ namespace Ryujinx.Memory.Tracking
|
|||||||
/// End address.
|
/// End address.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ulong EndAddress => Address + Size;
|
public ulong EndAddress => Address + Size;
|
||||||
|
|
||||||
|
public T Next { get; set; }
|
||||||
|
public T Previous { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new region.
|
/// Create a new region.
|
||||||
@@ -37,11 +40,11 @@ namespace Ryujinx.Memory.Tracking
|
|||||||
/// Check if this range overlaps with another.
|
/// Check if this range overlaps with another.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="address">Base address</param>
|
/// <param name="address">Base address</param>
|
||||||
/// <param name="size">Size of the range</param>
|
/// <param name="endAddress">End address</param>
|
||||||
/// <returns>True if overlapping, false otherwise</returns>
|
/// <returns>True if overlapping, false otherwise</returns>
|
||||||
public bool OverlapsWith(ulong address, ulong size)
|
public bool OverlapsWith(ulong address, ulong endAddress)
|
||||||
{
|
{
|
||||||
return Address < address + size && address < EndAddress;
|
return Address < endAddress && address < EndAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -68,6 +71,6 @@ namespace Ryujinx.Memory.Tracking
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="splitAddress">Address to split the region around</param>
|
/// <param name="splitAddress">Address to split the region around</param>
|
||||||
/// <returns>The second part of the split region, with start address at the given split.</returns>
|
/// <returns>The second part of the split region, with start address at the given split.</returns>
|
||||||
public abstract INonOverlappingRange Split(ulong splitAddress);
|
public abstract INonOverlappingRange<T> Split(ulong splitAddress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,10 +81,10 @@ namespace Ryujinx.Memory.Tracking
|
|||||||
{
|
{
|
||||||
NonOverlappingRangeList<VirtualRegion> regions = type == 0 ? _virtualRegions : _guestVirtualRegions;
|
NonOverlappingRangeList<VirtualRegion> regions = type == 0 ? _virtualRegions : _guestVirtualRegions;
|
||||||
regions.Lock.EnterReadLock();
|
regions.Lock.EnterReadLock();
|
||||||
Span<RangeItem<VirtualRegion>> overlaps = regions.FindOverlapsAsSpan(va, size);
|
ReadOnlySpan<VirtualRegion> overlaps = regions.FindOverlapsAsSpan(va, size);
|
||||||
for (int i = 0; i < overlaps.Length; i++)
|
for (int i = 0; i < overlaps.Length; i++)
|
||||||
{
|
{
|
||||||
VirtualRegion region = overlaps[i].Value;
|
VirtualRegion region = overlaps[i];
|
||||||
|
|
||||||
// If the region has been fully remapped, signal that it has been mapped again.
|
// If the region has been fully remapped, signal that it has been mapped again.
|
||||||
bool remapped = _memoryManager.IsRangeMapped(region.Address, region.Size);
|
bool remapped = _memoryManager.IsRangeMapped(region.Address, region.Size);
|
||||||
@@ -117,11 +117,11 @@ namespace Ryujinx.Memory.Tracking
|
|||||||
{
|
{
|
||||||
NonOverlappingRangeList<VirtualRegion> regions = type == 0 ? _virtualRegions : _guestVirtualRegions;
|
NonOverlappingRangeList<VirtualRegion> regions = type == 0 ? _virtualRegions : _guestVirtualRegions;
|
||||||
regions.Lock.EnterReadLock();
|
regions.Lock.EnterReadLock();
|
||||||
Span<RangeItem<VirtualRegion>> overlaps = regions.FindOverlapsAsSpan(va, size);
|
ReadOnlySpan<VirtualRegion> overlaps = regions.FindOverlapsAsSpan(va, size);
|
||||||
|
|
||||||
for (int i = 0; i < overlaps.Length; i++)
|
for (int i = 0; i < overlaps.Length; i++)
|
||||||
{
|
{
|
||||||
overlaps[i].Value.SignalMappingChanged(false);
|
overlaps[i].SignalMappingChanged(false);
|
||||||
}
|
}
|
||||||
regions.Lock.ExitReadLock();
|
regions.Lock.ExitReadLock();
|
||||||
}
|
}
|
||||||
@@ -301,7 +301,7 @@ namespace Ryujinx.Memory.Tracking
|
|||||||
|
|
||||||
// We use the non-span method here because keeping the lock will cause a deadlock.
|
// We use the non-span method here because keeping the lock will cause a deadlock.
|
||||||
regions.Lock.EnterReadLock();
|
regions.Lock.EnterReadLock();
|
||||||
RangeItem<VirtualRegion>[] overlaps = regions.FindOverlapsAsArray(address, size, out int length);
|
VirtualRegion[] overlaps = regions.FindOverlapsAsArray(address, size, out int length);
|
||||||
regions.Lock.ExitReadLock();
|
regions.Lock.ExitReadLock();
|
||||||
|
|
||||||
if (length == 0 && !precise)
|
if (length == 0 && !precise)
|
||||||
@@ -327,7 +327,7 @@ namespace Ryujinx.Memory.Tracking
|
|||||||
|
|
||||||
for (int i = 0; i < length; i++)
|
for (int i = 0; i < length; i++)
|
||||||
{
|
{
|
||||||
VirtualRegion region = overlaps[i].Value;
|
VirtualRegion region = overlaps[i];
|
||||||
|
|
||||||
if (precise)
|
if (precise)
|
||||||
{
|
{
|
||||||
@@ -341,7 +341,7 @@ namespace Ryujinx.Memory.Tracking
|
|||||||
|
|
||||||
if (length != 0)
|
if (length != 0)
|
||||||
{
|
{
|
||||||
ArrayPool<RangeItem<VirtualRegion>>.Shared.Return(overlaps);
|
ArrayPool<VirtualRegion>.Shared.Return(overlaps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace Ryujinx.Memory.Tracking
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A region of virtual memory.
|
/// A region of virtual memory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class VirtualRegion : AbstractRegion
|
class VirtualRegion : AbstractRegion<VirtualRegion>
|
||||||
{
|
{
|
||||||
public List<RegionHandle> Handles = [];
|
public List<RegionHandle> Handles = [];
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ namespace Ryujinx.Memory.Tracking
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override INonOverlappingRange Split(ulong splitAddress)
|
public override INonOverlappingRange<VirtualRegion> Split(ulong splitAddress)
|
||||||
{
|
{
|
||||||
VirtualRegion newRegion = new(_tracking, splitAddress, EndAddress - splitAddress, Guest, _lastPermission);
|
VirtualRegion newRegion = new(_tracking, splitAddress, EndAddress - splitAddress, Guest, _lastPermission);
|
||||||
Size = splitAddress - Address;
|
Size = splitAddress - Address;
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ namespace Ryujinx.Ava.Common
|
|||||||
{
|
{
|
||||||
Dispatcher.UIThread.Post(waitingDialog.Close);
|
Dispatcher.UIThread.Post(waitingDialog.Close);
|
||||||
|
|
||||||
NotificationHelper.ShowInformation(
|
RyujinxNotificationManager.ShowInformation(
|
||||||
RyujinxApp.FormatTitle(LocaleKeys.DialogNcaExtractionTitle),
|
RyujinxApp.FormatTitle(LocaleKeys.DialogNcaExtractionTitle),
|
||||||
$"{titleName}\n\n{LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage]}");
|
$"{titleName}\n\n{LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage]}");
|
||||||
}
|
}
|
||||||
@@ -380,7 +380,7 @@ namespace Ryujinx.Ava.Common
|
|||||||
{
|
{
|
||||||
Dispatcher.UIThread.Post(waitingDialog.Close);
|
Dispatcher.UIThread.Post(waitingDialog.Close);
|
||||||
|
|
||||||
NotificationHelper.ShowInformation(
|
RyujinxNotificationManager.ShowInformation(
|
||||||
RyujinxApp.FormatTitle(LocaleKeys.DialogNcaExtractionTitle),
|
RyujinxApp.FormatTitle(LocaleKeys.DialogNcaExtractionTitle),
|
||||||
$"{updateName}\n\n{LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage]}");
|
$"{updateName}\n\n{LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage]}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ namespace Ryujinx.Ava.Common.Locale
|
|||||||
{ LocaleKeys.RyujinxConfirm, [RyujinxApp.FullAppName] },
|
{ LocaleKeys.RyujinxConfirm, [RyujinxApp.FullAppName] },
|
||||||
{ LocaleKeys.RyujinxUpdater, [RyujinxApp.FullAppName] },
|
{ LocaleKeys.RyujinxUpdater, [RyujinxApp.FullAppName] },
|
||||||
{ LocaleKeys.RyujinxRebooter, [RyujinxApp.FullAppName] },
|
{ LocaleKeys.RyujinxRebooter, [RyujinxApp.FullAppName] },
|
||||||
|
{ LocaleKeys.SetupWizardGameDirsPageDescription, [RyujinxApp.FullAppName] },
|
||||||
{ LocaleKeys.CompatibilityListSearchBoxWatermarkWithCount, [CompatibilityDatabase.Entries.Length] },
|
{ LocaleKeys.CompatibilityListSearchBoxWatermarkWithCount, [CompatibilityDatabase.Entries.Length] },
|
||||||
{ LocaleKeys.CompatibilityListTitle, [CompatibilityDatabase.Entries.Length] }
|
{ LocaleKeys.CompatibilityListTitle, [CompatibilityDatabase.Entries.Length] }
|
||||||
});
|
});
|
||||||
|
|||||||
32
src/Ryujinx/Common/UIImages.cs
Normal file
32
src/Ryujinx/Common/UIImages.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using Avalonia.Media.Imaging;
|
||||||
|
using Avalonia.Platform;
|
||||||
|
using Gommon;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Common
|
||||||
|
{
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
// UiImages is ugly, so no
|
||||||
|
public static class UIImages
|
||||||
|
{
|
||||||
|
public const string LogoPathFormat = "resm:Ryujinx.Assets.UIImages.Logo_{0}_{1}.png?assembly=Ryujinx";
|
||||||
|
public const string IconPathFormat = "resm:Ryujinx.Assets.UIImages.Icon_{0}.png?assembly=Ryujinx";
|
||||||
|
|
||||||
|
public static Bitmap LoadBitmap(string uri)
|
||||||
|
=> new(AssetLoader.Open(new Uri(uri)));
|
||||||
|
|
||||||
|
public static Bitmap GetIconByName(string iconName)
|
||||||
|
=> LoadBitmap(IconPathFormat.Format(iconName));
|
||||||
|
|
||||||
|
public static Bitmap GetLogoByNameAndTheme(string iconName, bool isDarkTheme) =>
|
||||||
|
LoadBitmap(LogoPathFormat.Format(iconName,
|
||||||
|
isDarkTheme
|
||||||
|
? "Dark"
|
||||||
|
: "Light"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
public static Bitmap GetLogoByNameAndVariant(string iconName, string theme)
|
||||||
|
=> LoadBitmap(LogoPathFormat.Format(iconName, theme));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -156,12 +156,9 @@ namespace Ryujinx.Headless
|
|||||||
option.UserProfile = profile.Name;
|
option.UserProfile = profile.Name;
|
||||||
|
|
||||||
// Check if keys exists.
|
// Check if keys exists.
|
||||||
if (!File.Exists(Path.Combine(AppDataManager.KeysDirPath, "prod.keys")))
|
if (!File.Exists(Path.Combine(AppDataManager.GetKeysDir(), "prod.keys")))
|
||||||
{
|
{
|
||||||
if (!(AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile && File.Exists(Path.Combine(AppDataManager.KeysDirPathUser, "prod.keys"))))
|
Logger.Error?.Print(LogClass.Application, "Keys not found");
|
||||||
{
|
|
||||||
Logger.Error?.Print(LogClass.Application, "Keys not found");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ReloadConfig();
|
ReloadConfig();
|
||||||
|
|||||||
@@ -580,5 +580,10 @@ namespace Ryujinx.Headless
|
|||||||
{
|
{
|
||||||
return AccountSaveDataManager.GetLastUsedUser();
|
return AccountSaveDataManager.GetLastUsedUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void TakeScreenshot()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ using Ryujinx.Common.Configuration;
|
|||||||
using Ryujinx.Common.GraphicsDriver;
|
using Ryujinx.Common.GraphicsDriver;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Common.SystemInterop;
|
using Ryujinx.Common.SystemInterop;
|
||||||
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.Graphics.Vulkan.MoltenVK;
|
using Ryujinx.Graphics.Vulkan.MoltenVK;
|
||||||
using Ryujinx.Headless;
|
using Ryujinx.Headless;
|
||||||
using Ryujinx.SDL3.Common;
|
using Ryujinx.SDL3.Common;
|
||||||
@@ -31,6 +32,8 @@ namespace Ryujinx.Ava
|
|||||||
{
|
{
|
||||||
internal static class Program
|
internal static class Program
|
||||||
{
|
{
|
||||||
|
public static bool IsFirstStart { get; set; }
|
||||||
|
|
||||||
public static double WindowScaleFactor { get; set; }
|
public static double WindowScaleFactor { get; set; }
|
||||||
public static double DesktopScaleFactor { get; set; } = 1.0;
|
public static double DesktopScaleFactor { get; set; } = 1.0;
|
||||||
public static string Version { get; private set; }
|
public static string Version { get; private set; }
|
||||||
@@ -46,7 +49,7 @@ namespace Ryujinx.Ava
|
|||||||
public static int Main(string[] args)
|
public static int Main(string[] args)
|
||||||
{
|
{
|
||||||
Version = ReleaseInformation.Version;
|
Version = ReleaseInformation.Version;
|
||||||
|
|
||||||
if (OperatingSystem.IsWindows())
|
if (OperatingSystem.IsWindows())
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041))
|
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041))
|
||||||
@@ -55,8 +58,11 @@ namespace Ryujinx.Ava
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Environment.CurrentDirectory.StartsWithIgnoreCase("C:\\Program Files") ||
|
var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
||||||
Environment.CurrentDirectory.StartsWithIgnoreCase("C:\\Program Files (x86)"))
|
var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
|
||||||
|
|
||||||
|
if (Environment.CurrentDirectory.StartsWithIgnoreCase(programFiles) ||
|
||||||
|
Environment.CurrentDirectory.StartsWithIgnoreCase(programFilesX86))
|
||||||
{
|
{
|
||||||
_ = Win32NativeInterop.MessageBoxA(nint.Zero, "Ryujinx is not intended to be run from the Program Files folder. Please move it out and relaunch.", $"Ryujinx {Version}", MbIconwarning);
|
_ = Win32NativeInterop.MessageBoxA(nint.Zero, "Ryujinx is not intended to be run from the Program Files folder. Please move it out and relaunch.", $"Ryujinx {Version}", MbIconwarning);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -73,11 +79,23 @@ namespace Ryujinx.Ava
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool noGuiArg = ConsumeCommandLineArgument(ref args, "--no-gui") || ConsumeCommandLineArgument(ref args, "nogui");
|
||||||
|
bool coreDumpArg = ConsumeCommandLineArgument(ref args, "--core-dumps");
|
||||||
|
|
||||||
|
// TODO: Ryujinx causes core dumps on Linux when it exits "uncleanly", eg. through an unhandled exception.
|
||||||
|
// This is undesirable and causes very odd behavior during development (the process stops responding,
|
||||||
|
// the .NET debugger freezes or suddenly detaches, /tmp/ gets filled etc.), unless explicitly requested by the user.
|
||||||
|
// This needs to be investigated, but calling prctl() is better than modifying system-wide settings or leaving this be.
|
||||||
|
if (!coreDumpArg)
|
||||||
|
{
|
||||||
|
OsUtils.SetCoreDumpable(false);
|
||||||
|
}
|
||||||
|
|
||||||
PreviewerDetached = true;
|
PreviewerDetached = true;
|
||||||
|
|
||||||
if (args.Length > 0 && args[0] is "--no-gui" or "nogui")
|
if (noGuiArg)
|
||||||
{
|
{
|
||||||
HeadlessRyujinx.Entrypoint(args[1..]);
|
HeadlessRyujinx.Entrypoint(args);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +130,14 @@ namespace Ryujinx.Ava
|
|||||||
: [Win32RenderingMode.Software]
|
: [Win32RenderingMode.Software]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
private static bool ConsumeCommandLineArgument(ref string[] args, string targetArgument)
|
||||||
|
{
|
||||||
|
List<string> argList = [.. args];
|
||||||
|
bool found = argList.Remove(targetArgument);
|
||||||
|
args = argList.ToArray();
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
private static void Initialize(string[] args)
|
private static void Initialize(string[] args)
|
||||||
{
|
{
|
||||||
// Ensure Discord presence timestamp begins at the absolute start of when Ryujinx is launched
|
// Ensure Discord presence timestamp begins at the absolute start of when Ryujinx is launched
|
||||||
@@ -163,12 +189,9 @@ namespace Ryujinx.Ava
|
|||||||
DriverUtilities.InitDriverConfig(ConfigurationState.Instance.Graphics.BackendThreading == BackendThreading.Off);
|
DriverUtilities.InitDriverConfig(ConfigurationState.Instance.Graphics.BackendThreading == BackendThreading.Off);
|
||||||
|
|
||||||
// Check if keys exists.
|
// Check if keys exists.
|
||||||
if (!File.Exists(Path.Combine(AppDataManager.KeysDirPath, "prod.keys")))
|
if (!File.Exists(Path.Combine(AppDataManager.GetKeysDir(), "prod.keys")))
|
||||||
{
|
{
|
||||||
if (!(AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile && File.Exists(Path.Combine(AppDataManager.KeysDirPathUser, "prod.keys"))))
|
MainWindow.ShowKeyErrorOnLoad = true;
|
||||||
{
|
|
||||||
MainWindow.ShowKeyErrorOnLoad = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CommandLineState.LaunchPathArg != null)
|
if (CommandLineState.LaunchPathArg != null)
|
||||||
@@ -177,7 +200,6 @@ namespace Ryujinx.Ava
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static string GetDirGameUserConfig(string gameId, bool changeFolderForGame = false)
|
public static string GetDirGameUserConfig(string gameId, bool changeFolderForGame = false)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(gameId))
|
if (string.IsNullOrEmpty(gameId))
|
||||||
@@ -198,7 +220,6 @@ namespace Ryujinx.Ava
|
|||||||
|
|
||||||
public static void ReloadConfig(bool isRunGameWithCustomConfig = false)
|
public static void ReloadConfig(bool isRunGameWithCustomConfig = false)
|
||||||
{
|
{
|
||||||
|
|
||||||
string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName);
|
string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName);
|
||||||
string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName);
|
string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName);
|
||||||
|
|
||||||
@@ -224,6 +245,7 @@ namespace Ryujinx.Ava
|
|||||||
|
|
||||||
ConfigurationState.Instance.LoadDefault();
|
ConfigurationState.Instance.LoadDefault();
|
||||||
ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath);
|
ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath);
|
||||||
|
IsFirstStart = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -239,6 +261,8 @@ namespace Ryujinx.Ava
|
|||||||
|
|
||||||
ConfigurationFileFormat.RenameInvalidConfigFile(ConfigurationPath);
|
ConfigurationFileFormat.RenameInvalidConfigFile(ConfigurationPath);
|
||||||
|
|
||||||
|
IsFirstStart = true;
|
||||||
|
|
||||||
ConfigurationState.Instance.LoadDefault();
|
ConfigurationState.Instance.LoadDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -327,5 +327,10 @@ namespace Ryujinx.Ava.UI.Applet
|
|||||||
|
|
||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void TakeScreenshot()
|
||||||
|
{
|
||||||
|
_parent.ViewModel.AppHost.ScreenshotRequested = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Notifications;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Helpers
|
namespace Ryujinx.Ava.UI.Helpers
|
||||||
{
|
{
|
||||||
public static class ControlExtensions
|
public static class ControlExtensions
|
||||||
{
|
{
|
||||||
|
public static RyujinxNotificationManager CreateNotificationManager(
|
||||||
|
this Window window,
|
||||||
|
NotificationPosition visiblePosition = NotificationPosition.BottomRight,
|
||||||
|
int maxItems = RyujinxNotificationManager.MaxNotifications,
|
||||||
|
Thickness? margin = null
|
||||||
|
) => new(window, visiblePosition, maxItems, margin);
|
||||||
|
|
||||||
extension(Control ctrl)
|
extension(Control ctrl)
|
||||||
{
|
{
|
||||||
public int GridRow
|
public int GridRow
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Controls.Notifications;
|
|
||||||
using Avalonia.Threading;
|
|
||||||
using Ryujinx.Ava.Common.Locale;
|
|
||||||
using Ryujinx.Common;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Helpers
|
|
||||||
{
|
|
||||||
public static class NotificationHelper
|
|
||||||
{
|
|
||||||
private const int MaxNotifications = 4;
|
|
||||||
private const int NotificationDelayInMs = 5000;
|
|
||||||
|
|
||||||
private static WindowNotificationManager _notificationManager;
|
|
||||||
|
|
||||||
private static readonly BlockingCollection<Notification> _notifications = new();
|
|
||||||
|
|
||||||
public static void SetNotificationManager(Window host)
|
|
||||||
{
|
|
||||||
_notificationManager = new WindowNotificationManager(host)
|
|
||||||
{
|
|
||||||
Position = NotificationPosition.BottomRight,
|
|
||||||
MaxItems = MaxNotifications,
|
|
||||||
Margin = new Thickness(0, 0, 15, 40),
|
|
||||||
};
|
|
||||||
|
|
||||||
Lazy<AsyncWorkQueue<Notification>> maybeAsyncWorkQueue = new(
|
|
||||||
() => new AsyncWorkQueue<Notification>(notification =>
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
|
||||||
{
|
|
||||||
_notificationManager.Show(notification);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
"UI.NotificationThread",
|
|
||||||
_notifications),
|
|
||||||
LazyThreadSafetyMode.ExecutionAndPublication);
|
|
||||||
|
|
||||||
_notificationManager.TemplateApplied += (sender, args) =>
|
|
||||||
{
|
|
||||||
// NOTE: Force creation of the AsyncWorkQueue.
|
|
||||||
_ = maybeAsyncWorkQueue.Value;
|
|
||||||
};
|
|
||||||
|
|
||||||
host.Closing += (sender, args) =>
|
|
||||||
{
|
|
||||||
if (maybeAsyncWorkQueue.IsValueCreated)
|
|
||||||
{
|
|
||||||
maybeAsyncWorkQueue.Value.Dispose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Show(string title, string text, NotificationType type, bool waitingExit = false, Action onClick = null, Action onClose = null)
|
|
||||||
{
|
|
||||||
TimeSpan delay = waitingExit ? TimeSpan.FromMilliseconds(0) : TimeSpan.FromMilliseconds(NotificationDelayInMs);
|
|
||||||
|
|
||||||
_notifications.Add(new Notification(title, text, type, delay, onClick, onClose));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void ShowError(string message) =>
|
|
||||||
ShowError(
|
|
||||||
LocaleManager.Instance[LocaleKeys.DialogErrorTitle],
|
|
||||||
$"{LocaleManager.Instance[LocaleKeys.DialogErrorMessage]}\n\n{message}"
|
|
||||||
);
|
|
||||||
|
|
||||||
public static void ShowInformation(string title, string text, bool waitingExit = false, Action onClick = null, Action onClose = null) =>
|
|
||||||
Show(
|
|
||||||
title,
|
|
||||||
text,
|
|
||||||
NotificationType.Information,
|
|
||||||
waitingExit,
|
|
||||||
onClick,
|
|
||||||
onClose);
|
|
||||||
|
|
||||||
public static void ShowSuccess(string title, string text, bool waitingExit = false, Action onClick = null, Action onClose = null) =>
|
|
||||||
Show(
|
|
||||||
title,
|
|
||||||
text,
|
|
||||||
NotificationType.Success,
|
|
||||||
waitingExit,
|
|
||||||
onClick,
|
|
||||||
onClose);
|
|
||||||
|
|
||||||
public static void ShowWarning(string title, string text, bool waitingExit = false, Action onClick = null, Action onClose = null) =>
|
|
||||||
Show(
|
|
||||||
title,
|
|
||||||
text,
|
|
||||||
NotificationType.Warning,
|
|
||||||
waitingExit,
|
|
||||||
onClick,
|
|
||||||
onClose);
|
|
||||||
|
|
||||||
public static void ShowError(string title, string text, bool waitingExit = false, Action onClick = null, Action onClose = null) =>
|
|
||||||
Show(
|
|
||||||
title,
|
|
||||||
text,
|
|
||||||
NotificationType.Error,
|
|
||||||
waitingExit,
|
|
||||||
onClick,
|
|
||||||
onClose);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
179
src/Ryujinx/UI/Helpers/RyujinxNotificationManager.cs
Normal file
179
src/Ryujinx/UI/Helpers/RyujinxNotificationManager.cs
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Notifications;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Common;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Helpers
|
||||||
|
{
|
||||||
|
public class RyujinxNotificationManager
|
||||||
|
{
|
||||||
|
public static RyujinxNotificationManager Shared { get; set; }
|
||||||
|
|
||||||
|
public const int MaxNotifications = 4;
|
||||||
|
private const int NotificationDelayInMs = 5000;
|
||||||
|
|
||||||
|
private readonly WindowNotificationManager _notificationManager;
|
||||||
|
|
||||||
|
private readonly BlockingCollection<Notification> _notifications = new();
|
||||||
|
|
||||||
|
public RyujinxNotificationManager(Window host,
|
||||||
|
NotificationPosition visiblePosition = NotificationPosition.BottomRight,
|
||||||
|
int maxItems = MaxNotifications,
|
||||||
|
Thickness? margin = null)
|
||||||
|
{
|
||||||
|
_notificationManager = new WindowNotificationManager(host)
|
||||||
|
{
|
||||||
|
Position = visiblePosition,
|
||||||
|
MaxItems = maxItems,
|
||||||
|
Margin = margin ?? new Thickness(0, 0, 15, 40)
|
||||||
|
};
|
||||||
|
|
||||||
|
Lazy<AsyncWorkQueue<Notification>> maybeAsyncWorkQueue = new(
|
||||||
|
() => new AsyncWorkQueue<Notification>(notification =>
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
_notificationManager.Show(notification);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
"UI.NotificationThread",
|
||||||
|
_notifications),
|
||||||
|
LazyThreadSafetyMode.ExecutionAndPublication);
|
||||||
|
|
||||||
|
_notificationManager.TemplateApplied += (sender, args) =>
|
||||||
|
{
|
||||||
|
// NOTE: Force creation of the AsyncWorkQueue.
|
||||||
|
_ = maybeAsyncWorkQueue.Value;
|
||||||
|
};
|
||||||
|
|
||||||
|
host.Closing += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (maybeAsyncWorkQueue.IsValueCreated)
|
||||||
|
{
|
||||||
|
maybeAsyncWorkQueue.Value.Dispose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Show(string title, string text, NotificationType type, bool waitingExit = false,
|
||||||
|
Action onClick = null, Action onClose = null)
|
||||||
|
=> Shared?.Send(title, text, type, waitingExit, onClick, onClose);
|
||||||
|
|
||||||
|
public void Send(string title, string text, NotificationType type, bool waitingExit = false,
|
||||||
|
Action onClick = null, Action onClose = null)
|
||||||
|
{
|
||||||
|
TimeSpan delay = waitingExit
|
||||||
|
? TimeSpan.FromMilliseconds(0)
|
||||||
|
: TimeSpan.FromMilliseconds(NotificationDelayInMs);
|
||||||
|
|
||||||
|
_notifications.Add(new Notification(title, text, type, delay, onClick, onClose));
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Instance notification senders
|
||||||
|
|
||||||
|
public void Information(string title, string text, bool waitingExit = false, Action onClick = null,
|
||||||
|
Action onClose = null) =>
|
||||||
|
Send(
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
NotificationType.Information,
|
||||||
|
waitingExit,
|
||||||
|
onClick,
|
||||||
|
onClose);
|
||||||
|
|
||||||
|
public void Success(string title, string text, bool waitingExit = false, Action onClick = null,
|
||||||
|
Action onClose = null) =>
|
||||||
|
Send(
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
NotificationType.Success,
|
||||||
|
waitingExit,
|
||||||
|
onClick,
|
||||||
|
onClose);
|
||||||
|
|
||||||
|
public void Warning(string title, string text, bool waitingExit = false, Action onClick = null,
|
||||||
|
Action onClose = null) =>
|
||||||
|
Send(
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
NotificationType.Warning,
|
||||||
|
waitingExit,
|
||||||
|
onClick,
|
||||||
|
onClose);
|
||||||
|
|
||||||
|
public void Error(string title, string text, bool waitingExit = false, Action onClick = null,
|
||||||
|
Action onClose = null) =>
|
||||||
|
Send(
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
NotificationType.Error,
|
||||||
|
waitingExit,
|
||||||
|
onClick,
|
||||||
|
onClose);
|
||||||
|
|
||||||
|
public void Error(string message, bool waitingExit = false) =>
|
||||||
|
Error(
|
||||||
|
LocaleManager.Instance[LocaleKeys.DialogErrorTitle],
|
||||||
|
$"{LocaleManager.Instance[LocaleKeys.DialogErrorMessage]}\n\n{message}",
|
||||||
|
waitingExit: waitingExit
|
||||||
|
);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Static notification senders
|
||||||
|
|
||||||
|
public static void ShowInformation(string title, string text, bool waitingExit = false, Action onClick = null,
|
||||||
|
Action onClose = null) =>
|
||||||
|
Show(
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
NotificationType.Information,
|
||||||
|
waitingExit,
|
||||||
|
onClick,
|
||||||
|
onClose);
|
||||||
|
|
||||||
|
public static void ShowSuccess(string title, string text, bool waitingExit = false, Action onClick = null,
|
||||||
|
Action onClose = null) =>
|
||||||
|
Show(
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
NotificationType.Success,
|
||||||
|
waitingExit,
|
||||||
|
onClick,
|
||||||
|
onClose);
|
||||||
|
|
||||||
|
public static void ShowWarning(string title, string text, bool waitingExit = false, Action onClick = null,
|
||||||
|
Action onClose = null) =>
|
||||||
|
Show(
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
NotificationType.Warning,
|
||||||
|
waitingExit,
|
||||||
|
onClick,
|
||||||
|
onClose);
|
||||||
|
|
||||||
|
public static void ShowError(string title, string text, bool waitingExit = false, Action onClick = null,
|
||||||
|
Action onClose = null) =>
|
||||||
|
Show(
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
NotificationType.Error,
|
||||||
|
waitingExit,
|
||||||
|
onClick,
|
||||||
|
onClose);
|
||||||
|
|
||||||
|
public static void ShowError(string message, bool waitingExit = false) =>
|
||||||
|
ShowError(
|
||||||
|
LocaleManager.Instance[LocaleKeys.DialogErrorTitle],
|
||||||
|
$"{LocaleManager.Instance[LocaleKeys.DialogErrorMessage]}\n\n{message}",
|
||||||
|
waitingExit: waitingExit
|
||||||
|
);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
172
src/Ryujinx/UI/Models/FirmwareAvatarCache.cs
Normal file
172
src/Ryujinx/UI/Models/FirmwareAvatarCache.cs
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
using LibHac.Common;
|
||||||
|
using LibHac.Fs;
|
||||||
|
using LibHac.Fs.Fsa;
|
||||||
|
using LibHac.FsSystem;
|
||||||
|
using LibHac.Ncm;
|
||||||
|
using LibHac.Tools.Fs;
|
||||||
|
using LibHac.Tools.FsSystem;
|
||||||
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using SkiaSharp;
|
||||||
|
using System;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Models
|
||||||
|
{
|
||||||
|
public class FirmwareAvatarCache : BaseModel, IReadOnlyDictionary<string, byte[]>
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, byte[]> _backing = new();
|
||||||
|
|
||||||
|
public FirmwareAvatarCache(ContentManager contentManager, VirtualFileSystem virtualFileSystem)
|
||||||
|
{
|
||||||
|
string contentPath = contentManager.GetInstalledContentPath(0x010000000000080A, StorageId.BuiltInSystem, NcaContentType.Data);
|
||||||
|
string avatarPath = VirtualFileSystem.SwitchPathToSystemPath(contentPath);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(avatarPath))
|
||||||
|
{
|
||||||
|
using IStorage ncaFileStream = new LocalStorage(avatarPath, FileAccess.Read, FileMode.Open);
|
||||||
|
|
||||||
|
Nca nca = new(virtualFileSystem.KeySet, ncaFileStream);
|
||||||
|
IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
|
||||||
|
|
||||||
|
foreach (DirectoryEntryEx item in romfs.EnumerateEntries())
|
||||||
|
{
|
||||||
|
// TODO: Parse DatabaseInfo.bin and table.bin files for more accuracy.
|
||||||
|
if (item.Type == DirectoryEntryType.File && item.FullPath.Contains("chara") && item.FullPath.Contains("szs"))
|
||||||
|
{
|
||||||
|
using UniqueRef<IFile> file = new();
|
||||||
|
|
||||||
|
romfs.OpenFile(ref file.Ref, ("/" + item.FullPath).ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
|
|
||||||
|
using MemoryStream stream = new();
|
||||||
|
using MemoryStream streamPng = new();
|
||||||
|
|
||||||
|
file.Get.AsStream().CopyTo(stream);
|
||||||
|
|
||||||
|
stream.Position = 0;
|
||||||
|
|
||||||
|
SKImage avatarImage = SKImage.FromPixelCopy(new SKImageInfo(256, 256, SKColorType.Rgba8888, SKAlphaType.Premul), DecompressYaz0(stream));
|
||||||
|
|
||||||
|
using (SKData data = avatarImage.Encode(SKEncodedImageFormat.Png, 100))
|
||||||
|
{
|
||||||
|
data.SaveTo(streamPng);
|
||||||
|
}
|
||||||
|
|
||||||
|
_backing[item.FullPath] = streamPng.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ProfileImageModel> CreateProfileImageModels()
|
||||||
|
=> this.Select(x => new ProfileImageModel(x.Key, x.Value));
|
||||||
|
|
||||||
|
private static byte[] DecompressYaz0(MemoryStream stream)
|
||||||
|
{
|
||||||
|
using BinaryReader reader = new(stream);
|
||||||
|
|
||||||
|
reader.ReadInt32(); // Magic
|
||||||
|
|
||||||
|
uint decodedLength = BinaryPrimitives.ReverseEndianness(reader.ReadUInt32());
|
||||||
|
|
||||||
|
reader.ReadInt64(); // Padding
|
||||||
|
|
||||||
|
byte[] input = new byte[stream.Length - stream.Position];
|
||||||
|
stream.ReadExactly(input, 0, input.Length);
|
||||||
|
|
||||||
|
uint inputOffset = 0;
|
||||||
|
|
||||||
|
byte[] output = new byte[decodedLength];
|
||||||
|
uint outputOffset = 0;
|
||||||
|
|
||||||
|
ushort mask = 0;
|
||||||
|
byte header = 0;
|
||||||
|
|
||||||
|
while (outputOffset < decodedLength)
|
||||||
|
{
|
||||||
|
if ((mask >>= 1) == 0)
|
||||||
|
{
|
||||||
|
header = input[inputOffset++];
|
||||||
|
mask = 0x80;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((header & mask) != 0)
|
||||||
|
{
|
||||||
|
if (outputOffset == output.Length)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
output[outputOffset++] = input[inputOffset++];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
byte byte1 = input[inputOffset++];
|
||||||
|
byte byte2 = input[inputOffset++];
|
||||||
|
|
||||||
|
uint dist = (uint)((byte1 & 0xF) << 8) | byte2;
|
||||||
|
uint position = outputOffset - (dist + 1);
|
||||||
|
|
||||||
|
uint length = (uint)byte1 >> 4;
|
||||||
|
if (length == 0)
|
||||||
|
{
|
||||||
|
length = (uint)input[inputOffset++] + 0x12;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
length += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint gap = outputOffset - position;
|
||||||
|
uint nonOverlappingLength = length;
|
||||||
|
|
||||||
|
if (nonOverlappingLength > gap)
|
||||||
|
{
|
||||||
|
nonOverlappingLength = gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
Buffer.BlockCopy(output, (int)position, output, (int)outputOffset, (int)nonOverlappingLength);
|
||||||
|
outputOffset += nonOverlappingLength;
|
||||||
|
position += nonOverlappingLength;
|
||||||
|
length -= nonOverlappingLength;
|
||||||
|
|
||||||
|
while (length-- > 0)
|
||||||
|
{
|
||||||
|
output[outputOffset++] = output[position++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region dictionary impl
|
||||||
|
|
||||||
|
IEnumerator<KeyValuePair<string, byte[]>> IEnumerable<KeyValuePair<string, byte[]>>.GetEnumerator()
|
||||||
|
{
|
||||||
|
return (_backing as IEnumerable<KeyValuePair<string, byte[]>>).GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator GetEnumerator()
|
||||||
|
{
|
||||||
|
return ((IEnumerable)_backing).GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count => _backing.Count;
|
||||||
|
public bool ContainsKey(string key) => _backing.ContainsKey(key);
|
||||||
|
|
||||||
|
public bool TryGetValue(string key, out byte[] value) => _backing.TryGetValue(key, out value);
|
||||||
|
|
||||||
|
public byte[] this[string key] => _backing[key];
|
||||||
|
|
||||||
|
public IEnumerable<string> Keys => _backing.Keys;
|
||||||
|
public IEnumerable<byte[]> Values => _backing.Values;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ using Ryujinx.Ava.UI.Windows;
|
|||||||
using Ryujinx.Ava.Utilities;
|
using Ryujinx.Ava.Utilities;
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.Ava.UI.SetupWizard;
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||||
|
xmlns:pages="clr-namespace:Ryujinx.Ava.UI.SetupWizard.Pages"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:DataType="pages:SetupFinishedPageContext"
|
||||||
|
x:Class="Ryujinx.Ava.UI.SetupWizard.Pages.SetupFinishedPage">
|
||||||
|
<Grid
|
||||||
|
ColumnDefinitions="*"
|
||||||
|
RowDefinitions="*,Auto"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
HorizontalAlignment="Stretch">
|
||||||
|
<Border
|
||||||
|
Margin="15"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
CornerRadius="5"
|
||||||
|
Background="{DynamicResource AppListBackgroundColor}">
|
||||||
|
<TextBlock Margin="15" Text="{ext:Locale SetupWizardFinalPageDescription}" TextAlignment="Center" TextWrapping="Wrap" />
|
||||||
|
</Border>
|
||||||
|
<Button Grid.Row="1"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
MinWidth="45"
|
||||||
|
MinHeight="32"
|
||||||
|
Padding="8"
|
||||||
|
Background="Transparent"
|
||||||
|
Click="Button_OnClick"
|
||||||
|
CornerRadius="5"
|
||||||
|
Tag="https://discord.gg/PEuzjrFXUA"
|
||||||
|
ToolTip.Tip="{ext:Locale AboutDiscordUrlTooltipMessage}">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||||
|
<Image Source="{Binding OwningWizard.DiscordLogo}" />
|
||||||
|
<TextBlock Text="Discord"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Ryujinx.Ava.UI.Controls;
|
||||||
|
using Ryujinx.Common.Helper;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.SetupWizard.Pages
|
||||||
|
{
|
||||||
|
public partial class SetupFinishedPage : RyujinxControl<SetupFinishedPageContext>
|
||||||
|
{
|
||||||
|
public SetupFinishedPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Button_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is Button { Tag: string url })
|
||||||
|
OpenHelper.OpenUrl(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using Gommon;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.SetupWizard.Pages
|
||||||
|
{
|
||||||
|
public class SetupFinishedPageContext() : SetupWizardPageContext(LocaleKeys.SetupWizardFinalPageTitle)
|
||||||
|
{
|
||||||
|
public override LocaleKeys ActionContent => LocaleKeys.SetupWizardFinalPageAction;
|
||||||
|
|
||||||
|
// informative step; this implementation is not called.
|
||||||
|
public override Result CompleteStep() => Result.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/Ryujinx/UI/SetupWizard/Pages/Fw/SetupFirmwarePage.axaml
Normal file
27
src/Ryujinx/UI/SetupWizard/Pages/Fw/SetupFirmwarePage.axaml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||||
|
xmlns:pages="clr-namespace:Ryujinx.Ava.UI.SetupWizard.Pages"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:DataType="pages:SetupFirmwarePageContext"
|
||||||
|
x:Class="Ryujinx.Ava.UI.SetupWizard.Pages.SetupFirmwarePage">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="{ext:Locale SetupWizardFirmwarePageDescription}"/>
|
||||||
|
<Grid ColumnDefinitions="*" RowDefinitions="*,Auto">
|
||||||
|
<TextBox Name="FirmwarePathField" Margin="0, 10, 0, 5" Text="{Binding FirmwareSourcePath}" IsReadOnly="True" />
|
||||||
|
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="3.5">
|
||||||
|
<Button
|
||||||
|
Content="{ext:Locale SetupWizardFirmwarePageFolderBrowse}"
|
||||||
|
Command="{Binding BrowseFolderCommand}"
|
||||||
|
CommandParameter="{Binding #FirmwarePathField}"/>
|
||||||
|
<Button
|
||||||
|
Content="{ext:Locale SetupWizardFirmwarePageFileBrowse}"
|
||||||
|
Command="{Binding BrowseFileCommand}"
|
||||||
|
CommandParameter="{Binding #FirmwarePathField}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Ryujinx.Ava.UI.Controls;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.SetupWizard.Pages
|
||||||
|
{
|
||||||
|
public partial class SetupFirmwarePage : RyujinxControl<SetupFirmwarePageContext>
|
||||||
|
{
|
||||||
|
public SetupFirmwarePage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
167
src/Ryujinx/UI/SetupWizard/Pages/Fw/SetupFirmwarePageContext.cs
Normal file
167
src/Ryujinx/UI/SetupWizard/Pages/Fw/SetupFirmwarePageContext.cs
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using Gommon;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
|
using Ryujinx.Ava.Utilities;
|
||||||
|
using Ryujinx.Common;
|
||||||
|
using Ryujinx.Common.Configuration;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.SetupWizard.Pages
|
||||||
|
{
|
||||||
|
public partial class SetupFirmwarePageContext() : SetupWizardPageContext(LocaleKeys.SetupWizardFirmwarePageTitle)
|
||||||
|
{
|
||||||
|
[ObservableProperty] public partial string FirmwareSourcePath { get; set; }
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private static async Task BrowseFile(TextBox tb)
|
||||||
|
{
|
||||||
|
Optional<IStorageFile> result =
|
||||||
|
await RyujinxApp.MainWindow.ViewModel.StorageProvider.OpenSingleFilePickerAsync(
|
||||||
|
new FilePickerOpenOptions
|
||||||
|
{
|
||||||
|
Title = LocaleManager.Instance[LocaleKeys.SetupWizardFirmwarePageFilePopupTitle],
|
||||||
|
FileTypeFilter = new List<FilePickerFileType>
|
||||||
|
{
|
||||||
|
new(LocaleManager.Instance[LocaleKeys.FileDialogAllTypes])
|
||||||
|
{
|
||||||
|
Patterns = ["*.xci", "*.zip"],
|
||||||
|
AppleUniformTypeIdentifiers = ["com.ryujinx.xci", "public.zip-archive"],
|
||||||
|
MimeTypes = ["application/x-nx-xci", "application/zip"],
|
||||||
|
},
|
||||||
|
new("XCI")
|
||||||
|
{
|
||||||
|
Patterns = ["*.xci"],
|
||||||
|
AppleUniformTypeIdentifiers = ["com.ryujinx.xci"],
|
||||||
|
MimeTypes = ["application/x-nx-xci"],
|
||||||
|
},
|
||||||
|
new("ZIP")
|
||||||
|
{
|
||||||
|
Patterns = ["*.zip"],
|
||||||
|
AppleUniformTypeIdentifiers = ["public.zip-archive"],
|
||||||
|
MimeTypes = ["application/zip"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.TryGet(out IStorageFile firmwareFile))
|
||||||
|
{
|
||||||
|
tb.Text = firmwareFile.TryGetLocalPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private static async Task BrowseFolder(TextBox tb)
|
||||||
|
{
|
||||||
|
Optional<IStorageFolder> result =
|
||||||
|
await RyujinxApp.MainWindow.ViewModel.StorageProvider.OpenSingleFolderPickerAsync(
|
||||||
|
new FolderPickerOpenOptions
|
||||||
|
{
|
||||||
|
Title = LocaleManager.Instance[LocaleKeys.SetupWizardFirmwarePageFolderPopupTitle]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.TryGet(out IStorageFolder firmwareFolder))
|
||||||
|
{
|
||||||
|
tb.Text = firmwareFolder.TryGetLocalPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object CreateHelpContent()
|
||||||
|
{
|
||||||
|
Grid grid = new()
|
||||||
|
{
|
||||||
|
RowDefinitions = [new(GridLength.Auto), new(GridLength.Auto)],
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center
|
||||||
|
};
|
||||||
|
|
||||||
|
grid.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = LocaleManager.Instance[LocaleKeys.SetupWizardFirmwarePageHelpText],
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
GridRow = 0
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.Children.Add(new HyperlinkButton
|
||||||
|
{
|
||||||
|
Content = LocaleManager.Instance[LocaleKeys.SetupWizardHelpLinkButton],
|
||||||
|
NavigateUri = new Uri(SharedConstants.DumpFirmwareWikiUrl),
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
GridRow = 1
|
||||||
|
});
|
||||||
|
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Result CompleteStep()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(FirmwareSourcePath) && RyujinxSetupWizard.HasFirmware)
|
||||||
|
{
|
||||||
|
NotificationManager.Information(
|
||||||
|
title: LocaleManager.Instance[LocaleKeys.RyujinxInfo],
|
||||||
|
text: LocaleManager.GetFormatted(
|
||||||
|
LocaleKeys.SetupWizardFirmwarePageSkipText,
|
||||||
|
LocaleManager.Instance[LocaleKeys.SetupWizardActionBack]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return Result.Success; // This handles the user selecting no file/dir and just hitting Next.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Directory.Exists(FirmwareSourcePath))
|
||||||
|
return Result.Fail;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RyujinxApp.MainWindow.ContentManager.InstallFirmware(FirmwareSourcePath);
|
||||||
|
SystemVersion installedFwVer = RyujinxApp.MainWindow.ContentManager.GetCurrentFirmwareVersion();
|
||||||
|
if (installedFwVer != null)
|
||||||
|
{
|
||||||
|
NotificationManager.Information(
|
||||||
|
LocaleManager.Instance[LocaleKeys.SetupWizardFirmwarePageInstallSuccessNotificationTitle],
|
||||||
|
LocaleManager.GetFormatted(
|
||||||
|
LocaleKeys.SetupWizardFirmwarePageInstallSuccessNotificationTitle,
|
||||||
|
installedFwVer.VersionString
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
NotificationManager.Error(
|
||||||
|
LocaleManager.Instance[LocaleKeys.SetupWizardFirmwarePageInstallFailNotificationTitle],
|
||||||
|
LocaleManager.GetFormatted(
|
||||||
|
LocaleKeys.SetupWizardFirmwarePageInstallFailNotificationText,
|
||||||
|
FirmwareSourcePath
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
RyujinxApp.MainWindow.ViewModel.RefreshFirmwareStatus(installedFwVer, allowNullVersion: true);
|
||||||
|
|
||||||
|
// Purge Applet Cache.
|
||||||
|
|
||||||
|
DirectoryInfo miiEditorCacheFolder = new(
|
||||||
|
Path.Combine(AppDataManager.GamesDirPath, "0100000000001009", "cache")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (miiEditorCacheFolder.Exists)
|
||||||
|
{
|
||||||
|
miiEditorCacheFolder.Delete(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
NotificationManager.Error(e.Message, waitingExit: true);
|
||||||
|
return Result.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||||
|
xmlns:pages="clr-namespace:Ryujinx.UI.SetupWizard.Pages"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Ryujinx.UI.SetupWizard.Pages.SetupGameDirsPage"
|
||||||
|
x:DataType="pages:SetupGameDirsPageContext">
|
||||||
|
<StackPanel
|
||||||
|
Margin="10"
|
||||||
|
Spacing="10"
|
||||||
|
Orientation="Vertical" HorizontalAlignment="Stretch">
|
||||||
|
<TextBlock Foreground="{DynamicResource SecondaryTextColor}" Text="{ext:Locale SetupWizardGameDirsPageDescription}" />
|
||||||
|
<TextBlock Classes="h1" Text="{ext:Locale SettingsTabGeneralGameDirectories}" />
|
||||||
|
<StackPanel
|
||||||
|
Margin="10,0,0,0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Orientation="Vertical"
|
||||||
|
Spacing="10">
|
||||||
|
<ListBox
|
||||||
|
Name="GameDirsList"
|
||||||
|
MinHeight="120"
|
||||||
|
ItemsSource="{Binding GameDirs}">
|
||||||
|
<ListBox.Styles>
|
||||||
|
<Style Selector="ListBoxItem">
|
||||||
|
<Setter Property="Padding" Value="10" />
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ListBoxBackground}" />
|
||||||
|
</Style>
|
||||||
|
</ListBox.Styles>
|
||||||
|
</ListBox>
|
||||||
|
<Grid HorizontalAlignment="Stretch" ColumnDefinitions="*,Auto,Auto">
|
||||||
|
<TextBox
|
||||||
|
Name="GameDirPathBox"
|
||||||
|
Margin="0"
|
||||||
|
Watermark="{ext:Locale AddGameDirBoxTooltip}"
|
||||||
|
VerticalAlignment="Stretch" />
|
||||||
|
<Button
|
||||||
|
Name="AddGameDirButton"
|
||||||
|
Grid.Column="1"
|
||||||
|
MinWidth="90"
|
||||||
|
Margin="10,0,0,0">
|
||||||
|
<TextBlock HorizontalAlignment="Center"
|
||||||
|
Text="{ext:Locale SettingsTabGeneralAdd}" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
Name="RemoveGameDirButton"
|
||||||
|
Grid.Column="2"
|
||||||
|
MinWidth="90"
|
||||||
|
Margin="5,0,0,0"
|
||||||
|
Click="RemoveGameDirButton_OnClick">
|
||||||
|
<TextBlock HorizontalAlignment="Center"
|
||||||
|
Text="{ext:Locale SettingsTabGeneralRemove}" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
<Separator Height="1" />
|
||||||
|
<StackPanel Orientation="Vertical" Spacing="5">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Classes="h1" Text="{ext:Locale SettingsTabGeneralAutoloadDirectories}" />
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Foreground="{DynamicResource SecondaryTextColor}"
|
||||||
|
Text="{ext:Locale SettingsTabGeneralAutoloadNote}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel
|
||||||
|
Margin="10,0,0,0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Orientation="Vertical"
|
||||||
|
Spacing="10">
|
||||||
|
<ListBox
|
||||||
|
Name="AutoloadDirsList"
|
||||||
|
MinHeight="100"
|
||||||
|
ItemsSource="{Binding UpdateAndDlcDirs}">
|
||||||
|
<ListBox.Styles>
|
||||||
|
<Style Selector="ListBoxItem">
|
||||||
|
<Setter Property="Padding" Value="10" />
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ListBoxBackground}" />
|
||||||
|
</Style>
|
||||||
|
</ListBox.Styles>
|
||||||
|
</ListBox>
|
||||||
|
<Grid HorizontalAlignment="Stretch" ColumnDefinitions="*,Auto,Auto">
|
||||||
|
<TextBox
|
||||||
|
Name="AutoloadDirPathBox"
|
||||||
|
Margin="0"
|
||||||
|
Watermark="{ext:Locale AddGameDirBoxTooltip}"
|
||||||
|
VerticalAlignment="Stretch" />
|
||||||
|
<Button
|
||||||
|
Name="AddAutoloadDirButton"
|
||||||
|
Grid.Column="1"
|
||||||
|
MinWidth="90"
|
||||||
|
Margin="10,0,0,0">
|
||||||
|
<TextBlock HorizontalAlignment="Center"
|
||||||
|
Text="{ext:Locale SettingsTabGeneralAdd}" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
Name="RemoveAutoloadDirButton"
|
||||||
|
Grid.Column="2"
|
||||||
|
MinWidth="90"
|
||||||
|
Margin="5,0,0,0"
|
||||||
|
Click="RemoveAutoloadDirButton_OnClick">
|
||||||
|
<TextBlock HorizontalAlignment="Center"
|
||||||
|
Text="{ext:Locale SettingsTabGeneralRemove}" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using Avalonia.Collections;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
using Ryujinx.Ava;
|
||||||
|
using Ryujinx.Ava.UI.Controls;
|
||||||
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
|
using Ryujinx.Ava.Utilities;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ryujinx.UI.SetupWizard.Pages
|
||||||
|
{
|
||||||
|
public partial class SetupGameDirsPage : RyujinxControl<SetupGameDirsPageContext>
|
||||||
|
{
|
||||||
|
public SetupGameDirsPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
AddGameDirButton.Command =
|
||||||
|
Commands.Create(() => AddDirButton(GameDirPathBox, ViewModel.GameDirs));
|
||||||
|
AddAutoloadDirButton.Command =
|
||||||
|
Commands.Create(() => AddDirButton(AutoloadDirPathBox, ViewModel.UpdateAndDlcDirs));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddDirButton(TextBox addDirBox, ObservableCollection<string> directories)
|
||||||
|
{
|
||||||
|
string path = addDirBox.Text;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path) && !directories.Contains(path))
|
||||||
|
{
|
||||||
|
directories.Add(path);
|
||||||
|
|
||||||
|
addDirBox.Clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Gommon.Optional<IStorageFolder> folder = await RyujinxApp.MainWindow.ViewModel.StorageProvider.OpenSingleFolderPickerAsync();
|
||||||
|
|
||||||
|
if (folder.HasValue)
|
||||||
|
{
|
||||||
|
directories.Add(folder.Value.Path.LocalPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveGameDirButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
int oldIndex = GameDirsList.SelectedIndex;
|
||||||
|
|
||||||
|
foreach (string path in new List<string>(GameDirsList.SelectedItems.Cast<string>()))
|
||||||
|
{
|
||||||
|
ViewModel.GameDirs.Remove(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GameDirsList.ItemCount > 0)
|
||||||
|
{
|
||||||
|
GameDirsList.SelectedIndex = oldIndex < GameDirsList.ItemCount ? oldIndex : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveAutoloadDirButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
int oldIndex = AutoloadDirsList.SelectedIndex;
|
||||||
|
|
||||||
|
foreach (string path in new List<string>(AutoloadDirsList.SelectedItems.Cast<string>()))
|
||||||
|
{
|
||||||
|
ViewModel.UpdateAndDlcDirs.Remove(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AutoloadDirsList.ItemCount > 0)
|
||||||
|
{
|
||||||
|
AutoloadDirsList.SelectedIndex = oldIndex < AutoloadDirsList.ItemCount ? oldIndex : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using Gommon;
|
||||||
|
using Ryujinx.Ava;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Systems.Configuration;
|
||||||
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
|
using Ryujinx.Ava.UI.SetupWizard;
|
||||||
|
using Ryujinx.Common;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Ryujinx.UI.SetupWizard.Pages
|
||||||
|
{
|
||||||
|
public partial class SetupGameDirsPageContext() : SetupWizardPageContext(LocaleKeys.SetupWizardGameDirsPageTitle)
|
||||||
|
{
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial ObservableCollection<string> GameDirs { get; set; }
|
||||||
|
= new(ConfigurationState.Instance.UI.GameDirs);
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial ObservableCollection<string> UpdateAndDlcDirs { get; set; }
|
||||||
|
= new(ConfigurationState.Instance.UI.AutoloadDirs);
|
||||||
|
|
||||||
|
public override Result CompleteStep()
|
||||||
|
{
|
||||||
|
if (GameDirs.Count is 0)
|
||||||
|
{
|
||||||
|
NotificationManager.Error(LocaleManager.Instance[LocaleKeys.SetupWizardGameDirsPageNoFoldersSelectedError]);
|
||||||
|
return Result.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
OwningWizard.ModifyConfig(config =>
|
||||||
|
{
|
||||||
|
config.UI.GameDirs.Value = GameDirs.ToList();
|
||||||
|
config.UI.AutoloadDirs.Value = UpdateAndDlcDirs.ToList();
|
||||||
|
});
|
||||||
|
|
||||||
|
RyujinxApp.MainWindow.LoadApplications();
|
||||||
|
|
||||||
|
return Result.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object CreateHelpContent()
|
||||||
|
{
|
||||||
|
Grid grid = new()
|
||||||
|
{
|
||||||
|
RowDefinitions = [new(GridLength.Auto), new(GridLength.Auto)],
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center
|
||||||
|
};
|
||||||
|
|
||||||
|
grid.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = LocaleManager.Instance[LocaleKeys.SetupWizardGameDirsPageHelpText],
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
GridRow = 0
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.Children.Add(new HyperlinkButton
|
||||||
|
{
|
||||||
|
Content = LocaleManager.Instance[LocaleKeys.SetupWizardHelpLinkButton],
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
NavigateUri = new Uri(SharedConstants.DumpContentWikiUrl),
|
||||||
|
GridRow = 1
|
||||||
|
});
|
||||||
|
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/Ryujinx/UI/SetupWizard/Pages/Keys/SetupKeysPage.axaml
Normal file
22
src/Ryujinx/UI/SetupWizard/Pages/Keys/SetupKeysPage.axaml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:pages="clr-namespace:Ryujinx.Ava.UI.SetupWizard.Pages"
|
||||||
|
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Ryujinx.Ava.UI.SetupWizard.Pages.SetupKeysPage"
|
||||||
|
x:DataType="pages:SetupKeysPageContext">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="{ext:Locale SetupWizardKeysPageDescription}" Margin="0,0,0,10"/>
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<TextBox Name="KeysFolderPathField" Text="{Binding KeysFolderPath}" IsReadOnly="True" />
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Content="..."
|
||||||
|
Command="{Binding BrowseCommand}"
|
||||||
|
CommandParameter="{Binding #KeysFolderPathField}"
|
||||||
|
Margin="5,0,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
|
|
||||||
13
src/Ryujinx/UI/SetupWizard/Pages/Keys/SetupKeysPage.axaml.cs
Normal file
13
src/Ryujinx/UI/SetupWizard/Pages/Keys/SetupKeysPage.axaml.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using Ryujinx.Ava.UI.Controls;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.SetupWizard.Pages
|
||||||
|
{
|
||||||
|
public partial class SetupKeysPage : RyujinxControl<SetupKeysPageContext>
|
||||||
|
{
|
||||||
|
public SetupKeysPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
130
src/Ryujinx/UI/SetupWizard/Pages/Keys/SetupKeysPageContext.cs
Normal file
130
src/Ryujinx/UI/SetupWizard/Pages/Keys/SetupKeysPageContext.cs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using DynamicData;
|
||||||
|
using Gommon;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
|
using Ryujinx.Ava.Utilities;
|
||||||
|
using Ryujinx.Common;
|
||||||
|
using Ryujinx.Common.Configuration;
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.HLE.Exceptions;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.SetupWizard.Pages
|
||||||
|
{
|
||||||
|
public partial class SetupKeysPageContext() : SetupWizardPageContext(LocaleKeys.SetupWizardKeysPageTitle)
|
||||||
|
{
|
||||||
|
public override object CreateHelpContent()
|
||||||
|
{
|
||||||
|
Grid grid = new()
|
||||||
|
{
|
||||||
|
RowDefinitions = [new(GridLength.Auto), new(GridLength.Auto)],
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center
|
||||||
|
};
|
||||||
|
|
||||||
|
grid.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = LocaleManager.Instance[LocaleKeys.SetupWizardKeysPageHelpText],
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
GridRow = 0
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.Children.Add(new HyperlinkButton
|
||||||
|
{
|
||||||
|
Content = LocaleManager.Instance[LocaleKeys.SetupWizardHelpLinkButton],
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
NavigateUri = new Uri(SharedConstants.DumpKeysWikiUrl),
|
||||||
|
GridRow = 1
|
||||||
|
});
|
||||||
|
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ObservableProperty] public partial string KeysFolderPath { get; set; }
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private static async Task Browse(TextBox tb)
|
||||||
|
{
|
||||||
|
Optional<IStorageFolder> result =
|
||||||
|
await RyujinxApp.MainWindow.ViewModel.StorageProvider.OpenSingleFolderPickerAsync(
|
||||||
|
new FolderPickerOpenOptions
|
||||||
|
{
|
||||||
|
Title = LocaleManager.Instance[LocaleKeys.SetupWizardKeysPageFolderPopupTitle]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.TryGet(out IStorageFolder keyFolder))
|
||||||
|
{
|
||||||
|
tb.Text = keyFolder.TryGetLocalPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Result CompleteStep()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(KeysFolderPath) && RyujinxApp.MainWindow.VirtualFileSystem.HasKeySet)
|
||||||
|
{
|
||||||
|
NotificationManager.Information(
|
||||||
|
title: LocaleManager.Instance[LocaleKeys.RyujinxInfo],
|
||||||
|
text: LocaleManager.GetFormatted(
|
||||||
|
LocaleKeys.SetupWizardKeysPageSkipText,
|
||||||
|
LocaleManager.Instance[LocaleKeys.SetupWizardActionBack]
|
||||||
|
));
|
||||||
|
return Result.Success; // This handles the user selecting no folder and just hitting Next.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Directory.Exists(KeysFolderPath))
|
||||||
|
return Result.Fail;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.Info?.Print(LogClass.Application, $"Installing keys from {KeysFolderPath}");
|
||||||
|
|
||||||
|
ContentManager.InstallKeys(KeysFolderPath, AppDataManager.GetKeysDir());
|
||||||
|
|
||||||
|
NotificationManager.Information(
|
||||||
|
title: LocaleManager.Instance[LocaleKeys.RyujinxInfo],
|
||||||
|
text: LocaleManager.Instance[LocaleKeys.DialogKeysInstallerKeysInstallSuccessMessage]);
|
||||||
|
}
|
||||||
|
catch (InvalidFirmwarePackageException ifwpe)
|
||||||
|
{
|
||||||
|
NotificationManager.Error(ifwpe.Message, waitingExit: true);
|
||||||
|
return Result.Failure(NoKeysFoundInFolder.Shared);
|
||||||
|
}
|
||||||
|
catch (MissingKeyException ex)
|
||||||
|
{
|
||||||
|
NotificationManager.Error(ex.ToString(), waitingExit: true);
|
||||||
|
return Result.Failure(NoKeysFoundInFolder.Shared);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
string message = ex.Message;
|
||||||
|
if (ex is FormatException)
|
||||||
|
{
|
||||||
|
message = LocaleManager.Instance.UpdateAndGetDynamicValue(
|
||||||
|
LocaleKeys.DialogKeysInstallerKeysNotFoundErrorMessage, KeysFolderPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationManager.Error(message, waitingExit: true);
|
||||||
|
|
||||||
|
return Result.Failure(new MessageError(message));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
RyujinxApp.MainWindow.VirtualFileSystem.ReloadKeySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct NoKeysFoundInFolder : IErrorState
|
||||||
|
{
|
||||||
|
public static readonly NoKeysFoundInFolder Shared = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/Ryujinx/UI/SetupWizard/README.md
Normal file
3
src/Ryujinx/UI/SetupWizard/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Ryubing Setup Wizard
|
||||||
|
|
||||||
|
Directly modified from the code found [here](https://github.com/TKMM-Team/Tkmm/tree/master/src/Tkmm/Wizard).
|
||||||
82
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.Steps.cs
Normal file
82
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.Steps.cs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
using Ryujinx.Ava.UI.SetupWizard.Pages;
|
||||||
|
using Ryujinx.UI.SetupWizard.Pages;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.SetupWizard
|
||||||
|
{
|
||||||
|
public partial class RyujinxSetupWizard
|
||||||
|
{
|
||||||
|
private async ValueTask<bool> SetupKeys()
|
||||||
|
{
|
||||||
|
if (_overwrite || !RyujinxApp.MainWindow.VirtualFileSystem.HasKeySet)
|
||||||
|
{
|
||||||
|
Retry:
|
||||||
|
bool result = await NextPage<SetupKeysPage, SetupKeysPageContext>(out SetupKeysPageContext keyContext)
|
||||||
|
.Show();
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!keyContext.CompleteStep())
|
||||||
|
goto Retry;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<bool> SetupFirmware()
|
||||||
|
{
|
||||||
|
if (_overwrite || !HasFirmware)
|
||||||
|
{
|
||||||
|
if (!RyujinxApp.MainWindow.VirtualFileSystem.HasKeySet)
|
||||||
|
{
|
||||||
|
NotificationManager.Error("Keys still seem to not be installed. Please try again.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Retry:
|
||||||
|
bool result =
|
||||||
|
await NextPage<SetupFirmwarePage, SetupFirmwarePageContext>(out SetupFirmwarePageContext fwContext)
|
||||||
|
.Show();
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!fwContext.CompleteStep())
|
||||||
|
goto Retry;
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(HasFirmware));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<bool> SetupGameDirs()
|
||||||
|
{
|
||||||
|
|
||||||
|
if (!HasFirmware)
|
||||||
|
{
|
||||||
|
NotificationManager.Error("Firmware still seems to not be installed. Please try again.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Retry:
|
||||||
|
bool result =
|
||||||
|
await NextPage<SetupGameDirsPage, SetupGameDirsPageContext>(out SetupGameDirsPageContext gdContext)
|
||||||
|
.Show();
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!gdContext.CompleteStep())
|
||||||
|
goto Retry;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValueTask<bool> Finish()
|
||||||
|
=> NextPage<SetupFinishedPage, SetupFinishedPageContext>(out _)
|
||||||
|
.WithHelpButtonVisible(false)
|
||||||
|
.Show();
|
||||||
|
}
|
||||||
|
}
|
||||||
144
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.cs
Normal file
144
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.cs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Notifications;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
|
using Avalonia.Styling;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using Ryujinx.Ava.Common;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Systems.Configuration;
|
||||||
|
using Ryujinx.Ava.UI.Controls;
|
||||||
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.SetupWizard
|
||||||
|
{
|
||||||
|
public partial class RyujinxSetupWizard : BaseModel, IDisposable
|
||||||
|
{
|
||||||
|
private bool _configWasModified;
|
||||||
|
|
||||||
|
private readonly RyujinxSetupWizardWindow _window;
|
||||||
|
private readonly bool _overwrite;
|
||||||
|
|
||||||
|
public void SetWindowTitle(string titleText)
|
||||||
|
{
|
||||||
|
_window.Title = titleText;
|
||||||
|
ToolTip.SetTip(_window.RyuLogo, titleText);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RyujinxSetupWizard(RyujinxSetupWizardWindow wizardWindow, bool overwriteMode)
|
||||||
|
{
|
||||||
|
_window = wizardWindow;
|
||||||
|
_overwrite = overwriteMode;
|
||||||
|
|
||||||
|
if (Program.PreviewerDetached)
|
||||||
|
{
|
||||||
|
UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle);
|
||||||
|
RyujinxApp.ThemeChanged += Ryujinx_ThemeChanged;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UpdateLogoTheme("Dark");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SetupWizardPage FirstPage() => new(_window.WizardPresenter, this, isFirstPage: true);
|
||||||
|
|
||||||
|
private SetupWizardPage NextPage() => new(_window.WizardPresenter, this);
|
||||||
|
|
||||||
|
private SetupWizardPage NextPage<TControl, TContext>(out TContext boundContext)
|
||||||
|
where TControl : RyujinxControl<TContext>, new()
|
||||||
|
where TContext : SetupWizardPageContext, new()
|
||||||
|
=> NextPage()
|
||||||
|
.WithContent<TControl, TContext>(out boundContext)
|
||||||
|
.WithTitle(boundContext.Title)
|
||||||
|
.WithActionContent(boundContext.ActionContent);
|
||||||
|
|
||||||
|
public static bool HasFirmware => RyujinxApp.MainWindow.ContentManager.GetCurrentFirmwareVersion() != null;
|
||||||
|
|
||||||
|
public RyujinxNotificationManager NotificationManager { get; private set; }
|
||||||
|
|
||||||
|
internal void ModifyConfig(Action<ConfigurationState> modifier)
|
||||||
|
{
|
||||||
|
modifier(ConfigurationState.Instance);
|
||||||
|
_configWasModified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Start()
|
||||||
|
{
|
||||||
|
NotificationManager = _window.CreateNotificationManager(
|
||||||
|
// I wanted to do bottom center but that...literally just shows top center? Okay.
|
||||||
|
// Fuck it, weird window height hack to do it instead.
|
||||||
|
// 120 is not exact, just a random number. Looks fine though.
|
||||||
|
NotificationPosition.TopCenter,
|
||||||
|
margin: new Thickness(0, _window.Height - 135, 0, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
RyujinxSetupWizardWindow.IsOpen = true;
|
||||||
|
Start:
|
||||||
|
await FirstPage()
|
||||||
|
.WithTitle(LocaleKeys.SetupWizardFirstPageTitle)
|
||||||
|
.WithContent(LocaleKeys.SetupWizardFirstPageContent)
|
||||||
|
.WithActionContent(LocaleKeys.SetupWizardFirstPageAction)
|
||||||
|
.Show();
|
||||||
|
// result is unhandled as the first page cannot display anything other than the next button.
|
||||||
|
// back does not need to be handled
|
||||||
|
|
||||||
|
Keys:
|
||||||
|
if (!await SetupKeys())
|
||||||
|
goto Start;
|
||||||
|
|
||||||
|
Firmware:
|
||||||
|
if (!await SetupFirmware())
|
||||||
|
goto Keys;
|
||||||
|
|
||||||
|
GameDirs:
|
||||||
|
if (!await SetupGameDirs())
|
||||||
|
goto Firmware;
|
||||||
|
|
||||||
|
if (!await Finish())
|
||||||
|
goto GameDirs;
|
||||||
|
|
||||||
|
Return:
|
||||||
|
if (_configWasModified)
|
||||||
|
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.GlobalConfigurationPath);
|
||||||
|
|
||||||
|
NotificationManager = null;
|
||||||
|
_window.Close();
|
||||||
|
RyujinxSetupWizardWindow.IsOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Discord logo stuff
|
||||||
|
|
||||||
|
[ObservableProperty] public partial Bitmap DiscordLogo { get; set; }
|
||||||
|
|
||||||
|
private void Ryujinx_ThemeChanged()
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() => UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateLogoTheme(string theme)
|
||||||
|
{
|
||||||
|
bool isDarkTheme = theme == "Dark" ||
|
||||||
|
(theme == "Auto" && RyujinxApp.DetectSystemTheme() == ThemeVariant.Dark);
|
||||||
|
|
||||||
|
DiscordLogo = UIImages
|
||||||
|
.GetLogoByNameAndTheme("Discord", isDarkTheme)
|
||||||
|
.CreateScaledBitmap(new PixelSize(32, 24));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
RyujinxApp.ThemeChanged -= Ryujinx_ThemeChanged;
|
||||||
|
|
||||||
|
DiscordLogo.Dispose();
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizardWindow.axaml
Normal file
18
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizardWindow.axaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<windows:StyleableAppWindow xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:windows="clr-namespace:Ryujinx.Ava.UI.Windows"
|
||||||
|
xmlns:setupWizard="clr-namespace:Ryujinx.Ava.UI.SetupWizard"
|
||||||
|
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
|
||||||
|
CanResize="False"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Ryujinx.Ava.UI.SetupWizard.RyujinxSetupWizardWindow"
|
||||||
|
x:DataType="setupWizard:RyujinxSetupWizard">
|
||||||
|
<Grid VerticalAlignment="Stretch" HorizontalAlignment="Stretch" RowDefinitions="Auto,*">
|
||||||
|
<Grid Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left" Name="FlushControls">
|
||||||
|
<controls:RyujinxLogo Name="RyuLogo"/>
|
||||||
|
</Grid>
|
||||||
|
<ContentPresenter Grid.Row="1" Name="WizardPresenter"/>
|
||||||
|
</Grid>
|
||||||
|
</windows:StyleableAppWindow>
|
||||||
90
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizardWindow.axaml.cs
Normal file
90
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizardWindow.axaml.cs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Ryujinx.Ava.Systems.Configuration;
|
||||||
|
using Ryujinx.Ava.UI.Windows;
|
||||||
|
using Ryujinx.Common.Configuration;
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.SetupWizard
|
||||||
|
{
|
||||||
|
public partial class RyujinxSetupWizardWindow : StyleableAppWindow
|
||||||
|
{
|
||||||
|
public static bool IsOpen { get; set; }
|
||||||
|
|
||||||
|
public RyujinxSetupWizardWindow() : base(useCustomTitleBar: true)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
if (Program.PreviewerDetached)
|
||||||
|
{
|
||||||
|
FlushControls.IsVisible = !ConfigurationState.Instance.ShowOldUI;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Task ShowAsync(bool overwriteMode, Window owner = null)
|
||||||
|
{
|
||||||
|
if (!CanShowSetupWizard)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
Task windowTask = ShowAsync(
|
||||||
|
CreateWindow(out RyujinxSetupWizard wiz, overwriteMode),
|
||||||
|
owner
|
||||||
|
);
|
||||||
|
_ = wiz.Start();
|
||||||
|
return windowTask.ContinueWith(_ => wiz.Dispose());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RyujinxSetupWizardWindow CreateWindow(out RyujinxSetupWizard setupWizard, bool overwriteMode = false)
|
||||||
|
{
|
||||||
|
RyujinxSetupWizardWindow window = new();
|
||||||
|
window.DataContext = setupWizard = new RyujinxSetupWizard(window, overwriteMode);
|
||||||
|
window.Height = 700;
|
||||||
|
window.Width = 825;
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool CanShowSetupWizard =>
|
||||||
|
!File.Exists(Path.Combine(AppDataManager.BaseDirPath, ".DoNotShowSetupWizard"));
|
||||||
|
|
||||||
|
public static bool DisableSetupWizard()
|
||||||
|
{
|
||||||
|
if (!CanShowSetupWizard)
|
||||||
|
return false; //cannot disable; file exists, so it's already disabled.
|
||||||
|
|
||||||
|
string disableFile = Path.Combine(AppDataManager.BaseDirPath, ".DoNotShowSetupWizard");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Create(disableFile, 0).Dispose();
|
||||||
|
File.SetAttributes(disableFile, File.GetAttributes(disableFile) | FileAttributes.Hidden);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Error?.PrintStack(LogClass.Application, e.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool EnableSetupWizard()
|
||||||
|
{
|
||||||
|
if (CanShowSetupWizard)
|
||||||
|
return false; //cannot enable; file does not exist, so it's already enabled.
|
||||||
|
|
||||||
|
string disableFile = Path.Combine(AppDataManager.BaseDirPath, ".DoNotShowSetupWizard");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(disableFile);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Error?.PrintStack(LogClass.Application, e.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
src/Ryujinx/UI/SetupWizard/SetupWizardPage.Builder.cs
Normal file
94
src/Ryujinx/UI/SetupWizard/SetupWizardPage.Builder.cs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.UI.Controls;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.SetupWizard
|
||||||
|
{
|
||||||
|
public partial class SetupWizardPage
|
||||||
|
{
|
||||||
|
public SetupWizardPage WithTitle(LocaleKeys title) => WithTitle(LocaleManager.Instance[title]);
|
||||||
|
|
||||||
|
public SetupWizardPage WithTitle(string title)
|
||||||
|
{
|
||||||
|
Title = title;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SetupWizardPage WithContent(LocaleKeys content) => WithContent(LocaleManager.Instance[content]);
|
||||||
|
|
||||||
|
public SetupWizardPage WithContent(object? content)
|
||||||
|
{
|
||||||
|
if (content is StyledElement { Parent: ContentControl parent })
|
||||||
|
{
|
||||||
|
parent.Content = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Content = content;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SetupWizardPage WithHelpContent(LocaleKeys content) =>
|
||||||
|
WithHelpContent(LocaleManager.Instance[content]);
|
||||||
|
|
||||||
|
public SetupWizardPage WithHelpContent(object? content)
|
||||||
|
{
|
||||||
|
if (content is string str)
|
||||||
|
{
|
||||||
|
TextBlock tb = new()
|
||||||
|
{
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
TextAlignment = TextAlignment.Center,
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
FontSize = 20.0,
|
||||||
|
Text = str
|
||||||
|
};
|
||||||
|
|
||||||
|
tb.Classes.Add("h1");
|
||||||
|
|
||||||
|
content = tb;
|
||||||
|
}
|
||||||
|
|
||||||
|
HelpContent = content;
|
||||||
|
HasHelpContent = content != null;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SetupWizardPage WithContent<TControl>(object? context = null) where TControl : Control, new()
|
||||||
|
{
|
||||||
|
Content = new TControl { DataContext = context };
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SetupWizardPage WithContent<TControl, TContext>(out TContext boundContext)
|
||||||
|
where TControl : RyujinxControl<TContext>, new()
|
||||||
|
where TContext : SetupWizardPageContext, new()
|
||||||
|
{
|
||||||
|
boundContext = new() { OwningWizard = ownerWizard };
|
||||||
|
|
||||||
|
if (boundContext.CreateHelpContent() is { } content)
|
||||||
|
WithHelpContent(content);
|
||||||
|
|
||||||
|
return WithContent<TControl>(boundContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SetupWizardPage WithActionContent(LocaleKeys content) =>
|
||||||
|
WithActionContent(LocaleManager.Instance[content]);
|
||||||
|
|
||||||
|
public SetupWizardPage WithActionContent(object? content)
|
||||||
|
{
|
||||||
|
ActionContent = content;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SetupWizardPage WithHelpButtonVisible(bool visible)
|
||||||
|
{
|
||||||
|
ShowHelpButton = visible;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/Ryujinx/UI/SetupWizard/SetupWizardPage.cs
Normal file
67
src/Ryujinx/UI/SetupWizard/SetupWizardPage.cs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
using Avalonia.Controls.Presenters;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.SetupWizard
|
||||||
|
{
|
||||||
|
public partial class SetupWizardPage(
|
||||||
|
ContentPresenter contentPresenter,
|
||||||
|
RyujinxSetupWizard ownerWizard,
|
||||||
|
bool isFirstPage = false) : BaseModel
|
||||||
|
{
|
||||||
|
private bool? _result;
|
||||||
|
private readonly CancellationTokenSource _cts = new();
|
||||||
|
|
||||||
|
public bool IsFirstPage => isFirstPage;
|
||||||
|
|
||||||
|
public RyujinxSetupWizard Parent => ownerWizard;
|
||||||
|
|
||||||
|
[ObservableProperty] public partial string? Title { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty] public partial object? Content { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty] public partial object? HelpContent { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty] public partial bool HasHelpContent { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty] public partial bool ShowHelpButton { get; set; } = true;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial object? ActionContent { get; set; } = LocaleManager.Instance[LocaleKeys.SetupWizardActionNext];
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void MoveBack()
|
||||||
|
{
|
||||||
|
_result = false;
|
||||||
|
_cts.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void MoveNext()
|
||||||
|
{
|
||||||
|
_result = true;
|
||||||
|
_cts.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<bool> Show()
|
||||||
|
{
|
||||||
|
contentPresenter.Content = new SetupWizardPageView { ViewModel = this };
|
||||||
|
ownerWizard.SetWindowTitle(Title);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(-1, _cts.Token);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
return _result ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/Ryujinx/UI/SetupWizard/SetupWizardPageContext.cs
Normal file
35
src/Ryujinx/UI/SetupWizard/SetupWizardPageContext.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using Gommon;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
|
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.SetupWizard
|
||||||
|
{
|
||||||
|
public abstract class SetupWizardPageContext(LocaleKeys title) : BaseModel
|
||||||
|
{
|
||||||
|
public RyujinxSetupWizard OwningWizard
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
init
|
||||||
|
{
|
||||||
|
field = value;
|
||||||
|
NotificationManager = field.NotificationManager;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RyujinxNotificationManager NotificationManager { get; private init; }
|
||||||
|
|
||||||
|
public LocaleKeys Title => title;
|
||||||
|
|
||||||
|
public virtual LocaleKeys ActionContent => LocaleKeys.SetupWizardActionNext;
|
||||||
|
|
||||||
|
// ReSharper disable once UnusedMemberInSuper.Global
|
||||||
|
// it's used implicitly as we use this type as a where guard for generics for WithContent<TControl, TContext>,
|
||||||
|
// it also ensures all context types implement completion
|
||||||
|
public abstract Result CompleteStep();
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
public virtual object? CreateHelpContent() => null;
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/Ryujinx/UI/SetupWizard/SetupWizardPageView.axaml
Normal file
92
src/Ryujinx/UI/SetupWizard/SetupWizardPageView.axaml
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||||
|
xmlns:fa="using:Projektanker.Icons.Avalonia"
|
||||||
|
xmlns:wiz="using:Ryujinx.Ava.UI.SetupWizard"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:DataType="wiz:SetupWizardPage"
|
||||||
|
x:Class="Ryujinx.Ava.UI.SetupWizard.SetupWizardPageView">
|
||||||
|
<Grid RowDefinitions="*,Auto" Margin="60">
|
||||||
|
<ScrollViewer>
|
||||||
|
<Grid RowDefinitions="Auto,*">
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
TextWrapping="WrapWithOverflow"
|
||||||
|
FontSize="46"
|
||||||
|
Text="{Binding Title}" />
|
||||||
|
<ContentPresenter Grid.Row="1"
|
||||||
|
Content="{Binding}"
|
||||||
|
IsVisible="{Binding !#InfoToggle.IsChecked}"
|
||||||
|
TextWrapping="WrapWithOverflow"
|
||||||
|
Margin="0,15,0,0">
|
||||||
|
<ContentPresenter.DataTemplates>
|
||||||
|
<DataTemplate DataType="{x:Type wiz:SetupWizardPage}">
|
||||||
|
<ContentControl Content="{Binding Content}" VerticalAlignment="Stretch"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ContentPresenter.DataTemplates>
|
||||||
|
</ContentPresenter>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1"
|
||||||
|
ColumnDefinitions="*" RowDefinitions="*,Auto"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
IsVisible="{Binding #InfoToggle.IsChecked}">
|
||||||
|
<Border
|
||||||
|
Margin="15"
|
||||||
|
IsVisible="{Binding HasHelpContent}"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
ClipToBounds="True"
|
||||||
|
CornerRadius="5"
|
||||||
|
Background="{DynamicResource AppListBackgroundColor}">
|
||||||
|
<ContentPresenter Content="{Binding}"
|
||||||
|
Margin="5"
|
||||||
|
TextWrapping="WrapWithOverflow" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||||
|
<ContentPresenter.DataTemplates>
|
||||||
|
<DataTemplate DataType="{x:Type wiz:SetupWizardPage}">
|
||||||
|
<ContentControl Content="{Binding HelpContent}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ContentPresenter.DataTemplates>
|
||||||
|
</ContentPresenter>
|
||||||
|
</Border>
|
||||||
|
<Button Grid.Row="1"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
MinWidth="45"
|
||||||
|
MinHeight="32"
|
||||||
|
MaxWidth="45"
|
||||||
|
MaxHeight="32"
|
||||||
|
Padding="8"
|
||||||
|
Background="Transparent"
|
||||||
|
Click="Button_OnClick"
|
||||||
|
CornerRadius="5"
|
||||||
|
Tag="https://discord.gg/PEuzjrFXUA"
|
||||||
|
ToolTip.Tip="{ext:Locale AboutDiscordUrlTooltipMessage}">
|
||||||
|
<Image Source="{Binding Parent.DiscordLogo}" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="Auto,Auto,*" Grid.Row="1">
|
||||||
|
<ToggleButton Name="InfoToggle" Padding="6" IsVisible="{Binding ShowHelpButton}">
|
||||||
|
<fa:Icon Value="fa-solid fa-circle-info" />
|
||||||
|
</ToggleButton>
|
||||||
|
|
||||||
|
<Button IsVisible="{Binding !IsFirstPage}"
|
||||||
|
Grid.Column="1"
|
||||||
|
Content="{ext:Locale SetupWizardActionBack}"
|
||||||
|
Margin="10,0,0,0"
|
||||||
|
Command="{Binding MoveBackCommand}" />
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Grid.Column="2">
|
||||||
|
<Button Content="{Binding ActionContent}"
|
||||||
|
Command="{Binding MoveNextCommand}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user