diff options
149 files changed, 5298 insertions, 677 deletions
diff --git a/.ci/scripts/linux/docker.sh b/.ci/scripts/linux/docker.sh index f538a4081..090ca75f1 100644 --- a/.ci/scripts/linux/docker.sh +++ b/.ci/scripts/linux/docker.sh @@ -5,10 +5,11 @@ cd /yuzu ccache -s mkdir build || true && cd build -cmake .. -G Ninja -DYUZU_USE_BUNDLED_UNICORN=ON -DYUZU_USE_QT_WEB_ENGINE=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=/usr/lib/ccache/gcc -DCMAKE_CXX_COMPILER=/usr/lib/ccache/g++ -DYUZU_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON +cmake .. -G Ninja -DDISPLAY_VERSION=$1 -DYUZU_USE_BUNDLED_UNICORN=ON -DYUZU_USE_QT_WEB_ENGINE=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=/usr/lib/ccache/gcc -DCMAKE_CXX_COMPILER=/usr/lib/ccache/g++ -DYUZU_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON ninja ccache -s -ctest -VV -C Release +# Ignore zlib's tests, since they aren't gated behind a CMake option. +ctest -VV -E "(example|example64)" -C Release diff --git a/.ci/scripts/linux/exec.sh b/.ci/scripts/linux/exec.sh index a5a6c34b9..9fafa9208 100644 --- a/.ci/scripts/linux/exec.sh +++ b/.ci/scripts/linux/exec.sh @@ -2,4 +2,4 @@ mkdir -p "ccache" || true chmod a+x ./.ci/scripts/linux/docker.sh -docker run -e ENABLE_COMPATIBILITY_REPORTING -e CCACHE_DIR=/yuzu/ccache -v $(pwd):/yuzu yuzuemu/build-environments:linux-fresh /bin/bash /yuzu/.ci/scripts/linux/docker.sh +docker run -e ENABLE_COMPATIBILITY_REPORTING -e CCACHE_DIR=/yuzu/ccache -v $(pwd):/yuzu yuzuemu/build-environments:linux-fresh /bin/bash /yuzu/.ci/scripts/linux/docker.sh $1 diff --git a/.ci/scripts/merge/apply-patches-by-label-private.py b/.ci/scripts/merge/apply-patches-by-label-private.py index 11ec60010..fe0acd510 100644 --- a/.ci/scripts/merge/apply-patches-by-label-private.py +++ b/.ci/scripts/merge/apply-patches-by-label-private.py @@ -1,41 +1,45 @@ # Download all pull requests as patches that match a specific label # Usage: python download-patches-by-label.py <Label to Match> <Root Path Folder to DL to> -import requests, sys, json, urllib3.request, shutil, subprocess, os, traceback +import requests, sys, json, shutil, subprocess, os, traceback -org = os.getenv("PrivateMergeOrg".upper(), "yuzu-emu") -repo = os.getenv("PrivateMergeRepo".upper(), "yuzu-private") -tagline = os.getenv("MergeTaglinePrivate".upper(), "") +org = os.getenv("PRIVATEMERGEORG", "yuzu-emu") +repo = os.getenv("PRIVATEMERGEREPO", "yuzu-private") +tagline = sys.argv[3] user = sys.argv[1] -http = urllib3.PoolManager() dl_list = {} +TAG_NAME = sys.argv[2] + def check_individual(repo_id, pr_id): url = 'https://%sdev.azure.com/%s/%s/_apis/git/repositories/%s/pullRequests/%s/labels?api-version=5.1-preview.1' % (user, org, repo, repo_id, pr_id) response = requests.get(url) if (response.ok): - j = json.loads(response.content) - for tg in j['value']: - if (tg['name'] == sys.argv[2]): - return True + try: + js = response.json() + return any(tag.get('name') == TAG_NAME for tag in js['value']) + except: + return False return False -try: +def merge_pr(pn, ref): + print("Matched PR# %s" % pn) + print(subprocess.check_output(["git", "fetch", "https://%sdev.azure.com/%s/_git/%s" % (user, org, repo), ref, "-f"])) + print(subprocess.check_output(["git", "merge", "--squash", 'origin/' + ref.replace('refs/heads/','')])) + print(subprocess.check_output(["git", "commit", "-m\"Merge %s PR %s\"" % (tagline, pn)])) + +def main(): url = 'https://%sdev.azure.com/%s/%s/_apis/git/pullrequests?api-version=5.1' % (user, org, repo) response = requests.get(url) if (response.ok): - j = json.loads(response.content) - for pr in j["value"]: - repo_id = pr['repository']['id'] - pr_id = pr['pullRequestId'] - if (check_individual(repo_id, pr_id)): - pn = pr_id - ref = pr['sourceRefName'] - print("Matched PR# %s" % pn) - print(subprocess.check_output(["git", "fetch", "https://%sdev.azure.com/%s/_git/%s" % (user, org, repo), ref, "-f"])) - print(subprocess.check_output(["git", "merge", "--squash", 'origin/' + ref.replace('refs/heads/','')])) - print(subprocess.check_output(["git", "commit", "-m\"Merge %s PR %s\"" % (tagline, pn)])) -except: - traceback.print_exc(file=sys.stdout) - sys.exit(-1) + js = response.json() + tagged_prs = filter(lambda pr: check_individual(pr['repository']['id'], pr['pullRequestId']), js['value']) + map(lambda pr: merge_pr(pr['pullRequestId'], pr['sourceRefName']), tagged_prs) + +if __name__ == '__main__': + try: + main() + except: + traceback.print_exc(file=sys.stdout) + sys.exit(-1) diff --git a/.ci/scripts/merge/apply-patches-by-label.py b/.ci/scripts/merge/apply-patches-by-label.py index 7f1ea06cf..43ed74d7f 100644 --- a/.ci/scripts/merge/apply-patches-by-label.py +++ b/.ci/scripts/merge/apply-patches-by-label.py @@ -3,7 +3,7 @@ import requests, sys, json, urllib3.request, shutil, subprocess, os -tagline = os.getenv("MergeTaglinePublic".upper(), "") +tagline = sys.argv[2] http = urllib3.PoolManager() dl_list = {} @@ -14,11 +14,13 @@ def check_individual(labels): return True return False -try: - url = 'https://api.github.com/repos/yuzu-emu/yuzu/pulls' +def do_page(page): + url = 'https://api.github.com/repos/yuzu-emu/yuzu/pulls?page=%s' % page response = requests.get(url) if (response.ok): j = json.loads(response.content) + if j == []: + return for pr in j: if (check_individual(pr["labels"])): pn = pr["number"] @@ -26,5 +28,9 @@ try: print(subprocess.check_output(["git", "fetch", "https://github.com/yuzu-emu/yuzu.git", "pull/%s/head:pr-%s" % (pn, pn), "-f"])) print(subprocess.check_output(["git", "merge", "--squash", "pr-%s" % pn])) print(subprocess.check_output(["git", "commit", "-m\"Merge %s PR %s\"" % (tagline, pn)])) + +try: + for i in range(1,30): + do_page(i) except: sys.exit(-1) diff --git a/.ci/scripts/windows/docker.sh b/.ci/scripts/windows/docker.sh index f7093363b..e8f26933a 100644 --- a/.ci/scripts/windows/docker.sh +++ b/.ci/scripts/windows/docker.sh @@ -13,7 +13,7 @@ echo '' >> /bin/cmd chmod +x /bin/cmd mkdir build || true && cd build -cmake .. -G Ninja -DCMAKE_TOOLCHAIN_FILE="$(pwd)/../CMakeModules/MinGWCross.cmake" -DUSE_CCACHE=ON -DYUZU_USE_BUNDLED_UNICORN=ON -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DCMAKE_BUILD_TYPE=Release +cmake .. -G Ninja -DDISPLAY_VERSION=$1 -DCMAKE_TOOLCHAIN_FILE="$(pwd)/../CMakeModules/MinGWCross.cmake" -DUSE_CCACHE=ON -DYUZU_USE_BUNDLED_UNICORN=ON -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DCMAKE_BUILD_TYPE=Release ninja # Clean up the dirty hacks diff --git a/.ci/scripts/windows/exec.sh b/.ci/scripts/windows/exec.sh index d6a994856..4155ed5fc 100644 --- a/.ci/scripts/windows/exec.sh +++ b/.ci/scripts/windows/exec.sh @@ -2,4 +2,4 @@ mkdir -p "ccache" || true chmod a+x ./.ci/scripts/windows/docker.sh -docker run -e CCACHE_DIR=/yuzu/ccache -v $(pwd):/yuzu yuzuemu/build-environments:linux-mingw /bin/bash -ex /yuzu/.ci/scripts/windows/docker.sh +docker run -e CCACHE_DIR=/yuzu/ccache -v $(pwd):/yuzu yuzuemu/build-environments:linux-mingw /bin/bash -ex /yuzu/.ci/scripts/windows/docker.sh $1 diff --git a/.ci/scripts/windows/upload.ps1 b/.ci/scripts/windows/upload.ps1 new file mode 100644 index 000000000..3cb709924 --- /dev/null +++ b/.ci/scripts/windows/upload.ps1 @@ -0,0 +1,38 @@ +$GITDATE = $(git show -s --date=short --format='%ad') -replace "-","" +$GITREV = $(git show -s --format='%h') +$RELEASE_DIST = "yuzu-windows-msvc" + +$MSVC_BUILD_ZIP = "yuzu-windows-msvc-$GITDATE-$GITREV.zip" -replace " ", "" +$MSVC_BUILD_PDB = "yuzu-windows-msvc-$GITDATE-$GITREV-debugsymbols.zip" -replace " ", "" +$MSVC_SEVENZIP = "yuzu-windows-msvc-$GITDATE-$GITREV.7z" -replace " ", "" +$MSVC_TAR = "yuzu-windows-msvc-$GITDATE-$GITREV.tar" -replace " ", "" +$MSVC_TARXZ = "yuzu-windows-msvc-$GITDATE-$GITREV.tar.xz" -replace " ", "" + +$env:BUILD_ZIP = $MSVC_BUILD_ZIP +$env:BUILD_SYMBOLS = $MSVC_BUILD_PDB +$env:BUILD_UPDATE = $MSVC_SEVENZIP + +$BUILD_DIR = ".\build\bin\Release" + +mkdir pdb +Get-ChildItem "$BUILD_DIR\" -Recurse -Filter "*.pdb" | Copy-Item -destination .\pdb +7z a -tzip $MSVC_BUILD_PDB .\pdb\*.pdb +rm "$BUILD_DIR\*.pdb" +mkdir $RELEASE_DIST +mkdir "artifacts" + +Copy-Item "$BUILD_DIR\*" -Destination $RELEASE_DIST -Recurse +rm "$RELEASE_DIST\*.exe" +Get-ChildItem "$BUILD_DIR" -Recurse -Filter "yuzu*.exe" | Copy-Item -destination $RELEASE_DIST +Get-ChildItem "$BUILD_DIR" -Recurse -Filter "QtWebEngineProcess*.exe" | Copy-Item -destination $RELEASE_DIST +Copy-Item .\license.txt -Destination $RELEASE_DIST +Copy-Item .\README.md -Destination $RELEASE_DIST +7z a -tzip $MSVC_BUILD_ZIP $RELEASE_DIST\* +7z a $MSVC_SEVENZIP $RELEASE_DIST + +7z a -r -ttar $MSVC_TAR $RELEASE_DIST +7z a -r -txz $MSVC_TARXZ $MSVC_TAR + +Get-ChildItem . -Filter "*.zip" | Copy-Item -destination "artifacts" +Get-ChildItem . -Filter "*.7z" | Copy-Item -destination "artifacts" +Get-ChildItem . -Filter "*.tar.xz" | Copy-Item -destination "artifacts"
\ No newline at end of file diff --git a/.ci/templates/build-mock.yml b/.ci/templates/build-mock.yml index e7aba93de..0318a0ad8 100644 --- a/.ci/templates/build-mock.yml +++ b/.ci/templates/build-mock.yml @@ -1,5 +1,5 @@ steps: - script: mkdir artifacts || echo 'X' > artifacts/T1.txt - publish: artifacts - artifact: 'yuzu-$(BuildName)-$(BuildSuffix)' + artifact: 'yuzu-$(BuildName)-mock' displayName: 'Upload Artifacts'
\ No newline at end of file diff --git a/.ci/templates/build-msvc.yml b/.ci/templates/build-msvc.yml new file mode 100644 index 000000000..b44a08247 --- /dev/null +++ b/.ci/templates/build-msvc.yml @@ -0,0 +1,22 @@ +parameters: + artifactSource: 'true' + cache: 'false' + version: '' + +steps: +- script: mkdir build && cd build && cmake -G "Visual Studio 15 2017 Win64" --config Release -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_BUNDLED_UNICORN=1 -DYUZU_USE_QT_WEB_ENGINE=ON -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DYUZU_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} -DUSE_DISCORD_PRESENCE=ON -DDISPLAY_VERSION=${{ parameters['version'] }} .. && cd .. + displayName: 'Configure CMake' +- task: MSBuild@1 + displayName: 'Build' + inputs: + solution: 'build/yuzu.sln' + maximumCpuCount: true + configuration: release +- task: PowerShell@2 + displayName: 'Package Artifacts' + inputs: + targetType: 'filePath' + filePath: './.ci/scripts/windows/upload.ps1' +- publish: artifacts + artifact: 'yuzu-$(BuildName)-windows-msvc' + displayName: 'Upload Artifacts' diff --git a/.ci/templates/build-single.yml b/.ci/templates/build-single.yml index 9bc27247e..7b27693be 100644 --- a/.ci/templates/build-single.yml +++ b/.ci/templates/build-single.yml @@ -1,10 +1,9 @@ parameters: artifactSource: 'true' cache: 'false' + version: '' steps: -- script: export DATE=`date '+%Y.%m.%d'` && export CI=true && AZURE_REPO_NAME=yuzu-emu/yuzu-$(BuildName) && AZURE_REPO_TAG=$(BuildName)-$DATE - displayName: 'Determine Build Name' - task: DockerInstaller@0 displayName: 'Prepare Environment' inputs: @@ -15,7 +14,7 @@ steps: key: yuzu-v1-$(BuildName)-$(BuildSuffix)-$(CacheSuffix) path: $(System.DefaultWorkingDirectory)/ccache cacheHitVar: CACHE_RESTORED -- script: chmod a+x ./.ci/scripts/$(ScriptFolder)/exec.sh && ./.ci/scripts/$(ScriptFolder)/exec.sh +- script: chmod a+x ./.ci/scripts/$(ScriptFolder)/exec.sh && ./.ci/scripts/$(ScriptFolder)/exec.sh ${{ parameters['version'] }} displayName: 'Build' - script: chmod a+x ./.ci/scripts/$(ScriptFolder)/upload.sh && RELEASE_NAME=$(BuildName) ./.ci/scripts/$(ScriptFolder)/upload.sh displayName: 'Package Artifacts' diff --git a/.ci/templates/build-standard.yml b/.ci/templates/build-standard.yml index aa180894e..7422c8346 100644 --- a/.ci/templates/build-standard.yml +++ b/.ci/templates/build-standard.yml @@ -1,3 +1,6 @@ +parameters: + version: '' + jobs: - job: build displayName: 'standard' @@ -20,4 +23,5 @@ jobs: - template: ./build-single.yml parameters: artifactSource: 'false' - cache: $(parameters.cache)
\ No newline at end of file + cache: $(parameters.cache) + version: $(parameters.version)
\ No newline at end of file diff --git a/.ci/templates/build-testing.yml b/.ci/templates/build-testing.yml index 4c9625944..30c8aaac3 100644 --- a/.ci/templates/build-testing.yml +++ b/.ci/templates/build-testing.yml @@ -1,3 +1,6 @@ +parameters: + version: '' + jobs: - job: build_test displayName: 'testing' @@ -31,3 +34,4 @@ jobs: parameters: artifactSource: 'false' cache: 'false' + version: $(parameters.version)
\ No newline at end of file diff --git a/.ci/templates/merge-private.yml b/.ci/templates/merge-private.yml index a640cfbde..f15a74355 100644 --- a/.ci/templates/merge-private.yml +++ b/.ci/templates/merge-private.yml @@ -31,17 +31,7 @@ jobs: needSubmodules: 'true' - script: chmod a+x $(System.DefaultWorkingDirectory)/.ci/scripts/merge/yuzubot-git-config.sh && $(System.DefaultWorkingDirectory)/.ci/scripts/merge/yuzubot-git-config.sh displayName: 'Apply Git Configuration' - - script: git tag -a $(BuildName)-$(Build.BuildId) -m "yuzu $(BuildName) $(Build.BuildNumber) $(Build.DefinitionName)" - displayName: 'Tag Source' - script: git remote add other $(GitRepoPushChangesURL) displayName: 'Register Repository' - - script: git push --follow-tags --force other HEAD:$(GitPushBranch) + - script: git push --force other HEAD:$(GitPushBranch) displayName: 'Update Code' - - script: git rev-list -n 1 $(BuildName)-$(Build.BuildId) > $(Build.ArtifactStagingDirectory)/tag-commit.sha - displayName: 'Calculate Release Point' - - task: PublishPipelineArtifact@1 - displayName: 'Upload Release Point' - inputs: - targetPath: '$(Build.ArtifactStagingDirectory)/tag-commit.sha' - artifact: 'yuzu-$(BuildName)-release-point' - replaceExistingArchive: true
\ No newline at end of file diff --git a/.ci/templates/merge.yml b/.ci/templates/merge.yml index efc82778a..460dfa1c1 100644 --- a/.ci/templates/merge.yml +++ b/.ci/templates/merge.yml @@ -30,17 +30,7 @@ jobs: needSubmodules: 'true' - script: chmod a+x $(System.DefaultWorkingDirectory)/.ci/scripts/merge/yuzubot-git-config.sh && $(System.DefaultWorkingDirectory)/.ci/scripts/merge/yuzubot-git-config.sh displayName: 'Apply Git Configuration' - - script: git tag -a $(BuildName)-$(Build.BuildId) -m "yuzu $(BuildName) $(Build.BuildNumber) $(Build.DefinitionName)" - displayName: 'Tag Source' - script: git remote add other $(GitRepoPushChangesURL) displayName: 'Register Repository' - - script: git push --follow-tags --force other HEAD:$(GitPushBranch) + - script: git push --force other HEAD:$(GitPushBranch) displayName: 'Update Code' - - script: git rev-list -n 1 $(BuildName)-$(Build.BuildId) > $(Build.ArtifactStagingDirectory)/tag-commit.sha - displayName: 'Calculate Release Point' - - task: PublishPipelineArtifact@1 - displayName: 'Upload Release Point' - inputs: - targetPath: '$(Build.ArtifactStagingDirectory)/tag-commit.sha' - artifact: 'yuzu-$(BuildName)-release-point' - replaceExistingArchive: true
\ No newline at end of file diff --git a/.ci/templates/mergebot-private.yml b/.ci/templates/mergebot-private.yml index a673c5b01..f9a40cf61 100644 --- a/.ci/templates/mergebot-private.yml +++ b/.ci/templates/mergebot-private.yml @@ -8,16 +8,23 @@ steps: - script: chmod a+x $(System.DefaultWorkingDirectory)/.ci/scripts/merge/yuzubot-git-config.sh && $(System.DefaultWorkingDirectory)/.ci/scripts/merge/yuzubot-git-config.sh displayName: 'Apply Git Configuration' - task: PythonScript@0 - displayName: 'Discover, Download, and Apply Patches' + displayName: 'Discover, Download, and Apply Patches (Mainline)' inputs: scriptSource: 'filePath' scriptPath: '.ci/scripts/merge/apply-patches-by-label.py' - arguments: '${{ parameters.matchLabelPublic }} patches-public' + arguments: '${{ parameters.matchLabelPublic }} $(MergeTaglinePublic) patches-public' workingDirectory: '$(System.DefaultWorkingDirectory)' - task: PythonScript@0 - displayName: 'Discover, Download, and Apply Patches' + displayName: 'Discover, Download, and Apply Patches (Patreon Public)' + inputs: + scriptSource: 'filePath' + scriptPath: '.ci/scripts/merge/apply-patches-by-label.py' + arguments: '${{ parameters.matchLabel }} "$(MergeTaglinePrivate) Public" patches-mixed-public' + workingDirectory: '$(System.DefaultWorkingDirectory)' + - task: PythonScript@0 + displayName: 'Discover, Download, and Apply Patches (Patreon Private)' inputs: scriptSource: 'filePath' scriptPath: '.ci/scripts/merge/apply-patches-by-label-private.py' - arguments: '$(PrivateMergeUser) ${{ parameters.matchLabel }} patches-private' + arguments: '$(PrivateMergeUser) ${{ parameters.matchLabel }} "$(MergeTaglinePrivate) Private" patches-private' workingDirectory: '$(System.DefaultWorkingDirectory)' diff --git a/.ci/templates/mergebot.yml b/.ci/templates/mergebot.yml index 5211efcc6..a4c5c2a28 100644 --- a/.ci/templates/mergebot.yml +++ b/.ci/templates/mergebot.yml @@ -11,5 +11,5 @@ steps: inputs: scriptSource: 'filePath' scriptPath: '.ci/scripts/merge/apply-patches-by-label.py' - arguments: '${{ parameters.matchLabel }} patches' + arguments: '${{ parameters.matchLabel }} Tagged patches' workingDirectory: '$(System.DefaultWorkingDirectory)' diff --git a/.ci/templates/release-download.yml b/.ci/templates/release-download.yml index 50ca06bb2..f7e30690f 100644 --- a/.ci/templates/release-download.yml +++ b/.ci/templates/release-download.yml @@ -2,7 +2,7 @@ steps: - task: DownloadPipelineArtifact@2 displayName: 'Download Windows Release' inputs: - artifactName: 'yuzu-$(BuildName)-windows-mingw' + artifactName: 'yuzu-$(BuildName)-windows-msvc' buildType: 'current' targetPath: '$(Build.ArtifactStagingDirectory)' - task: DownloadPipelineArtifact@2 diff --git a/.ci/templates/release-github.yml b/.ci/templates/release-github.yml index 39fd47f1c..c200954f1 100644 --- a/.ci/templates/release-github.yml +++ b/.ci/templates/release-github.yml @@ -1,11 +1,13 @@ steps: - template: ./release-download.yml - task: GitHubRelease@0 + displayName: 'GitHub Release' inputs: action: 'create' - title: 'yuzu $(BuildName) #$(Build.BuildId)' + title: '$(ReleasePrefix) $(DisplayVersion)' assets: '$(Build.ArtifactStagingDirectory)/*' gitHubConnection: $(GitHubReleaseConnectionName) repositoryName: '$(Build.Repository.Name)' target: '$(Build.SourceVersion)' - tagSource: 'auto'
\ No newline at end of file + tagSource: manual + tag: $(BuildName)-$(DisplayPrefix)-$(DisplayVersion)
\ No newline at end of file diff --git a/.ci/templates/release-private-tag.yml b/.ci/templates/release-private-tag.yml new file mode 100644 index 000000000..e80d57593 --- /dev/null +++ b/.ci/templates/release-private-tag.yml @@ -0,0 +1,9 @@ +steps: + - script: chmod a+x $(System.DefaultWorkingDirectory)/.ci/scripts/merge/yuzubot-git-config.sh && $(System.DefaultWorkingDirectory)/.ci/scripts/merge/yuzubot-git-config.sh + displayName: 'Apply Git Configuration' + - script: git tag -a $(BuildName)-$(DisplayPrefix)-$(DisplayVersion) -m "yuzu $(BuildName) $(Build.BuildNumber) $(Build.DefinitionName) $(DisplayPrefix)-$(DisplayVersion)" + displayName: 'Tag Source' + - script: git remote add other $(GitRepoPushChangesURL) + displayName: 'Register Repository' + - script: git push other $(BuildName)-$(DisplayPrefix)-$(DisplayVersion) + displayName: 'Update Code'
\ No newline at end of file diff --git a/.ci/yuzu-mainline-step2.yml b/.ci/yuzu-mainline-step2.yml index fec724d11..5f2dfb3d8 100644 --- a/.ci/yuzu-mainline-step2.yml +++ b/.ci/yuzu-mainline-step2.yml @@ -1,6 +1,9 @@ trigger: - master +variables: + DisplayVersion: $[counter(variables['DisplayPrefix'], 1)] + stages: - stage: format displayName: 'format' @@ -15,14 +18,51 @@ stages: dependsOn: format displayName: 'build' jobs: - - template: ./templates/build-standard.yml - parameters: - cache: 'true' + - job: build + displayName: 'standard' + pool: + vmImage: ubuntu-latest + strategy: + maxParallel: 10 + matrix: + linux: + BuildSuffix: 'linux' + ScriptFolder: 'linux' + steps: + - template: ./templates/sync-source.yml + parameters: + artifactSource: $(parameters.artifactSource) + needSubmodules: 'true' + - template: ./templates/build-single.yml + parameters: + artifactSource: 'false' + cache: 'true' + version: $(DisplayVersion) +- stage: build_win + dependsOn: format + displayName: 'build-windows' + jobs: + - job: build + displayName: 'msvc' + pool: + vmImage: vs2017-win2016 + steps: + - template: ./templates/sync-source.yml + parameters: + artifactSource: $(parameters.artifactSource) + needSubmodules: 'true' + - template: ./templates/build-msvc.yml + parameters: + artifactSource: 'false' + cache: 'true' + version: $(DisplayVersion) - stage: release - displayName: 'Release' - dependsOn: build + displayName: 'release' + dependsOn: + - build + - build_win jobs: - job: github - displayName: 'GitHub Release' + displayName: 'github' steps: - template: ./templates/release-github.yml
\ No newline at end of file diff --git a/.ci/yuzu-patreon-step2.yml b/.ci/yuzu-patreon-step2.yml index 080118224..35c5fbe36 100644 --- a/.ci/yuzu-patreon-step2.yml +++ b/.ci/yuzu-patreon-step2.yml @@ -1,6 +1,9 @@ trigger: - master +variables: + DisplayVersion: $[counter(variables['DisplayPrefix'], 1)] + stages: - stage: format displayName: 'format' @@ -15,14 +18,25 @@ stages: dependsOn: format displayName: 'build' jobs: - - template: ./templates/build-standard.yml - parameters: - cache: 'true' + - job: build + displayName: 'windows-msvc' + pool: + vmImage: vs2017-win2016 + steps: + - template: ./templates/sync-source.yml + parameters: + artifactSource: $(parameters.artifactSource) + needSubmodules: 'true' + - template: ./templates/build-msvc.yml + parameters: + artifactSource: 'false' + cache: $(parameters.cache) + version: $(DisplayVersion) - stage: release displayName: 'release' dependsOn: build jobs: - - job: azure - displayName: 'azure' - steps: - - template: ./templates/release-universal.yml
\ No newline at end of file + - job: release + displayName: 'source' + steps: + - template: ./templates/release-private-tag.yml diff --git a/.gitmodules b/.gitmodules index 3a49c4874..35e0d1240 100644 --- a/.gitmodules +++ b/.gitmodules @@ -46,3 +46,9 @@ [submodule "sirit"] path = externals/sirit url = https://github.com/ReinUsesLisp/sirit +[submodule "libzip"] + path = externals/libzip + url = https://github.com/DarkLordZach/libzip +[submodule "zlib"] + path = externals/zlib + url = https://github.com/madler/zlib diff --git a/CMakeLists.txt b/CMakeLists.txt index bfa104034..9b3b0d6d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,8 @@ option(YUZU_USE_BUNDLED_UNICORN "Build/Download bundled Unicorn" ON) option(YUZU_USE_QT_WEB_ENGINE "Use QtWebEngine for web applet implementation" OFF) +option(YUZU_ENABLE_BOXCAT "Enable the Boxcat service, a yuzu high-level implementation of BCAT" ON) + option(ENABLE_CUBEB "Enables the cubeb audio backend" ON) option(ENABLE_VULKAN "Enables Vulkan backend" ON) diff --git a/CMakeModules/GenerateSCMRev.cmake b/CMakeModules/GenerateSCMRev.cmake index a1ace89cb..09eabe2c7 100644 --- a/CMakeModules/GenerateSCMRev.cmake +++ b/CMakeModules/GenerateSCMRev.cmake @@ -83,9 +83,15 @@ set(HASH_FILES "${VIDEO_CORE}/shader/decode/video.cpp" "${VIDEO_CORE}/shader/decode/warp.cpp" "${VIDEO_CORE}/shader/decode/xmad.cpp" + "${VIDEO_CORE}/shader/ast.cpp" + "${VIDEO_CORE}/shader/ast.h" "${VIDEO_CORE}/shader/control_flow.cpp" "${VIDEO_CORE}/shader/control_flow.h" + "${VIDEO_CORE}/shader/compiler_settings.cpp" + "${VIDEO_CORE}/shader/compiler_settings.h" "${VIDEO_CORE}/shader/decode.cpp" + "${VIDEO_CORE}/shader/expr.cpp" + "${VIDEO_CORE}/shader/expr.h" "${VIDEO_CORE}/shader/node.h" "${VIDEO_CORE}/shader/node_helper.cpp" "${VIDEO_CORE}/shader/node_helper.h" @@ -1,7 +1,6 @@ yuzu emulator ============= [![Travis CI Build Status](https://travis-ci.org/yuzu-emu/yuzu.svg?branch=master)](https://travis-ci.org/yuzu-emu/yuzu) -[![AppVeyor CI Build Status](https://ci.appveyor.com/api/projects/status/77k97svb2usreu68?svg=true)](https://ci.appveyor.com/project/bunnei/yuzu) [![Azure Mainline CI Build Status](https://dev.azure.com/yuzu-emu/yuzu/_apis/build/status/yuzu%20mainline?branchName=master)](https://dev.azure.com/yuzu-emu/yuzu/) yuzu is an experimental open-source emulator for the Nintendo Switch from the creators of [Citra](https://citra-emu.org/). diff --git a/dist/qt_themes/default/icons/256x256/yuzu.png b/dist/qt_themes/default/icons/256x256/yuzu.png Binary files differindex 8fd4dc259..1e501d8a6 100644 --- a/dist/qt_themes/default/icons/256x256/yuzu.png +++ b/dist/qt_themes/default/icons/256x256/yuzu.png diff --git a/dist/yuzu.icns b/dist/yuzu.icns Binary files differindex 1e0120d7e..fea288c95 100644 --- a/dist/yuzu.icns +++ b/dist/yuzu.icns diff --git a/dist/yuzu.ico b/dist/yuzu.ico Binary files differindex 4f372f571..df3be8464 100644 --- a/dist/yuzu.ico +++ b/dist/yuzu.ico diff --git a/dist/yuzu.svg b/dist/yuzu.svg index 1e16f061a..93171d1bf 100644 --- a/dist/yuzu.svg +++ b/dist/yuzu.svg @@ -1,86 +1 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> - -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - version="1.1" - id="svg815" - xml:space="preserve" - width="72" - height="80" - viewBox="0 0 72 80" - sodipodi:docname="center-logo-v3.svg" - inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"><metadata - id="metadata821"><rdf:RDF><cc:Work - rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs - id="defs819"><clipPath - clipPathUnits="userSpaceOnUse" - id="clipPath831"><path - d="M 0,60 H 54 V 0 H 0 Z" - id="path829" - inkscape:connector-curvature="0" /></clipPath><clipPath - clipPathUnits="userSpaceOnUse" - id="clipPath843"><path - d="M 0,60 H 54 V 0 H 0 Z" - id="path841" - inkscape:connector-curvature="0" /></clipPath><clipPath - clipPathUnits="userSpaceOnUse" - id="clipPath855"><path - d="M 0,60 H 54 V 0 H 0 Z" - id="path853" - inkscape:connector-curvature="0" /></clipPath><clipPath - clipPathUnits="userSpaceOnUse" - id="clipPath867"><path - d="M 0,60 H 54 V 0 H 0 Z" - id="path865" - inkscape:connector-curvature="0" /></clipPath></defs><sodipodi:namedview - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1" - objecttolerance="10" - gridtolerance="10" - guidetolerance="10" - inkscape:pageopacity="0" - inkscape:pageshadow="2" - inkscape:window-width="1920" - inkscape:window-height="1017" - id="namedview817" - showgrid="false" - inkscape:zoom="7.4953319" - inkscape:cx="28.177201" - inkscape:cy="44.348084" - inkscape:window-x="-8" - inkscape:window-y="-8" - inkscape:window-maximized="1" - inkscape:current-layer="g823" /><g - id="g823" - inkscape:groupmode="layer" - inkscape:label="center-logo-v3" - transform="matrix(1.3333333,0,0,-1.3333333,0,80)"><g - id="right" - inkscape:label="#g825"><g - id="g827" - clip-path="url(#clipPath831)"><g - id="g833" - transform="translate(30,48)"><path - d="m 0,0 v -48 c 13.255,0 24,10.745 24,24 C 24,-10.745 13.255,0 0,0 M 3,-3.214 C 13.163,-4.674 21,-13.439 21,-24 21,-34.561 13.163,-43.326 3,-44.786 v 41.572" - style="fill:#ff3c28;fill-opacity:1;fill-rule:nonzero;stroke:none" - id="path835" - inkscape:connector-curvature="0" /></g></g></g><g - id="left" - inkscape:label="#g837"><g - id="g839" - clip-path="url(#clipPath843)"><g - id="g845" - transform="translate(24,60)"><path - d="m 0,0 c -13.255,0 -24,-10.745 -24,-24 0,-13.255 10.745,-24 24,-24 z m -3,-3.214 v -41.572 c -10.163,1.46 -18,10.225 -18,20.786 0,10.561 7.837,19.326 18,20.786" - style="fill:#0ab9e6;fill-opacity:1;fill-rule:nonzero;stroke:none" - id="path847" - inkscape:connector-curvature="0" /></g></g></g></g></svg>
\ No newline at end of file +<svg id="svg815" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 614.4 682.67"><defs><style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}.cls-3{fill:#ff3c28;}.cls-4{fill:#0ab9e6;}</style><clipPath id="clip-path"><rect class="cls-1" x="-43" y="-46.67" width="699.6" height="777.33"/></clipPath></defs><title>Artboard 1</title><g id="g823"><g id="right"><g class="cls-2"><g id="g827"><g id="g833"><path id="path835" class="cls-3" d="M340.81,138V682.08c150.26,0,272.06-121.81,272.06-272.06S491.07,138,340.81,138M394,197.55a219.06,219.06,0,0,1,0,424.94V197.55"/></g></g></g></g><g id="left"><g class="cls-2"><g id="g839"><g id="g845"><path id="path847" class="cls-4" d="M272.79,1.92C122.53,1.92.73,123.73.73,274s121.8,272.07,272.06,272.07ZM219.65,61.51v425A219,219,0,0,1,118,119.18,217.51,217.51,0,0,1,219.65,61.51"/></g></g></g></g></g></svg>
\ No newline at end of file diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index e6fa11a03..3539828b8 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -77,6 +77,12 @@ if (ENABLE_VULKAN) add_subdirectory(sirit) endif() +# zlib +add_subdirectory(zlib EXCLUDE_FROM_ALL) + +# libzip +add_subdirectory(libzip EXCLUDE_FROM_ALL) + if (ENABLE_WEB_SERVICE) # LibreSSL set(LIBRESSL_SKIP_INSTALL ON CACHE BOOL "") diff --git a/externals/libzip b/externals/libzip new file mode 160000 +Subproject bd7a8103e96bc6d50164447f6b7b57bb786d8e2 diff --git a/externals/zlib b/externals/zlib new file mode 160000 +Subproject cacf7f1d4e3d44d871b605da3b647f07d718623 diff --git a/license.txt b/license.txt index 2b858f9a7..bf5aec0e6 100644 --- a/license.txt +++ b/license.txt @@ -341,15 +341,24 @@ Public License instead of this License. The icons used in this project have the following licenses: -Icon Name | License | Origin/Author ---- | --- | --- -checked.png | Free for non-commercial use -failed.png | Free for non-commercial use -lock.png | CC BY-ND 3.0 | https://icons8.com -plus_folder.png | CC BY-ND 3.0 | https://icons8.com -bad_folder.png | CC BY-ND 3.0 | https://icons8.com -chip.png | CC BY-ND 3.0 | https://icons8.com -folder.png | CC BY-ND 3.0 | https://icons8.com -plus.png (Default, Dark) | CC0 1.0 | Designed by BreadFish64 from the Citra team -plus.png (Colorful, Colorful Dark) | CC BY-ND 3.0 | https://icons8.com -sd_card.png | CC BY-ND 3.0 | https://icons8.com +Icon Name | License | Origin/Author +--- | --- | --- +checked.png | Free for non-commercial use +failed.png | Free for non-commercial use +lock.png | CC BY-ND 3.0 | https://icons8.com +plus_folder.png (Default, Dark) | CC BY-ND 3.0 | https://icons8.com +bad_folder.png (Default, Dark) | CC BY-ND 3.0 | https://icons8.com +chip.png (Default, Dark) | CC BY-ND 3.0 | https://icons8.com +folder.png (Default, Dark) | CC BY-ND 3.0 | https://icons8.com +plus.png (Default, Dark) | CC0 1.0 | Designed by BreadFish64 from the Citra team +sd_card.png (Default, Dark) | CC BY-ND 3.0 | https://icons8.com +plus_folder.png (Colorful, Colorful Dark) | CC BY-ND 3.0 | https://icons8.com +bad_folder.png (Colorful, Colorful Dark) | CC BY-ND 3.0 | https://icons8.com +chip.png (Colorful, Colorful Dark) | CC BY-ND 3.0 | https://icons8.com +folder.png (Colorful, Colorful Dark) | CC BY-ND 3.0 | https://icons8.com +plus.png (Colorful, Colorful Dark) | CC BY-ND 3.0 | https://icons8.com +sd_card.png (Colorful, Colorful Dark) | CC BY-ND 3.0 | https://icons8.com + +Note: +Some icons are different in different themes, and they are separately listed +only when they have different licenses/origins. diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index dfed8b51d..906c486fd 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -15,11 +15,23 @@ if (DEFINED ENV{CI}) set(BUILD_TAG $ENV{AZURE_REPO_TAG}) endif() endif() +if (DEFINED ENV{TITLEBARFORMATIDLE}) + set(TITLE_BAR_FORMAT_IDLE $ENV{TITLEBARFORMATIDLE}) +endif () +if (DEFINED ENV{TITLEBARFORMATRUNNING}) + set(TITLE_BAR_FORMAT_RUNNING $ENV{TITLEBARFORMATRUNNING}) +endif () +if (DEFINED ENV{DISPLAYVERSION}) + set(DISPLAY_VERSION $ENV{DISPLAYVERSION}) +endif () add_custom_command(OUTPUT scm_rev.cpp COMMAND ${CMAKE_COMMAND} -DSRC_DIR="${CMAKE_SOURCE_DIR}" -DBUILD_REPOSITORY="${BUILD_REPOSITORY}" + -DTITLE_BAR_FORMAT_IDLE="${TITLE_BAR_FORMAT_IDLE}" + -DTITLE_BAR_FORMAT_RUNNING="${TITLE_BAR_FORMAT_RUNNING}" -DBUILD_TAG="${BUILD_TAG}" + -DBUILD_ID="${DISPLAY_VERSION}" -P "${CMAKE_SOURCE_DIR}/CMakeModules/GenerateSCMRev.cmake" DEPENDS # WARNING! It was too much work to try and make a common location for this list, @@ -60,9 +72,15 @@ add_custom_command(OUTPUT scm_rev.cpp "${VIDEO_CORE}/shader/decode/video.cpp" "${VIDEO_CORE}/shader/decode/warp.cpp" "${VIDEO_CORE}/shader/decode/xmad.cpp" + "${VIDEO_CORE}/shader/ast.cpp" + "${VIDEO_CORE}/shader/ast.h" "${VIDEO_CORE}/shader/control_flow.cpp" "${VIDEO_CORE}/shader/control_flow.h" + "${VIDEO_CORE}/shader/compiler_settings.cpp" + "${VIDEO_CORE}/shader/compiler_settings.h" "${VIDEO_CORE}/shader/decode.cpp" + "${VIDEO_CORE}/shader/expr.cpp" + "${VIDEO_CORE}/shader/expr.h" "${VIDEO_CORE}/shader/node.h" "${VIDEO_CORE}/shader/node_helper.cpp" "${VIDEO_CORE}/shader/node_helper.h" diff --git a/src/common/alignment.h b/src/common/alignment.h index 88d5d3a65..cdd4833f8 100644 --- a/src/common/alignment.h +++ b/src/common/alignment.h @@ -51,7 +51,17 @@ public: using reference = T&; using const_reference = const T&; + using propagate_on_container_copy_assignment = std::true_type; + using propagate_on_container_move_assignment = std::true_type; + using propagate_on_container_swap = std::true_type; + using is_always_equal = std::true_type; + public: + constexpr AlignmentAllocator() noexcept = default; + + template <typename T2> + constexpr AlignmentAllocator(const AlignmentAllocator<T2, Align>&) noexcept {} + pointer address(reference r) noexcept { return std::addressof(r); } diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index 2d9374783..41167f57a 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -713,7 +713,6 @@ const std::string& GetUserPath(UserPath path, const std::string& new_path) { case UserPath::RootDir: user_path = paths[UserPath::RootDir] + DIR_SEP; break; - case UserPath::UserDir: user_path = paths[UserPath::RootDir] + DIR_SEP; paths[UserPath::ConfigDir] = user_path + CONFIG_DIR DIR_SEP; @@ -721,6 +720,8 @@ const std::string& GetUserPath(UserPath path, const std::string& new_path) { paths[UserPath::SDMCDir] = user_path + SDMC_DIR DIR_SEP; paths[UserPath::NANDDir] = user_path + NAND_DIR DIR_SEP; break; + default: + break; } } diff --git a/src/common/scm_rev.cpp.in b/src/common/scm_rev.cpp.in index d69038f65..5f126f324 100644 --- a/src/common/scm_rev.cpp.in +++ b/src/common/scm_rev.cpp.in @@ -11,6 +11,9 @@ #define BUILD_DATE "@BUILD_DATE@" #define BUILD_FULLNAME "@BUILD_FULLNAME@" #define BUILD_VERSION "@BUILD_VERSION@" +#define BUILD_ID "@BUILD_ID@" +#define TITLE_BAR_FORMAT_IDLE "@TITLE_BAR_FORMAT_IDLE@" +#define TITLE_BAR_FORMAT_RUNNING "@TITLE_BAR_FORMAT_RUNNING@" #define SHADER_CACHE_VERSION "@SHADER_CACHE_VERSION@" namespace Common { @@ -22,6 +25,9 @@ const char g_build_name[] = BUILD_NAME; const char g_build_date[] = BUILD_DATE; const char g_build_fullname[] = BUILD_FULLNAME; const char g_build_version[] = BUILD_VERSION; +const char g_build_id[] = BUILD_ID; +const char g_title_bar_format_idle[] = TITLE_BAR_FORMAT_IDLE; +const char g_title_bar_format_running[] = TITLE_BAR_FORMAT_RUNNING; const char g_shader_cache_version[] = SHADER_CACHE_VERSION; } // namespace diff --git a/src/common/scm_rev.h b/src/common/scm_rev.h index 666bf0367..563015ec9 100644 --- a/src/common/scm_rev.h +++ b/src/common/scm_rev.h @@ -13,6 +13,9 @@ extern const char g_build_name[]; extern const char g_build_date[]; extern const char g_build_fullname[]; extern const char g_build_version[]; +extern const char g_build_id[]; +extern const char g_title_bar_format_idle[]; +extern const char g_title_bar_format_running[]; extern const char g_shader_cache_version[]; } // namespace Common diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index a6b56c9c6..3b1d72cf9 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,3 +1,9 @@ +if (YUZU_ENABLE_BOXCAT) + set(BCAT_BOXCAT_ADDITIONAL_SOURCES hle/service/bcat/backend/boxcat.cpp hle/service/bcat/backend/boxcat.h) +else() + set(BCAT_BOXCAT_ADDITIONAL_SOURCES) +endif() + add_library(core STATIC arm/arm_interface.h arm/arm_interface.cpp @@ -82,6 +88,8 @@ add_library(core STATIC file_sys/vfs_concat.h file_sys/vfs_layered.cpp file_sys/vfs_layered.h + file_sys/vfs_libzip.cpp + file_sys/vfs_libzip.h file_sys/vfs_offset.cpp file_sys/vfs_offset.h file_sys/vfs_real.cpp @@ -241,6 +249,9 @@ add_library(core STATIC hle/service/audio/errors.h hle/service/audio/hwopus.cpp hle/service/audio/hwopus.h + hle/service/bcat/backend/backend.cpp + hle/service/bcat/backend/backend.h + ${BCAT_BOXCAT_ADDITIONAL_SOURCES} hle/service/bcat/bcat.cpp hle/service/bcat/bcat.h hle/service/bcat/module.cpp @@ -324,6 +335,8 @@ add_library(core STATIC hle/service/ldr/ldr.h hle/service/lm/lm.cpp hle/service/lm/lm.h + hle/service/lm/manager.cpp + hle/service/lm/manager.h hle/service/mig/mig.cpp hle/service/mig/mig.h hle/service/mii/mii.cpp @@ -499,6 +512,15 @@ create_target_directory_groups(core) target_link_libraries(core PUBLIC common PRIVATE audio_core video_core) target_link_libraries(core PUBLIC Boost::boost PRIVATE fmt json-headers mbedtls opus unicorn open_source_archives) + +if (YUZU_ENABLE_BOXCAT) + get_directory_property(OPENSSL_LIBS + DIRECTORY ${PROJECT_SOURCE_DIR}/externals/libressl + DEFINITION OPENSSL_LIBS) + target_compile_definitions(core PRIVATE -DCPPHTTPLIB_OPENSSL_SUPPORT -DYUZU_ENABLE_BOXCAT) + target_link_libraries(core PRIVATE httplib json-headers ${OPENSSL_LIBS} zip) +endif() + if (ENABLE_WEB_SERVICE) target_compile_definitions(core PRIVATE -DENABLE_WEB_SERVICE) target_link_libraries(core PRIVATE web_service) diff --git a/src/core/core.cpp b/src/core/core.cpp index 92ba42fb9..4d0ac72a5 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -35,6 +35,7 @@ #include "core/hle/service/apm/controller.h" #include "core/hle/service/filesystem/filesystem.h" #include "core/hle/service/glue/manager.h" +#include "core/hle/service/lm/manager.h" #include "core/hle/service/service.h" #include "core/hle/service/sm/sm.h" #include "core/loader/loader.h" @@ -111,7 +112,8 @@ FileSys::VirtualFile GetGameFileFromPath(const FileSys::VirtualFilesystem& vfs, } struct System::Impl { explicit Impl(System& system) - : kernel{system}, cpu_core_manager{system}, applet_manager{system}, reporter{system} {} + : kernel{system}, fs_controller{system}, cpu_core_manager{system}, + applet_manager{system}, reporter{system} {} Cpu& CurrentCpuCore() { return cpu_core_manager.GetCurrentCore(); @@ -249,6 +251,8 @@ struct System::Impl { telemetry_session->AddField(Telemetry::FieldType::Performance, "Mean_Frametime_MS", perf_stats->GetMeanFrametime()); + lm_manager.Flush(); + is_powered_on = false; exit_lock = false; @@ -337,8 +341,10 @@ struct System::Impl { bool is_powered_on = false; bool exit_lock = false; + Reporter reporter; std::unique_ptr<Memory::CheatEngine> cheat_engine; std::unique_ptr<Tools::Freezer> memory_freezer; + std::array<u8, 0x20> build_id{}; /// Frontend applets Service::AM::Applets::AppletManager applet_manager; @@ -346,8 +352,9 @@ struct System::Impl { /// APM (Performance) services Service::APM::Controller apm_controller{core_timing}; - /// Glue services + /// Service State Service::Glue::ARPManager arp_manager; + Service::LM::Manager lm_manager{reporter}; /// Service manager std::shared_ptr<Service::SM::ServiceManager> service_manager; @@ -355,8 +362,6 @@ struct System::Impl { /// Telemetry session for this emulation session std::unique_ptr<Core::TelemetrySession> telemetry_session; - Reporter reporter; - ResultStatus status = ResultStatus::Success; std::string status_details = ""; @@ -632,6 +637,14 @@ const Service::APM::Controller& System::GetAPMController() const { return impl->apm_controller; } +Service::LM::Manager& System::GetLogManager() { + return impl->lm_manager; +} + +const Service::LM::Manager& System::GetLogManager() const { + return impl->lm_manager; +} + void System::SetExitLock(bool locked) { impl->exit_lock = locked; } @@ -640,6 +653,14 @@ bool System::GetExitLock() const { return impl->exit_lock; } +void System::SetCurrentProcessBuildID(const CurrentBuildProcessID& id) { + impl->build_id = id; +} + +const System::CurrentBuildProcessID& System::GetCurrentProcessBuildID() const { + return impl->build_id; +} + System::ResultStatus System::Init(Frontend::EmuWindow& emu_window) { return impl->Init(*this, emu_window); } diff --git a/src/core/core.h b/src/core/core.h index ff10ebe12..90e7ac607 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -8,7 +8,6 @@ #include <memory> #include <string> -#include <map> #include "common/common_types.h" #include "core/file_sys/vfs_types.h" #include "core/hle/kernel/object.h" @@ -58,6 +57,10 @@ namespace Glue { class ARPManager; } +namespace LM { +class Manager; +} // namespace LM + namespace SM { class ServiceManager; } // namespace SM @@ -98,6 +101,8 @@ FileSys::VirtualFile GetGameFileFromPath(const FileSys::VirtualFilesystem& vfs, class System { public: + using CurrentBuildProcessID = std::array<u8, 0x20>; + System(const System&) = delete; System& operator=(const System&) = delete; @@ -326,10 +331,18 @@ public: const Service::APM::Controller& GetAPMController() const; + Service::LM::Manager& GetLogManager(); + + const Service::LM::Manager& GetLogManager() const; + void SetExitLock(bool locked); bool GetExitLock() const; + void SetCurrentProcessBuildID(const CurrentBuildProcessID& id); + + const CurrentBuildProcessID& GetCurrentProcessBuildID() const; + private: System(); @@ -353,8 +366,4 @@ private: static System s_instance; }; -inline Kernel::Process* CurrentProcess() { - return System::GetInstance().CurrentProcess(); -} - } // namespace Core diff --git a/src/core/crypto/key_manager.cpp b/src/core/crypto/key_manager.cpp index 46aceec3d..222fc95ba 100644 --- a/src/core/crypto/key_manager.cpp +++ b/src/core/crypto/key_manager.cpp @@ -423,7 +423,7 @@ static std::optional<u64> FindTicketOffset(const std::array<u8, size>& data) { std::optional<std::pair<Key128, Key128>> ParseTicket(const Ticket& ticket, const RSAKeyPair<2048>& key) { const auto issuer = ticket.GetData().issuer; - if (issuer == std::array<u8, 0x40>{}) + if (IsAllZeroArray(issuer)) return {}; if (issuer[0] != 'R' || issuer[1] != 'o' || issuer[2] != 'o' || issuer[3] != 't') { LOG_INFO(Crypto, "Attempting to parse ticket with non-standard certificate authority."); diff --git a/src/core/file_sys/bis_factory.cpp b/src/core/file_sys/bis_factory.cpp index 8f758d6d9..0af44f340 100644 --- a/src/core/file_sys/bis_factory.cpp +++ b/src/core/file_sys/bis_factory.cpp @@ -136,4 +136,9 @@ u64 BISFactory::GetFullNANDTotalSpace() const { return static_cast<u64>(Settings::values.nand_total_size); } +VirtualDir BISFactory::GetBCATDirectory(u64 title_id) const { + return GetOrCreateDirectoryRelative(nand_root, + fmt::format("/system/save/bcat/{:016X}", title_id)); +} + } // namespace FileSys diff --git a/src/core/file_sys/bis_factory.h b/src/core/file_sys/bis_factory.h index bdfe728c9..8f0451c98 100644 --- a/src/core/file_sys/bis_factory.h +++ b/src/core/file_sys/bis_factory.h @@ -61,6 +61,8 @@ public: u64 GetUserNANDTotalSpace() const; u64 GetFullNANDTotalSpace() const; + VirtualDir GetBCATDirectory(u64 title_id) const; + private: VirtualDir nand_root; VirtualDir load_root; diff --git a/src/core/file_sys/romfs_factory.cpp b/src/core/file_sys/romfs_factory.cpp index 84cd4684c..4bd2e6183 100644 --- a/src/core/file_sys/romfs_factory.cpp +++ b/src/core/file_sys/romfs_factory.cpp @@ -35,11 +35,11 @@ void RomFSFactory::SetPackedUpdate(VirtualFile update_raw) { this->update_raw = std::move(update_raw); } -ResultVal<VirtualFile> RomFSFactory::OpenCurrentProcess() const { +ResultVal<VirtualFile> RomFSFactory::OpenCurrentProcess(u64 current_process_title_id) const { if (!updatable) return MakeResult<VirtualFile>(file); - const PatchManager patch_manager(Core::CurrentProcess()->GetTitleID()); + const PatchManager patch_manager(current_process_title_id); return MakeResult<VirtualFile>( patch_manager.PatchRomFS(file, ivfc_offset, ContentRecordType::Program, update_raw)); } diff --git a/src/core/file_sys/romfs_factory.h b/src/core/file_sys/romfs_factory.h index da63a313a..c5d40285c 100644 --- a/src/core/file_sys/romfs_factory.h +++ b/src/core/file_sys/romfs_factory.h @@ -33,7 +33,7 @@ public: ~RomFSFactory(); void SetPackedUpdate(VirtualFile update_raw); - ResultVal<VirtualFile> OpenCurrentProcess() const; + ResultVal<VirtualFile> OpenCurrentProcess(u64 current_process_title_id) const; ResultVal<VirtualFile> Open(u64 title_id, StorageId storage, ContentRecordType type) const; private: diff --git a/src/core/file_sys/savedata_factory.cpp b/src/core/file_sys/savedata_factory.cpp index f77cc02ac..fc8755c78 100644 --- a/src/core/file_sys/savedata_factory.cpp +++ b/src/core/file_sys/savedata_factory.cpp @@ -127,8 +127,9 @@ std::string SaveDataFactory::GetFullPath(SaveDataSpaceId space, SaveDataType typ u128 user_id, u64 save_id) { // According to switchbrew, if a save is of type SaveData and the title id field is 0, it should // be interpreted as the title id of the current process. - if (type == SaveDataType::SaveData && title_id == 0) - title_id = Core::CurrentProcess()->GetTitleID(); + if (type == SaveDataType::SaveData && title_id == 0) { + title_id = Core::System::GetInstance().CurrentProcess()->GetTitleID(); + } std::string out = GetSaveDataSpaceIdPath(space); diff --git a/src/core/file_sys/vfs_libzip.cpp b/src/core/file_sys/vfs_libzip.cpp new file mode 100644 index 000000000..8bdaa7e4a --- /dev/null +++ b/src/core/file_sys/vfs_libzip.cpp @@ -0,0 +1,79 @@ +// Copyright 2019 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <string> +#include <zip.h> +#include "common/logging/backend.h" +#include "core/file_sys/vfs.h" +#include "core/file_sys/vfs_libzip.h" +#include "core/file_sys/vfs_vector.h" + +namespace FileSys { + +VirtualDir ExtractZIP(VirtualFile file) { + zip_error_t error{}; + + const auto data = file->ReadAllBytes(); + std::unique_ptr<zip_source_t, decltype(&zip_source_close)> src{ + zip_source_buffer_create(data.data(), data.size(), 0, &error), zip_source_close}; + if (src == nullptr) + return nullptr; + + std::unique_ptr<zip_t, decltype(&zip_close)> zip{zip_open_from_source(src.get(), 0, &error), + zip_close}; + if (zip == nullptr) + return nullptr; + + std::shared_ptr<VectorVfsDirectory> out = std::make_shared<VectorVfsDirectory>(); + + const auto num_entries = zip_get_num_entries(zip.get(), 0); + + zip_stat_t stat{}; + zip_stat_init(&stat); + + for (std::size_t i = 0; i < num_entries; ++i) { + const auto stat_res = zip_stat_index(zip.get(), i, 0, &stat); + if (stat_res == -1) + return nullptr; + + const std::string name(stat.name); + if (name.empty()) + continue; + + if (name.back() != '/') { + std::unique_ptr<zip_file_t, decltype(&zip_fclose)> file{ + zip_fopen_index(zip.get(), i, 0), zip_fclose}; + + std::vector<u8> buf(stat.size); + if (zip_fread(file.get(), buf.data(), buf.size()) != buf.size()) + return nullptr; + + const auto parts = FileUtil::SplitPathComponents(stat.name); + const auto new_file = std::make_shared<VectorVfsFile>(buf, parts.back()); + + std::shared_ptr<VectorVfsDirectory> dtrv = out; + for (std::size_t j = 0; j < parts.size() - 1; ++j) { + if (dtrv == nullptr) + return nullptr; + const auto subdir = dtrv->GetSubdirectory(parts[j]); + if (subdir == nullptr) { + const auto temp = std::make_shared<VectorVfsDirectory>( + std::vector<VirtualFile>{}, std::vector<VirtualDir>{}, parts[j]); + dtrv->AddDirectory(temp); + dtrv = temp; + } else { + dtrv = std::dynamic_pointer_cast<VectorVfsDirectory>(subdir); + } + } + + if (dtrv == nullptr) + return nullptr; + dtrv->AddFile(new_file); + } + } + + return out; +} + +} // namespace FileSys diff --git a/src/core/file_sys/vfs_libzip.h b/src/core/file_sys/vfs_libzip.h new file mode 100644 index 000000000..f68af576a --- /dev/null +++ b/src/core/file_sys/vfs_libzip.h @@ -0,0 +1,13 @@ +// Copyright 2019 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "core/file_sys/vfs_types.h" + +namespace FileSys { + +VirtualDir ExtractZIP(VirtualFile zip); + +} // namespace FileSys diff --git a/src/core/gdbstub/gdbstub.cpp b/src/core/gdbstub/gdbstub.cpp index afa812598..db51d722f 100644 --- a/src/core/gdbstub/gdbstub.cpp +++ b/src/core/gdbstub/gdbstub.cpp @@ -641,7 +641,8 @@ static void HandleQuery() { strlen("Xfer:features:read:target.xml:")) == 0) { SendReply(target_xml); } else if (strncmp(query, "Offsets", strlen("Offsets")) == 0) { - const VAddr base_address = Core::CurrentProcess()->VMManager().GetCodeRegionBaseAddress(); + const VAddr base_address = + Core::System::GetInstance().CurrentProcess()->VMManager().GetCodeRegionBaseAddress(); std::string buffer = fmt::format("TextSeg={:0x}", base_address); SendReply(buffer.c_str()); } else if (strncmp(query, "fThreadInfo", strlen("fThreadInfo")) == 0) { diff --git a/src/core/hle/kernel/handle_table.cpp b/src/core/hle/kernel/handle_table.cpp index bdfaa977f..2cc5d536b 100644 --- a/src/core/hle/kernel/handle_table.cpp +++ b/src/core/hle/kernel/handle_table.cpp @@ -103,7 +103,7 @@ SharedPtr<Object> HandleTable::GetGeneric(Handle handle) const { if (handle == CurrentThread) { return GetCurrentThread(); } else if (handle == CurrentProcess) { - return Core::CurrentProcess(); + return Core::System::GetInstance().CurrentProcess(); } if (!IsValid(handle)) { diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index 797c9a06f..941ebc93a 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -31,6 +31,7 @@ #include "core/hle/service/am/tcap.h" #include "core/hle/service/apm/controller.h" #include "core/hle/service/apm/interface.h" +#include "core/hle/service/bcat/backend/backend.h" #include "core/hle/service/filesystem/filesystem.h" #include "core/hle/service/ns/ns.h" #include "core/hle/service/nvflinger/nvflinger.h" @@ -46,15 +47,20 @@ constexpr ResultCode ERR_NO_DATA_IN_CHANNEL{ErrorModule::AM, 0x2}; constexpr ResultCode ERR_NO_MESSAGES{ErrorModule::AM, 0x3}; constexpr ResultCode ERR_SIZE_OUT_OF_BOUNDS{ErrorModule::AM, 0x1F7}; -constexpr u32 POP_LAUNCH_PARAMETER_MAGIC = 0xC79497CA; +enum class LaunchParameterKind : u32 { + ApplicationSpecific = 1, + AccountPreselectedUser = 2, +}; + +constexpr u32 LAUNCH_PARAMETER_ACCOUNT_PRESELECTED_USER_MAGIC = 0xC79497CA; -struct LaunchParameters { +struct LaunchParameterAccountPreselectedUser { u32_le magic; u32_le is_account_selected; u128 current_user; INSERT_PADDING_BYTES(0x70); }; -static_assert(sizeof(LaunchParameters) == 0x88); +static_assert(sizeof(LaunchParameterAccountPreselectedUser) == 0x88); IWindowController::IWindowController(Core::System& system_) : ServiceFramework("IWindowController"), system{system_} { @@ -1128,26 +1134,55 @@ void IApplicationFunctions::EndBlockingHomeButton(Kernel::HLERequestContext& ctx } void IApplicationFunctions::PopLaunchParameter(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_AM, "called"); + IPC::RequestParser rp{ctx}; + const auto kind = rp.PopEnum<LaunchParameterKind>(); - LaunchParameters params{}; + LOG_DEBUG(Service_AM, "called, kind={:08X}", static_cast<u8>(kind)); - params.magic = POP_LAUNCH_PARAMETER_MAGIC; - params.is_account_selected = 1; + if (kind == LaunchParameterKind::ApplicationSpecific && !launch_popped_application_specific) { + const auto backend = BCAT::CreateBackendFromSettings( + [this](u64 tid) { return system.GetFileSystemController().GetBCATDirectory(tid); }); + const auto build_id_full = system.GetCurrentProcessBuildID(); + u64 build_id{}; + std::memcpy(&build_id, build_id_full.data(), sizeof(u64)); - Account::ProfileManager profile_manager{}; - const auto uuid = profile_manager.GetUser(Settings::values.current_user); - ASSERT(uuid); - params.current_user = uuid->uuid; + const auto data = + backend->GetLaunchParameter({system.CurrentProcess()->GetTitleID(), build_id}); - IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + if (data.has_value()) { + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(RESULT_SUCCESS); + rb.PushIpcInterface<AM::IStorage>(*data); + launch_popped_application_specific = true; + return; + } + } else if (kind == LaunchParameterKind::AccountPreselectedUser && + !launch_popped_account_preselect) { + LaunchParameterAccountPreselectedUser params{}; - rb.Push(RESULT_SUCCESS); + params.magic = LAUNCH_PARAMETER_ACCOUNT_PRESELECTED_USER_MAGIC; + params.is_account_selected = 1; - std::vector<u8> buffer(sizeof(LaunchParameters)); - std::memcpy(buffer.data(), ¶ms, buffer.size()); + Account::ProfileManager profile_manager{}; + const auto uuid = profile_manager.GetUser(Settings::values.current_user); + ASSERT(uuid); + params.current_user = uuid->uuid; - rb.PushIpcInterface<AM::IStorage>(buffer); + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + + rb.Push(RESULT_SUCCESS); + + std::vector<u8> buffer(sizeof(LaunchParameterAccountPreselectedUser)); + std::memcpy(buffer.data(), ¶ms, buffer.size()); + + rb.PushIpcInterface<AM::IStorage>(buffer); + launch_popped_account_preselect = true; + return; + } + + LOG_ERROR(Service_AM, "Attempted to load launch parameter but none was found!"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERR_NO_DATA_IN_CHANNEL); } void IApplicationFunctions::CreateApplicationAndRequestToStartForQuest( @@ -1165,7 +1200,7 @@ void IApplicationFunctions::EnsureSaveData(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_AM, "called, uid={:016X}{:016X}", user_id[1], user_id[0]); FileSys::SaveDataDescriptor descriptor{}; - descriptor.title_id = Core::CurrentProcess()->GetTitleID(); + descriptor.title_id = system.CurrentProcess()->GetTitleID(); descriptor.user_id = user_id; descriptor.type = FileSys::SaveDataType::SaveData; const auto res = system.GetFileSystemController().CreateSaveData( diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h index a3baeb673..ccd053c13 100644 --- a/src/core/hle/service/am/am.h +++ b/src/core/hle/service/am/am.h @@ -147,6 +147,7 @@ private: void GetAccumulatedSuspendedTickValue(Kernel::HLERequestContext& ctx); void GetAccumulatedSuspendedTickChangedEvent(Kernel::HLERequestContext& ctx); + Core::System& system; std::shared_ptr<NVFlinger::NVFlinger> nvflinger; Kernel::EventPair launchable_event; Kernel::EventPair accumulated_suspended_tick_changed_event; @@ -154,8 +155,6 @@ private: u32 idle_time_detection_extension = 0; u64 num_fatal_sections_entered = 0; bool is_auto_sleep_disabled = false; - - Core::System& system; }; class ICommonStateGetter final : public ServiceFramework<ICommonStateGetter> { @@ -255,6 +254,8 @@ private: void EnableApplicationCrashReport(Kernel::HLERequestContext& ctx); void GetGpuErrorDetectedSystemEvent(Kernel::HLERequestContext& ctx); + bool launch_popped_application_specific = false; + bool launch_popped_account_preselect = false; Kernel::EventPair gpu_error_detected_event; Core::System& system; }; diff --git a/src/core/hle/service/am/applets/applets.cpp b/src/core/hle/service/am/applets/applets.cpp index d2e35362f..720fe766f 100644 --- a/src/core/hle/service/am/applets/applets.cpp +++ b/src/core/hle/service/am/applets/applets.cpp @@ -157,6 +157,10 @@ AppletManager::AppletManager(Core::System& system_) : system{system_} {} AppletManager::~AppletManager() = default; +const AppletFrontendSet& AppletManager::GetAppletFrontendSet() const { + return frontend; +} + void AppletManager::SetAppletFrontendSet(AppletFrontendSet set) { if (set.parental_controls != nullptr) frontend.parental_controls = std::move(set.parental_controls); diff --git a/src/core/hle/service/am/applets/applets.h b/src/core/hle/service/am/applets/applets.h index 764c3418c..226be88b1 100644 --- a/src/core/hle/service/am/applets/applets.h +++ b/src/core/hle/service/am/applets/applets.h @@ -190,6 +190,8 @@ public: explicit AppletManager(Core::System& system_); ~AppletManager(); + const AppletFrontendSet& GetAppletFrontendSet() const; + void SetAppletFrontendSet(AppletFrontendSet set); void SetDefaultAppletFrontendSet(); void SetDefaultAppletsIfMissing(); diff --git a/src/core/hle/service/apm/controller.cpp b/src/core/hle/service/apm/controller.cpp index 4376612eb..073d0f6fa 100644 --- a/src/core/hle/service/apm/controller.cpp +++ b/src/core/hle/service/apm/controller.cpp @@ -13,7 +13,7 @@ constexpr PerformanceConfiguration DEFAULT_PERFORMANCE_CONFIGURATION = PerformanceConfiguration::Config7; Controller::Controller(Core::Timing::CoreTiming& core_timing) - : core_timing(core_timing), configs{ + : core_timing{core_timing}, configs{ {PerformanceMode::Handheld, DEFAULT_PERFORMANCE_CONFIGURATION}, {PerformanceMode::Docked, DEFAULT_PERFORMANCE_CONFIGURATION}, } {} @@ -63,6 +63,7 @@ PerformanceConfiguration Controller::GetCurrentPerformanceConfiguration(Performa void Controller::SetClockSpeed(u32 mhz) { LOG_INFO(Service_APM, "called, mhz={:08X}", mhz); // TODO(DarkLordZach): Actually signal core_timing to change clock speed. + // TODO(Rodrigo): Remove [[maybe_unused]] when core_timing is used. } } // namespace Service::APM diff --git a/src/core/hle/service/apm/controller.h b/src/core/hle/service/apm/controller.h index 8ac80eaea..454caa6eb 100644 --- a/src/core/hle/service/apm/controller.h +++ b/src/core/hle/service/apm/controller.h @@ -50,7 +50,7 @@ enum class PerformanceMode : u8 { // system during times of high load -- this simply maps to different PerformanceConfigs to use. class Controller { public: - Controller(Core::Timing::CoreTiming& core_timing); + explicit Controller(Core::Timing::CoreTiming& core_timing); ~Controller(); void SetPerformanceConfiguration(PerformanceMode mode, PerformanceConfiguration config); @@ -62,9 +62,9 @@ public: private: void SetClockSpeed(u32 mhz); - std::map<PerformanceMode, PerformanceConfiguration> configs; + [[maybe_unused]] Core::Timing::CoreTiming& core_timing; - Core::Timing::CoreTiming& core_timing; + std::map<PerformanceMode, PerformanceConfiguration> configs; }; } // namespace Service::APM diff --git a/src/core/hle/service/audio/audout_u.cpp b/src/core/hle/service/audio/audout_u.cpp index fb84a8f13..9afefb5c6 100644 --- a/src/core/hle/service/audio/audout_u.cpp +++ b/src/core/hle/service/audio/audout_u.cpp @@ -205,7 +205,7 @@ private: AudioCore::StreamPtr stream; std::string device_name; - AudoutParams audio_params{}; + [[maybe_unused]] AudoutParams audio_params {}; /// This is the event handle used to check if the audio buffer was released Kernel::EventPair buffer_event; diff --git a/src/core/hle/service/bcat/backend/backend.cpp b/src/core/hle/service/bcat/backend/backend.cpp new file mode 100644 index 000000000..9d6946bc5 --- /dev/null +++ b/src/core/hle/service/bcat/backend/backend.cpp @@ -0,0 +1,137 @@ +// Copyright 2019 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/hex_util.h" +#include "common/logging/log.h" +#include "core/core.h" +#include "core/hle/lock.h" +#include "core/hle/service/bcat/backend/backend.h" + +namespace Service::BCAT { + +ProgressServiceBackend::ProgressServiceBackend(std::string_view event_name) { + auto& kernel{Core::System::GetInstance().Kernel()}; + event = Kernel::WritableEvent::CreateEventPair( + kernel, Kernel::ResetType::Automatic, + std::string("ProgressServiceBackend:UpdateEvent:").append(event_name)); +} + +Kernel::SharedPtr<Kernel::ReadableEvent> ProgressServiceBackend::GetEvent() const { + return event.readable; +} + +DeliveryCacheProgressImpl& ProgressServiceBackend::GetImpl() { + return impl; +} + +void ProgressServiceBackend::SetNeedHLELock(bool need) { + need_hle_lock = need; +} + +void ProgressServiceBackend::SetTotalSize(u64 size) { + impl.total_bytes = size; + SignalUpdate(); +} + +void ProgressServiceBackend::StartConnecting() { + impl.status = DeliveryCacheProgressImpl::Status::Connecting; + SignalUpdate(); +} + +void ProgressServiceBackend::StartProcessingDataList() { + impl.status = DeliveryCacheProgressImpl::Status::ProcessingDataList; + SignalUpdate(); +} + +void ProgressServiceBackend::StartDownloadingFile(std::string_view dir_name, + std::string_view file_name, u64 file_size) { + impl.status = DeliveryCacheProgressImpl::Status::Downloading; + impl.current_downloaded_bytes = 0; + impl.current_total_bytes = file_size; + std::memcpy(impl.current_directory.data(), dir_name.data(), + std::min<u64>(dir_name.size(), 0x31ull)); + std::memcpy(impl.current_file.data(), file_name.data(), + std::min<u64>(file_name.size(), 0x31ull)); + SignalUpdate(); +} + +void ProgressServiceBackend::UpdateFileProgress(u64 downloaded) { + impl.current_downloaded_bytes = downloaded; + SignalUpdate(); +} + +void ProgressServiceBackend::FinishDownloadingFile() { + impl.total_downloaded_bytes += impl.current_total_bytes; + SignalUpdate(); +} + +void ProgressServiceBackend::CommitDirectory(std::string_view dir_name) { + impl.status = DeliveryCacheProgressImpl::Status::Committing; + impl.current_file.fill(0); + impl.current_downloaded_bytes = 0; + impl.current_total_bytes = 0; + std::memcpy(impl.current_directory.data(), dir_name.data(), + std::min<u64>(dir_name.size(), 0x31ull)); + SignalUpdate(); +} + +void ProgressServiceBackend::FinishDownload(ResultCode result) { + impl.total_downloaded_bytes = impl.total_bytes; + impl.status = DeliveryCacheProgressImpl::Status::Done; + impl.result = result; + SignalUpdate(); +} + +void ProgressServiceBackend::SignalUpdate() const { + if (need_hle_lock) { + std::lock_guard<std::recursive_mutex> lock(HLE::g_hle_lock); + event.writable->Signal(); + } else { + event.writable->Signal(); + } +} + +Backend::Backend(DirectoryGetter getter) : dir_getter(std::move(getter)) {} + +Backend::~Backend() = default; + +NullBackend::NullBackend(DirectoryGetter getter) : Backend(std::move(getter)) {} + +NullBackend::~NullBackend() = default; + +bool NullBackend::Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) { + LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, build_id={:016X}", title.title_id, + title.build_id); + + progress.FinishDownload(RESULT_SUCCESS); + return true; +} + +bool NullBackend::SynchronizeDirectory(TitleIDVersion title, std::string name, + ProgressServiceBackend& progress) { + LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, build_id={:016X}, name={}", title.title_id, + title.build_id, name); + + progress.FinishDownload(RESULT_SUCCESS); + return true; +} + +bool NullBackend::Clear(u64 title_id) { + LOG_DEBUG(Service_BCAT, "called, title_id={:016X}"); + + return true; +} + +void NullBackend::SetPassphrase(u64 title_id, const Passphrase& passphrase) { + LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, passphrase = {}", title_id, + Common::HexToString(passphrase)); +} + +std::optional<std::vector<u8>> NullBackend::GetLaunchParameter(TitleIDVersion title) { + LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, build_id={:016X}", title.title_id, + title.build_id); + return std::nullopt; +} + +} // namespace Service::BCAT diff --git a/src/core/hle/service/bcat/backend/backend.h b/src/core/hle/service/bcat/backend/backend.h new file mode 100644 index 000000000..51dbd3316 --- /dev/null +++ b/src/core/hle/service/bcat/backend/backend.h @@ -0,0 +1,150 @@ +// Copyright 2019 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <functional> +#include <optional> +#include <string> +#include <string_view> + +#include "common/common_types.h" +#include "core/file_sys/vfs_types.h" +#include "core/hle/kernel/readable_event.h" +#include "core/hle/kernel/writable_event.h" +#include "core/hle/result.h" + +namespace Service::BCAT { + +struct DeliveryCacheProgressImpl; + +using DirectoryGetter = std::function<FileSys::VirtualDir(u64)>; +using Passphrase = std::array<u8, 0x20>; + +struct TitleIDVersion { + u64 title_id; + u64 build_id; +}; + +using DirectoryName = std::array<char, 0x20>; +using FileName = std::array<char, 0x20>; + +struct DeliveryCacheProgressImpl { + enum class Status : s32 { + None = 0x0, + Queued = 0x1, + Connecting = 0x2, + ProcessingDataList = 0x3, + Downloading = 0x4, + Committing = 0x5, + Done = 0x9, + }; + + Status status; + ResultCode result = RESULT_SUCCESS; + DirectoryName current_directory; + FileName current_file; + s64 current_downloaded_bytes; ///< Bytes downloaded on current file. + s64 current_total_bytes; ///< Bytes total on current file. + s64 total_downloaded_bytes; ///< Bytes downloaded on overall download. + s64 total_bytes; ///< Bytes total on overall download. + INSERT_PADDING_BYTES( + 0x198); ///< Appears to be unused in official code, possibly reserved for future use. +}; +static_assert(sizeof(DeliveryCacheProgressImpl) == 0x200, + "DeliveryCacheProgressImpl has incorrect size."); + +// A class to manage the signalling to the game about BCAT download progress. +// Some of this class is implemented in module.cpp to avoid exposing the implementation structure. +class ProgressServiceBackend { + friend class IBcatService; + +public: + // Clients should call this with true if any of the functions are going to be called from a + // non-HLE thread and this class need to lock the hle mutex. (default is false) + void SetNeedHLELock(bool need); + + // Sets the number of bytes total in the entire download. + void SetTotalSize(u64 size); + + // Notifies the application that the backend has started connecting to the server. + void StartConnecting(); + // Notifies the application that the backend has begun accumulating and processing metadata. + void StartProcessingDataList(); + + // Notifies the application that a file is starting to be downloaded. + void StartDownloadingFile(std::string_view dir_name, std::string_view file_name, u64 file_size); + // Updates the progress of the current file to the size passed. + void UpdateFileProgress(u64 downloaded); + // Notifies the application that the current file has completed download. + void FinishDownloadingFile(); + + // Notifies the application that all files in this directory have completed and are being + // finalized. + void CommitDirectory(std::string_view dir_name); + + // Notifies the application that the operation completed with result code result. + void FinishDownload(ResultCode result); + +private: + explicit ProgressServiceBackend(std::string_view event_name); + + Kernel::SharedPtr<Kernel::ReadableEvent> GetEvent() const; + DeliveryCacheProgressImpl& GetImpl(); + + void SignalUpdate() const; + + DeliveryCacheProgressImpl impl{}; + Kernel::EventPair event; + bool need_hle_lock = false; +}; + +// A class representing an abstract backend for BCAT functionality. +class Backend { +public: + explicit Backend(DirectoryGetter getter); + virtual ~Backend(); + + // Called when the backend is needed to synchronize the data for the game with title ID and + // version in title. A ProgressServiceBackend object is provided to alert the application of + // status. + virtual bool Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) = 0; + // Very similar to Synchronize, but only for the directory provided. Backends should not alter + // the data for any other directories. + virtual bool SynchronizeDirectory(TitleIDVersion title, std::string name, + ProgressServiceBackend& progress) = 0; + + // Removes all cached data associated with title id provided. + virtual bool Clear(u64 title_id) = 0; + + // Sets the BCAT Passphrase to be used with the associated title ID. + virtual void SetPassphrase(u64 title_id, const Passphrase& passphrase) = 0; + + // Gets the launch parameter used by AM associated with the title ID and version provided. + virtual std::optional<std::vector<u8>> GetLaunchParameter(TitleIDVersion title) = 0; + +protected: + DirectoryGetter dir_getter; +}; + +// A backend of BCAT that provides no operation. +class NullBackend : public Backend { +public: + explicit NullBackend(DirectoryGetter getter); + ~NullBackend() override; + + bool Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) override; + bool SynchronizeDirectory(TitleIDVersion title, std::string name, + ProgressServiceBackend& progress) override; + + bool Clear(u64 title_id) override; + + void SetPassphrase(u64 title_id, const Passphrase& passphrase) override; + + std::optional<std::vector<u8>> GetLaunchParameter(TitleIDVersion title) override; +}; + +std::unique_ptr<Backend> CreateBackendFromSettings(DirectoryGetter getter); + +} // namespace Service::BCAT diff --git a/src/core/hle/service/bcat/backend/boxcat.cpp b/src/core/hle/service/bcat/backend/boxcat.cpp new file mode 100644 index 000000000..64022982b --- /dev/null +++ b/src/core/hle/service/bcat/backend/boxcat.cpp @@ -0,0 +1,504 @@ +// Copyright 2019 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <fmt/ostream.h> +#include <httplib.h> +#include <json.hpp> +#include <mbedtls/sha256.h> +#include "common/hex_util.h" +#include "common/logging/backend.h" +#include "common/logging/log.h" +#include "core/core.h" +#include "core/file_sys/vfs.h" +#include "core/file_sys/vfs_libzip.h" +#include "core/file_sys/vfs_vector.h" +#include "core/frontend/applets/error.h" +#include "core/hle/service/am/applets/applets.h" +#include "core/hle/service/bcat/backend/boxcat.h" +#include "core/settings.h" + +namespace { + +// Prevents conflicts with windows macro called CreateFile +FileSys::VirtualFile VfsCreateFileWrap(FileSys::VirtualDir dir, std::string_view name) { + return dir->CreateFile(name); +} + +// Prevents conflicts with windows macro called DeleteFile +bool VfsDeleteFileWrap(FileSys::VirtualDir dir, std::string_view name) { + return dir->DeleteFile(name); +} + +} // Anonymous namespace + +namespace Service::BCAT { + +constexpr ResultCode ERROR_GENERAL_BCAT_FAILURE{ErrorModule::BCAT, 1}; + +constexpr char BOXCAT_HOSTNAME[] = "api.yuzu-emu.org"; + +// Formatted using fmt with arg[0] = hex title id +constexpr char BOXCAT_PATHNAME_DATA[] = "/game-assets/{:016X}/boxcat"; +constexpr char BOXCAT_PATHNAME_LAUNCHPARAM[] = "/game-assets/{:016X}/launchparam"; + +constexpr char BOXCAT_PATHNAME_EVENTS[] = "/game-assets/boxcat/events"; + +constexpr char BOXCAT_API_VERSION[] = "1"; +constexpr char BOXCAT_CLIENT_TYPE[] = "yuzu"; + +// HTTP status codes for Boxcat +enum class ResponseStatus { + Ok = 200, ///< Operation completed successfully. + BadClientVersion = 301, ///< The Boxcat-Client-Version doesn't match the server. + NoUpdate = 304, ///< The digest provided would match the new data, no need to update. + NoMatchTitleId = 404, ///< The title ID provided doesn't have a boxcat implementation. + NoMatchBuildId = 406, ///< The build ID provided is blacklisted (potentially because of format + ///< issues or whatnot) and has no data. +}; + +enum class DownloadResult { + Success = 0, + NoResponse, + GeneralWebError, + NoMatchTitleId, + NoMatchBuildId, + InvalidContentType, + GeneralFSError, + BadClientVersion, +}; + +constexpr std::array<const char*, 8> DOWNLOAD_RESULT_LOG_MESSAGES{ + "Success", + "There was no response from the server.", + "There was a general web error code returned from the server.", + "The title ID of the current game doesn't have a boxcat implementation. If you believe an " + "implementation should be added, contact yuzu support.", + "The build ID of the current version of the game is marked as incompatible with the current " + "BCAT distribution. Try upgrading or downgrading your game version or contacting yuzu support.", + "The content type of the web response was invalid.", + "There was a general filesystem error while saving the zip file.", + "The server is either too new or too old to serve the request. Try using the latest version of " + "an official release of yuzu.", +}; + +std::ostream& operator<<(std::ostream& os, DownloadResult result) { + return os << DOWNLOAD_RESULT_LOG_MESSAGES.at(static_cast<std::size_t>(result)); +} + +constexpr u32 PORT = 443; +constexpr u32 TIMEOUT_SECONDS = 30; +[[maybe_unused]] constexpr u64 VFS_COPY_BLOCK_SIZE = 1ULL << 24; // 4MB + +namespace { + +std::string GetBINFilePath(u64 title_id) { + return fmt::format("{}bcat/{:016X}/launchparam.bin", + FileUtil::GetUserPath(FileUtil::UserPath::CacheDir), title_id); +} + +std::string GetZIPFilePath(u64 title_id) { + return fmt::format("{}bcat/{:016X}/data.zip", + FileUtil::GetUserPath(FileUtil::UserPath::CacheDir), title_id); +} + +// If the error is something the user should know about (build ID mismatch, bad client version), +// display an error. +void HandleDownloadDisplayResult(DownloadResult res) { + if (res == DownloadResult::Success || res == DownloadResult::NoResponse || + res == DownloadResult::GeneralWebError || res == DownloadResult::GeneralFSError || + res == DownloadResult::NoMatchTitleId || res == DownloadResult::InvalidContentType) { + return; + } + + const auto& frontend{Core::System::GetInstance().GetAppletManager().GetAppletFrontendSet()}; + frontend.error->ShowCustomErrorText( + ResultCode(-1), "There was an error while attempting to use Boxcat.", + DOWNLOAD_RESULT_LOG_MESSAGES[static_cast<std::size_t>(res)], [] {}); +} + +bool VfsRawCopyProgress(FileSys::VirtualFile src, FileSys::VirtualFile dest, + std::string_view dir_name, ProgressServiceBackend& progress, + std::size_t block_size = 0x1000) { + if (src == nullptr || dest == nullptr || !src->IsReadable() || !dest->IsWritable()) + return false; + if (!dest->Resize(src->GetSize())) + return false; + + progress.StartDownloadingFile(dir_name, src->GetName(), src->GetSize()); + + std::vector<u8> temp(std::min(block_size, src->GetSize())); + for (std::size_t i = 0; i < src->GetSize(); i += block_size) { + const auto read = std::min(block_size, src->GetSize() - i); + + if (src->Read(temp.data(), read, i) != read) { + return false; + } + + if (dest->Write(temp.data(), read, i) != read) { + return false; + } + + progress.UpdateFileProgress(i); + } + + progress.FinishDownloadingFile(); + + return true; +} + +bool VfsRawCopyDProgressSingle(FileSys::VirtualDir src, FileSys::VirtualDir dest, + ProgressServiceBackend& progress, std::size_t block_size = 0x1000) { + if (src == nullptr || dest == nullptr || !src->IsReadable() || !dest->IsWritable()) + return false; + + for (const auto& file : src->GetFiles()) { + const auto out_file = VfsCreateFileWrap(dest, file->GetName()); + if (!VfsRawCopyProgress(file, out_file, src->GetName(), progress, block_size)) { + return false; + } + } + progress.CommitDirectory(src->GetName()); + + return true; +} + +bool VfsRawCopyDProgress(FileSys::VirtualDir src, FileSys::VirtualDir dest, + ProgressServiceBackend& progress, std::size_t block_size = 0x1000) { + if (src == nullptr || dest == nullptr || !src->IsReadable() || !dest->IsWritable()) + return false; + + for (const auto& dir : src->GetSubdirectories()) { + const auto out = dest->CreateSubdirectory(dir->GetName()); + if (!VfsRawCopyDProgressSingle(dir, out, progress, block_size)) { + return false; + } + } + + return true; +} + +} // Anonymous namespace + +class Boxcat::Client { +public: + Client(std::string path, u64 title_id, u64 build_id) + : path(std::move(path)), title_id(title_id), build_id(build_id) {} + + DownloadResult DownloadDataZip() { + return DownloadInternal(fmt::format(BOXCAT_PATHNAME_DATA, title_id), TIMEOUT_SECONDS, + "application/zip"); + } + + DownloadResult DownloadLaunchParam() { + return DownloadInternal(fmt::format(BOXCAT_PATHNAME_LAUNCHPARAM, title_id), + TIMEOUT_SECONDS / 3, "application/octet-stream"); + } + +private: + DownloadResult DownloadInternal(const std::string& resolved_path, u32 timeout_seconds, + const std::string& content_type_name) { + if (client == nullptr) { + client = std::make_unique<httplib::SSLClient>(BOXCAT_HOSTNAME, PORT, timeout_seconds); + } + + httplib::Headers headers{ + {std::string("Game-Assets-API-Version"), std::string(BOXCAT_API_VERSION)}, + {std::string("Boxcat-Client-Type"), std::string(BOXCAT_CLIENT_TYPE)}, + {std::string("Game-Build-Id"), fmt::format("{:016X}", build_id)}, + }; + + if (FileUtil::Exists(path)) { + FileUtil::IOFile file{path, "rb"}; + if (file.IsOpen()) { + std::vector<u8> bytes(file.GetSize()); + file.ReadBytes(bytes.data(), bytes.size()); + const auto digest = DigestFile(bytes); + headers.insert({std::string("If-None-Match"), Common::HexToString(digest, false)}); + } + } + + const auto response = client->Get(resolved_path.c_str(), headers); + if (response == nullptr) + return DownloadResult::NoResponse; + + if (response->status == static_cast<int>(ResponseStatus::NoUpdate)) + return DownloadResult::Success; + if (response->status == static_cast<int>(ResponseStatus::BadClientVersion)) + return DownloadResult::BadClientVersion; + if (response->status == static_cast<int>(ResponseStatus::NoMatchTitleId)) + return DownloadResult::NoMatchTitleId; + if (response->status == static_cast<int>(ResponseStatus::NoMatchBuildId)) + return DownloadResult::NoMatchBuildId; + if (response->status != static_cast<int>(ResponseStatus::Ok)) + return DownloadResult::GeneralWebError; + + const auto content_type = response->headers.find("content-type"); + if (content_type == response->headers.end() || + content_type->second.find(content_type_name) == std::string::npos) { + return DownloadResult::InvalidContentType; + } + + FileUtil::CreateFullPath(path); + FileUtil::IOFile file{path, "wb"}; + if (!file.IsOpen()) + return DownloadResult::GeneralFSError; + if (!file.Resize(response->body.size())) + return DownloadResult::GeneralFSError; + if (file.WriteBytes(response->body.data(), response->body.size()) != response->body.size()) + return DownloadResult::GeneralFSError; + + return DownloadResult::Success; + } + + using Digest = std::array<u8, 0x20>; + static Digest DigestFile(std::vector<u8> bytes) { + Digest out{}; + mbedtls_sha256(bytes.data(), bytes.size(), out.data(), 0); + return out; + } + + std::unique_ptr<httplib::Client> client; + std::string path; + u64 title_id; + u64 build_id; +}; + +Boxcat::Boxcat(DirectoryGetter getter) : Backend(std::move(getter)) {} + +Boxcat::~Boxcat() = default; + +void SynchronizeInternal(DirectoryGetter dir_getter, TitleIDVersion title, + ProgressServiceBackend& progress, + std::optional<std::string> dir_name = {}) { + progress.SetNeedHLELock(true); + + if (Settings::values.bcat_boxcat_local) { + LOG_INFO(Service_BCAT, "Boxcat using local data by override, skipping download."); + const auto dir = dir_getter(title.title_id); + if (dir) + progress.SetTotalSize(dir->GetSize()); + progress.FinishDownload(RESULT_SUCCESS); + return; + } + + const auto zip_path{GetZIPFilePath(title.title_id)}; + Boxcat::Client client{zip_path, title.title_id, title.build_id}; + + progress.StartConnecting(); + + const auto res = client.DownloadDataZip(); + if (res != DownloadResult::Success) { + LOG_ERROR(Service_BCAT, "Boxcat synchronization failed with error '{}'!", res); + + if (res == DownloadResult::NoMatchBuildId || res == DownloadResult::NoMatchTitleId) { + FileUtil::Delete(zip_path); + } + + HandleDownloadDisplayResult(res); + progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE); + return; + } + + progress.StartProcessingDataList(); + + FileUtil::IOFile zip{zip_path, "rb"}; + const auto size = zip.GetSize(); + std::vector<u8> bytes(size); + if (!zip.IsOpen() || size == 0 || zip.ReadBytes(bytes.data(), bytes.size()) != bytes.size()) { + LOG_ERROR(Service_BCAT, "Boxcat failed to read ZIP file at path '{}'!", zip_path); + progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE); + return; + } + + const auto extracted = FileSys::ExtractZIP(std::make_shared<FileSys::VectorVfsFile>(bytes)); + if (extracted == nullptr) { + LOG_ERROR(Service_BCAT, "Boxcat failed to extract ZIP file!"); + progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE); + return; + } + + if (dir_name == std::nullopt) { + progress.SetTotalSize(extracted->GetSize()); + + const auto target_dir = dir_getter(title.title_id); + if (target_dir == nullptr || !VfsRawCopyDProgress(extracted, target_dir, progress)) { + LOG_ERROR(Service_BCAT, "Boxcat failed to copy extracted ZIP to target directory!"); + progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE); + return; + } + } else { + const auto target_dir = dir_getter(title.title_id); + if (target_dir == nullptr) { + LOG_ERROR(Service_BCAT, "Boxcat failed to get directory for title ID!"); + progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE); + return; + } + + const auto target_sub = target_dir->GetSubdirectory(*dir_name); + const auto source_sub = extracted->GetSubdirectory(*dir_name); + + progress.SetTotalSize(source_sub->GetSize()); + + std::vector<std::string> filenames; + { + const auto files = target_sub->GetFiles(); + std::transform(files.begin(), files.end(), std::back_inserter(filenames), + [](const auto& vfile) { return vfile->GetName(); }); + } + + for (const auto& filename : filenames) { + VfsDeleteFileWrap(target_sub, filename); + } + + if (target_sub == nullptr || source_sub == nullptr || + !VfsRawCopyDProgressSingle(source_sub, target_sub, progress)) { + LOG_ERROR(Service_BCAT, "Boxcat failed to copy extracted ZIP to target directory!"); + progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE); + return; + } + } + + progress.FinishDownload(RESULT_SUCCESS); +} + +bool Boxcat::Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) { + is_syncing.exchange(true); + std::thread([this, title, &progress] { SynchronizeInternal(dir_getter, title, progress); }) + .detach(); + return true; +} + +bool Boxcat::SynchronizeDirectory(TitleIDVersion title, std::string name, + ProgressServiceBackend& progress) { + is_syncing.exchange(true); + std::thread( + [this, title, name, &progress] { SynchronizeInternal(dir_getter, title, progress, name); }) + .detach(); + return true; +} + +bool Boxcat::Clear(u64 title_id) { + if (Settings::values.bcat_boxcat_local) { + LOG_INFO(Service_BCAT, "Boxcat using local data by override, skipping clear."); + return true; + } + + const auto dir = dir_getter(title_id); + + std::vector<std::string> dirnames; + + for (const auto& subdir : dir->GetSubdirectories()) + dirnames.push_back(subdir->GetName()); + + for (const auto& subdir : dirnames) { + if (!dir->DeleteSubdirectoryRecursive(subdir)) + return false; + } + + return true; +} + +void Boxcat::SetPassphrase(u64 title_id, const Passphrase& passphrase) { + LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, passphrase={}", title_id, + Common::HexToString(passphrase)); +} + +std::optional<std::vector<u8>> Boxcat::GetLaunchParameter(TitleIDVersion title) { + const auto path{GetBINFilePath(title.title_id)}; + + if (Settings::values.bcat_boxcat_local) { + LOG_INFO(Service_BCAT, "Boxcat using local data by override, skipping download."); + } else { + Boxcat::Client client{path, title.title_id, title.build_id}; + + const auto res = client.DownloadLaunchParam(); + if (res != DownloadResult::Success) { + LOG_ERROR(Service_BCAT, "Boxcat synchronization failed with error '{}'!", res); + + if (res == DownloadResult::NoMatchBuildId || res == DownloadResult::NoMatchTitleId) { + FileUtil::Delete(path); + } + + HandleDownloadDisplayResult(res); + return std::nullopt; + } + } + + FileUtil::IOFile bin{path, "rb"}; + const auto size = bin.GetSize(); + std::vector<u8> bytes(size); + if (!bin.IsOpen() || size == 0 || bin.ReadBytes(bytes.data(), bytes.size()) != bytes.size()) { + LOG_ERROR(Service_BCAT, "Boxcat failed to read launch parameter binary at path '{}'!", + path); + return std::nullopt; + } + + return bytes; +} + +Boxcat::StatusResult Boxcat::GetStatus(std::optional<std::string>& global, + std::map<std::string, EventStatus>& games) { + httplib::SSLClient client{BOXCAT_HOSTNAME, static_cast<int>(PORT), + static_cast<int>(TIMEOUT_SECONDS)}; + + httplib::Headers headers{ + {std::string("Game-Assets-API-Version"), std::string(BOXCAT_API_VERSION)}, + {std::string("Boxcat-Client-Type"), std::string(BOXCAT_CLIENT_TYPE)}, + }; + + const auto response = client.Get(BOXCAT_PATHNAME_EVENTS, headers); + if (response == nullptr) + return StatusResult::Offline; + + if (response->status == static_cast<int>(ResponseStatus::BadClientVersion)) + return StatusResult::BadClientVersion; + + try { + nlohmann::json json = nlohmann::json::parse(response->body); + + if (!json["online"].get<bool>()) + return StatusResult::Offline; + + if (json["global"].is_null()) + global = std::nullopt; + else + global = json["global"].get<std::string>(); + + if (json["games"].is_array()) { + for (const auto object : json["games"]) { + if (object.is_object() && object.find("name") != object.end()) { + EventStatus detail{}; + if (object["header"].is_string()) { + detail.header = object["header"].get<std::string>(); + } else { + detail.header = std::nullopt; + } + + if (object["footer"].is_string()) { + detail.footer = object["footer"].get<std::string>(); + } else { + detail.footer = std::nullopt; + } + + if (object["events"].is_array()) { + for (const auto& event : object["events"]) { + if (!event.is_string()) + continue; + detail.events.push_back(event.get<std::string>()); + } + } + + games.insert_or_assign(object["name"], std::move(detail)); + } + } + } + + return StatusResult::Success; + } catch (const nlohmann::json::parse_error& error) { + LOG_ERROR(Service_BCAT, "{}", error.what()); + return StatusResult::ParseError; + } +} + +} // namespace Service::BCAT diff --git a/src/core/hle/service/bcat/backend/boxcat.h b/src/core/hle/service/bcat/backend/boxcat.h new file mode 100644 index 000000000..601151189 --- /dev/null +++ b/src/core/hle/service/bcat/backend/boxcat.h @@ -0,0 +1,58 @@ +// Copyright 2019 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <atomic> +#include <map> +#include <optional> +#include "core/hle/service/bcat/backend/backend.h" + +namespace Service::BCAT { + +struct EventStatus { + std::optional<std::string> header; + std::optional<std::string> footer; + std::vector<std::string> events; +}; + +/// Boxcat is yuzu's custom backend implementation of Nintendo's BCAT service. It is free to use and +/// doesn't require a switch or nintendo account. The content is controlled by the yuzu team. +class Boxcat final : public Backend { + friend void SynchronizeInternal(DirectoryGetter dir_getter, TitleIDVersion title, + ProgressServiceBackend& progress, + std::optional<std::string> dir_name); + +public: + explicit Boxcat(DirectoryGetter getter); + ~Boxcat() override; + + bool Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) override; + bool SynchronizeDirectory(TitleIDVersion title, std::string name, + ProgressServiceBackend& progress) override; + + bool Clear(u64 title_id) override; + + void SetPassphrase(u64 title_id, const Passphrase& passphrase) override; + + std::optional<std::vector<u8>> GetLaunchParameter(TitleIDVersion title) override; + + enum class StatusResult { + Success, + Offline, + ParseError, + BadClientVersion, + }; + + static StatusResult GetStatus(std::optional<std::string>& global, + std::map<std::string, EventStatus>& games); + +private: + std::atomic_bool is_syncing{false}; + + class Client; + std::unique_ptr<Client> client; +}; + +} // namespace Service::BCAT diff --git a/src/core/hle/service/bcat/bcat.cpp b/src/core/hle/service/bcat/bcat.cpp index 179aa4949..8bb2528c9 100644 --- a/src/core/hle/service/bcat/bcat.cpp +++ b/src/core/hle/service/bcat/bcat.cpp @@ -6,11 +6,16 @@ namespace Service::BCAT { -BCAT::BCAT(std::shared_ptr<Module> module, const char* name) - : Module::Interface(std::move(module), name) { +BCAT::BCAT(Core::System& system, std::shared_ptr<Module> module, + FileSystem::FileSystemController& fsc, const char* name) + : Interface(system, std::move(module), fsc, name) { + // clang-format off static const FunctionInfo functions[] = { {0, &BCAT::CreateBcatService, "CreateBcatService"}, + {1, &BCAT::CreateDeliveryCacheStorageService, "CreateDeliveryCacheStorageService"}, + {2, &BCAT::CreateDeliveryCacheStorageServiceWithApplicationId, "CreateDeliveryCacheStorageServiceWithApplicationId"}, }; + // clang-format on RegisterHandlers(functions); } diff --git a/src/core/hle/service/bcat/bcat.h b/src/core/hle/service/bcat/bcat.h index 802bd689a..6354465fc 100644 --- a/src/core/hle/service/bcat/bcat.h +++ b/src/core/hle/service/bcat/bcat.h @@ -6,11 +6,16 @@ #include "core/hle/service/bcat/module.h" +namespace Core { +class System; +} + namespace Service::BCAT { class BCAT final : public Module::Interface { public: - explicit BCAT(std::shared_ptr<Module> module, const char* name); + explicit BCAT(Core::System& system, std::shared_ptr<Module> module, + FileSystem::FileSystemController& fsc, const char* name); ~BCAT() override; }; diff --git a/src/core/hle/service/bcat/module.cpp b/src/core/hle/service/bcat/module.cpp index b7bd738fc..4e4aa758b 100644 --- a/src/core/hle/service/bcat/module.cpp +++ b/src/core/hle/service/bcat/module.cpp @@ -2,34 +2,257 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include <cctype> +#include <mbedtls/md5.h> +#include "backend/boxcat.h" +#include "common/hex_util.h" #include "common/logging/log.h" +#include "common/string_util.h" +#include "core/file_sys/vfs.h" #include "core/hle/ipc_helpers.h" +#include "core/hle/kernel/process.h" +#include "core/hle/kernel/readable_event.h" +#include "core/hle/kernel/writable_event.h" +#include "core/hle/service/bcat/backend/backend.h" #include "core/hle/service/bcat/bcat.h" #include "core/hle/service/bcat/module.h" +#include "core/hle/service/filesystem/filesystem.h" +#include "core/settings.h" namespace Service::BCAT { +constexpr ResultCode ERROR_INVALID_ARGUMENT{ErrorModule::BCAT, 1}; +constexpr ResultCode ERROR_FAILED_OPEN_ENTITY{ErrorModule::BCAT, 2}; +constexpr ResultCode ERROR_ENTITY_ALREADY_OPEN{ErrorModule::BCAT, 6}; +constexpr ResultCode ERROR_NO_OPEN_ENTITY{ErrorModule::BCAT, 7}; + +// The command to clear the delivery cache just calls fs IFileSystem DeleteFile on all of the files +// and if any of them have a non-zero result it just forwards that result. This is the FS error code +// for permission denied, which is the closest approximation of this scenario. +constexpr ResultCode ERROR_FAILED_CLEAR_CACHE{ErrorModule::FS, 6400}; + +using BCATDigest = std::array<u8, 0x10>; + +namespace { + +u64 GetCurrentBuildID(const Core::System::CurrentBuildProcessID& id) { + u64 out{}; + std::memcpy(&out, id.data(), sizeof(u64)); + return out; +} + +// The digest is only used to determine if a file is unique compared to others of the same name. +// Since the algorithm isn't ever checked in game, MD5 is safe. +BCATDigest DigestFile(const FileSys::VirtualFile& file) { + BCATDigest out{}; + const auto bytes = file->ReadAllBytes(); + mbedtls_md5(bytes.data(), bytes.size(), out.data()); + return out; +} + +// For a name to be valid it must be non-empty, must have a null terminating character as the final +// char, can only contain numbers, letters, underscores and a hyphen if directory and a period if +// file. +bool VerifyNameValidInternal(Kernel::HLERequestContext& ctx, std::array<char, 0x20> name, + char match_char) { + const auto null_chars = std::count(name.begin(), name.end(), 0); + const auto bad_chars = std::count_if(name.begin(), name.end(), [match_char](char c) { + return !std::isalnum(static_cast<u8>(c)) && c != '_' && c != match_char && c != '\0'; + }); + if (null_chars == 0x20 || null_chars == 0 || bad_chars != 0 || name[0x1F] != '\0') { + LOG_ERROR(Service_BCAT, "Name passed was invalid!"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_INVALID_ARGUMENT); + return false; + } + + return true; +} + +bool VerifyNameValidDir(Kernel::HLERequestContext& ctx, DirectoryName name) { + return VerifyNameValidInternal(ctx, name, '-'); +} + +bool VerifyNameValidFile(Kernel::HLERequestContext& ctx, FileName name) { + return VerifyNameValidInternal(ctx, name, '.'); +} + +} // Anonymous namespace + +struct DeliveryCacheDirectoryEntry { + FileName name; + u64 size; + BCATDigest digest; +}; + +class IDeliveryCacheProgressService final : public ServiceFramework<IDeliveryCacheProgressService> { +public: + IDeliveryCacheProgressService(Kernel::SharedPtr<Kernel::ReadableEvent> event, + const DeliveryCacheProgressImpl& impl) + : ServiceFramework{"IDeliveryCacheProgressService"}, event(std::move(event)), impl(impl) { + // clang-format off + static const FunctionInfo functions[] = { + {0, &IDeliveryCacheProgressService::GetEvent, "GetEvent"}, + {1, &IDeliveryCacheProgressService::GetImpl, "GetImpl"}, + }; + // clang-format on + + RegisterHandlers(functions); + } + +private: + void GetEvent(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_BCAT, "called"); + + IPC::ResponseBuilder rb{ctx, 2, 1}; + rb.Push(RESULT_SUCCESS); + rb.PushCopyObjects(event); + } + + void GetImpl(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_BCAT, "called"); + + ctx.WriteBuffer(&impl, sizeof(DeliveryCacheProgressImpl)); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(RESULT_SUCCESS); + } + + Kernel::SharedPtr<Kernel::ReadableEvent> event; + const DeliveryCacheProgressImpl& impl; +}; + class IBcatService final : public ServiceFramework<IBcatService> { public: - IBcatService() : ServiceFramework("IBcatService") { + explicit IBcatService(Core::System& system_, Backend& backend_) + : ServiceFramework("IBcatService"), system{system_}, backend{backend_} { + // clang-format off static const FunctionInfo functions[] = { - {10100, nullptr, "RequestSyncDeliveryCache"}, - {10101, nullptr, "RequestSyncDeliveryCacheWithDirectoryName"}, + {10100, &IBcatService::RequestSyncDeliveryCache, "RequestSyncDeliveryCache"}, + {10101, &IBcatService::RequestSyncDeliveryCacheWithDirectoryName, "RequestSyncDeliveryCacheWithDirectoryName"}, {10200, nullptr, "CancelSyncDeliveryCacheRequest"}, {20100, nullptr, "RequestSyncDeliveryCacheWithApplicationId"}, {20101, nullptr, "RequestSyncDeliveryCacheWithApplicationIdAndDirectoryName"}, - {30100, nullptr, "SetPassphrase"}, + {30100, &IBcatService::SetPassphrase, "SetPassphrase"}, {30200, nullptr, "RegisterBackgroundDeliveryTask"}, {30201, nullptr, "UnregisterBackgroundDeliveryTask"}, {30202, nullptr, "BlockDeliveryTask"}, {30203, nullptr, "UnblockDeliveryTask"}, {90100, nullptr, "EnumerateBackgroundDeliveryTask"}, {90200, nullptr, "GetDeliveryList"}, - {90201, nullptr, "ClearDeliveryCacheStorage"}, + {90201, &IBcatService::ClearDeliveryCacheStorage, "ClearDeliveryCacheStorage"}, {90300, nullptr, "GetPushNotificationLog"}, }; + // clang-format on RegisterHandlers(functions); } + +private: + enum class SyncType { + Normal, + Directory, + Count, + }; + + std::shared_ptr<IDeliveryCacheProgressService> CreateProgressService(SyncType type) { + auto& backend{progress.at(static_cast<std::size_t>(type))}; + return std::make_shared<IDeliveryCacheProgressService>(backend.GetEvent(), + backend.GetImpl()); + } + + void RequestSyncDeliveryCache(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_BCAT, "called"); + + backend.Synchronize({system.CurrentProcess()->GetTitleID(), + GetCurrentBuildID(system.GetCurrentProcessBuildID())}, + progress.at(static_cast<std::size_t>(SyncType::Normal))); + + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(RESULT_SUCCESS); + rb.PushIpcInterface(CreateProgressService(SyncType::Normal)); + } + + void RequestSyncDeliveryCacheWithDirectoryName(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto name_raw = rp.PopRaw<DirectoryName>(); + const auto name = + Common::StringFromFixedZeroTerminatedBuffer(name_raw.data(), name_raw.size()); + + LOG_DEBUG(Service_BCAT, "called, name={}", name); + + backend.SynchronizeDirectory({system.CurrentProcess()->GetTitleID(), + GetCurrentBuildID(system.GetCurrentProcessBuildID())}, + name, + progress.at(static_cast<std::size_t>(SyncType::Directory))); + + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(RESULT_SUCCESS); + rb.PushIpcInterface(CreateProgressService(SyncType::Directory)); + } + + void SetPassphrase(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto title_id = rp.PopRaw<u64>(); + + const auto passphrase_raw = ctx.ReadBuffer(); + + LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, passphrase={}", title_id, + Common::HexToString(passphrase_raw)); + + if (title_id == 0) { + LOG_ERROR(Service_BCAT, "Invalid title ID!"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_INVALID_ARGUMENT); + } + + if (passphrase_raw.size() > 0x40) { + LOG_ERROR(Service_BCAT, "Passphrase too large!"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_INVALID_ARGUMENT); + return; + } + + Passphrase passphrase{}; + std::memcpy(passphrase.data(), passphrase_raw.data(), + std::min(passphrase.size(), passphrase_raw.size())); + + backend.SetPassphrase(title_id, passphrase); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(RESULT_SUCCESS); + } + + void ClearDeliveryCacheStorage(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto title_id = rp.PopRaw<u64>(); + + LOG_DEBUG(Service_BCAT, "called, title_id={:016X}", title_id); + + if (title_id == 0) { + LOG_ERROR(Service_BCAT, "Invalid title ID!"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_INVALID_ARGUMENT); + return; + } + + if (!backend.Clear(title_id)) { + LOG_ERROR(Service_BCAT, "Could not clear the directory successfully!"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_FAILED_CLEAR_CACHE); + return; + } + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(RESULT_SUCCESS); + } + + Core::System& system; + Backend& backend; + + std::array<ProgressServiceBackend, static_cast<std::size_t>(SyncType::Count)> progress{ + ProgressServiceBackend{"Normal"}, + ProgressServiceBackend{"Directory"}, + }; }; void Module::Interface::CreateBcatService(Kernel::HLERequestContext& ctx) { @@ -37,20 +260,332 @@ void Module::Interface::CreateBcatService(Kernel::HLERequestContext& ctx) { IPC::ResponseBuilder rb{ctx, 2, 0, 1}; rb.Push(RESULT_SUCCESS); - rb.PushIpcInterface<IBcatService>(); + rb.PushIpcInterface<IBcatService>(system, *backend); +} + +class IDeliveryCacheFileService final : public ServiceFramework<IDeliveryCacheFileService> { +public: + IDeliveryCacheFileService(FileSys::VirtualDir root_) + : ServiceFramework{"IDeliveryCacheFileService"}, root(std::move(root_)) { + // clang-format off + static const FunctionInfo functions[] = { + {0, &IDeliveryCacheFileService::Open, "Open"}, + {1, &IDeliveryCacheFileService::Read, "Read"}, + {2, &IDeliveryCacheFileService::GetSize, "GetSize"}, + {3, &IDeliveryCacheFileService::GetDigest, "GetDigest"}, + }; + // clang-format on + + RegisterHandlers(functions); + } + +private: + void Open(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto dir_name_raw = rp.PopRaw<DirectoryName>(); + const auto file_name_raw = rp.PopRaw<FileName>(); + + const auto dir_name = + Common::StringFromFixedZeroTerminatedBuffer(dir_name_raw.data(), dir_name_raw.size()); + const auto file_name = + Common::StringFromFixedZeroTerminatedBuffer(file_name_raw.data(), file_name_raw.size()); + + LOG_DEBUG(Service_BCAT, "called, dir_name={}, file_name={}", dir_name, file_name); + + if (!VerifyNameValidDir(ctx, dir_name_raw) || !VerifyNameValidFile(ctx, file_name_raw)) + return; + + if (current_file != nullptr) { + LOG_ERROR(Service_BCAT, "A file has already been opened on this interface!"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_ENTITY_ALREADY_OPEN); + return; + } + + const auto dir = root->GetSubdirectory(dir_name); + + if (dir == nullptr) { + LOG_ERROR(Service_BCAT, "The directory of name={} couldn't be opened!", dir_name); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_FAILED_OPEN_ENTITY); + return; + } + + current_file = dir->GetFile(file_name); + + if (current_file == nullptr) { + LOG_ERROR(Service_BCAT, "The file of name={} couldn't be opened!", file_name); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_FAILED_OPEN_ENTITY); + return; + } + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(RESULT_SUCCESS); + } + + void Read(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto offset{rp.PopRaw<u64>()}; + + auto size = ctx.GetWriteBufferSize(); + + LOG_DEBUG(Service_BCAT, "called, offset={:016X}, size={:016X}", offset, size); + + if (current_file == nullptr) { + LOG_ERROR(Service_BCAT, "There is no file currently open!"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_NO_OPEN_ENTITY); + } + + size = std::min<u64>(current_file->GetSize() - offset, size); + const auto buffer = current_file->ReadBytes(size, offset); + ctx.WriteBuffer(buffer); + + IPC::ResponseBuilder rb{ctx, 4}; + rb.Push(RESULT_SUCCESS); + rb.Push<u64>(buffer.size()); + } + + void GetSize(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_BCAT, "called"); + + if (current_file == nullptr) { + LOG_ERROR(Service_BCAT, "There is no file currently open!"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_NO_OPEN_ENTITY); + } + + IPC::ResponseBuilder rb{ctx, 4}; + rb.Push(RESULT_SUCCESS); + rb.Push<u64>(current_file->GetSize()); + } + + void GetDigest(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_BCAT, "called"); + + if (current_file == nullptr) { + LOG_ERROR(Service_BCAT, "There is no file currently open!"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_NO_OPEN_ENTITY); + } + + IPC::ResponseBuilder rb{ctx, 6}; + rb.Push(RESULT_SUCCESS); + rb.PushRaw(DigestFile(current_file)); + } + + FileSys::VirtualDir root; + FileSys::VirtualFile current_file; +}; + +class IDeliveryCacheDirectoryService final + : public ServiceFramework<IDeliveryCacheDirectoryService> { +public: + IDeliveryCacheDirectoryService(FileSys::VirtualDir root_) + : ServiceFramework{"IDeliveryCacheDirectoryService"}, root(std::move(root_)) { + // clang-format off + static const FunctionInfo functions[] = { + {0, &IDeliveryCacheDirectoryService::Open, "Open"}, + {1, &IDeliveryCacheDirectoryService::Read, "Read"}, + {2, &IDeliveryCacheDirectoryService::GetCount, "GetCount"}, + }; + // clang-format on + + RegisterHandlers(functions); + } + +private: + void Open(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto name_raw = rp.PopRaw<DirectoryName>(); + const auto name = + Common::StringFromFixedZeroTerminatedBuffer(name_raw.data(), name_raw.size()); + + LOG_DEBUG(Service_BCAT, "called, name={}", name); + + if (!VerifyNameValidDir(ctx, name_raw)) + return; + + if (current_dir != nullptr) { + LOG_ERROR(Service_BCAT, "A file has already been opened on this interface!"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_ENTITY_ALREADY_OPEN); + return; + } + + current_dir = root->GetSubdirectory(name); + + if (current_dir == nullptr) { + LOG_ERROR(Service_BCAT, "Failed to open the directory name={}!", name); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_FAILED_OPEN_ENTITY); + return; + } + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(RESULT_SUCCESS); + } + + void Read(Kernel::HLERequestContext& ctx) { + auto write_size = ctx.GetWriteBufferSize() / sizeof(DeliveryCacheDirectoryEntry); + + LOG_DEBUG(Service_BCAT, "called, write_size={:016X}", write_size); + + if (current_dir == nullptr) { + LOG_ERROR(Service_BCAT, "There is no open directory!"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_NO_OPEN_ENTITY); + return; + } + + const auto files = current_dir->GetFiles(); + write_size = std::min<u64>(write_size, files.size()); + std::vector<DeliveryCacheDirectoryEntry> entries(write_size); + std::transform( + files.begin(), files.begin() + write_size, entries.begin(), [](const auto& file) { + FileName name{}; + std::memcpy(name.data(), file->GetName().data(), + std::min(file->GetName().size(), name.size())); + return DeliveryCacheDirectoryEntry{name, file->GetSize(), DigestFile(file)}; + }); + + ctx.WriteBuffer(entries); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(RESULT_SUCCESS); + rb.Push(static_cast<u32>(write_size * sizeof(DeliveryCacheDirectoryEntry))); + } + + void GetCount(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_BCAT, "called"); + + if (current_dir == nullptr) { + LOG_ERROR(Service_BCAT, "There is no open directory!"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_NO_OPEN_ENTITY); + return; + } + + const auto files = current_dir->GetFiles(); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(RESULT_SUCCESS); + rb.Push(static_cast<u32>(files.size())); + } + + FileSys::VirtualDir root; + FileSys::VirtualDir current_dir; +}; + +class IDeliveryCacheStorageService final : public ServiceFramework<IDeliveryCacheStorageService> { +public: + IDeliveryCacheStorageService(FileSys::VirtualDir root_) + : ServiceFramework{"IDeliveryCacheStorageService"}, root(std::move(root_)) { + // clang-format off + static const FunctionInfo functions[] = { + {0, &IDeliveryCacheStorageService::CreateFileService, "CreateFileService"}, + {1, &IDeliveryCacheStorageService::CreateDirectoryService, "CreateDirectoryService"}, + {10, &IDeliveryCacheStorageService::EnumerateDeliveryCacheDirectory, "EnumerateDeliveryCacheDirectory"}, + }; + // clang-format on + + RegisterHandlers(functions); + + for (const auto& subdir : root->GetSubdirectories()) { + DirectoryName name{}; + std::memcpy(name.data(), subdir->GetName().data(), + std::min(sizeof(DirectoryName) - 1, subdir->GetName().size())); + entries.push_back(name); + } + } + +private: + void CreateFileService(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_BCAT, "called"); + + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(RESULT_SUCCESS); + rb.PushIpcInterface<IDeliveryCacheFileService>(root); + } + + void CreateDirectoryService(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_BCAT, "called"); + + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(RESULT_SUCCESS); + rb.PushIpcInterface<IDeliveryCacheDirectoryService>(root); + } + + void EnumerateDeliveryCacheDirectory(Kernel::HLERequestContext& ctx) { + auto size = ctx.GetWriteBufferSize() / sizeof(DirectoryName); + + LOG_DEBUG(Service_BCAT, "called, size={:016X}", size); + + size = std::min<u64>(size, entries.size() - next_read_index); + ctx.WriteBuffer(entries.data() + next_read_index, size * sizeof(DirectoryName)); + next_read_index += size; + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(RESULT_SUCCESS); + rb.Push(static_cast<u32>(size)); + } + + FileSys::VirtualDir root; + std::vector<DirectoryName> entries; + u64 next_read_index = 0; +}; + +void Module::Interface::CreateDeliveryCacheStorageService(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_BCAT, "called"); + + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(RESULT_SUCCESS); + rb.PushIpcInterface<IDeliveryCacheStorageService>( + fsc.GetBCATDirectory(system.CurrentProcess()->GetTitleID())); +} + +void Module::Interface::CreateDeliveryCacheStorageServiceWithApplicationId( + Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto title_id = rp.PopRaw<u64>(); + + LOG_DEBUG(Service_BCAT, "called, title_id={:016X}", title_id); + + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(RESULT_SUCCESS); + rb.PushIpcInterface<IDeliveryCacheStorageService>(fsc.GetBCATDirectory(title_id)); +} + +std::unique_ptr<Backend> CreateBackendFromSettings(DirectoryGetter getter) { + const auto backend = Settings::values.bcat_backend; + +#ifdef YUZU_ENABLE_BOXCAT + if (backend == "boxcat") + return std::make_unique<Boxcat>(std::move(getter)); +#endif + + return std::make_unique<NullBackend>(std::move(getter)); } -Module::Interface::Interface(std::shared_ptr<Module> module, const char* name) - : ServiceFramework(name), module(std::move(module)) {} +Module::Interface::Interface(Core::System& system_, std::shared_ptr<Module> module_, + FileSystem::FileSystemController& fsc_, const char* name) + : ServiceFramework(name), fsc{fsc_}, module{std::move(module_)}, + backend{CreateBackendFromSettings([&fsc_](u64 tid) { return fsc_.GetBCATDirectory(tid); })}, + system{system_} {} Module::Interface::~Interface() = default; -void InstallInterfaces(SM::ServiceManager& service_manager) { +void InstallInterfaces(Core::System& system) { auto module = std::make_shared<Module>(); - std::make_shared<BCAT>(module, "bcat:a")->InstallAsService(service_manager); - std::make_shared<BCAT>(module, "bcat:m")->InstallAsService(service_manager); - std::make_shared<BCAT>(module, "bcat:u")->InstallAsService(service_manager); - std::make_shared<BCAT>(module, "bcat:s")->InstallAsService(service_manager); + std::make_shared<BCAT>(system, module, system.GetFileSystemController(), "bcat:a") + ->InstallAsService(system.ServiceManager()); + std::make_shared<BCAT>(system, module, system.GetFileSystemController(), "bcat:m") + ->InstallAsService(system.ServiceManager()); + std::make_shared<BCAT>(system, module, system.GetFileSystemController(), "bcat:u") + ->InstallAsService(system.ServiceManager()); + std::make_shared<BCAT>(system, module, system.GetFileSystemController(), "bcat:s") + ->InstallAsService(system.ServiceManager()); } } // namespace Service::BCAT diff --git a/src/core/hle/service/bcat/module.h b/src/core/hle/service/bcat/module.h index f0d63cab0..e4ba23ba0 100644 --- a/src/core/hle/service/bcat/module.h +++ b/src/core/hle/service/bcat/module.h @@ -6,23 +6,46 @@ #include "core/hle/service/service.h" -namespace Service::BCAT { +namespace Core { +class System; +} + +namespace Service { + +namespace FileSystem { +class FileSystemController; +} // namespace FileSystem + +namespace BCAT { + +class Backend; class Module final { public: class Interface : public ServiceFramework<Interface> { public: - explicit Interface(std::shared_ptr<Module> module, const char* name); + explicit Interface(Core::System& system_, std::shared_ptr<Module> module_, + FileSystem::FileSystemController& fsc_, const char* name); ~Interface() override; void CreateBcatService(Kernel::HLERequestContext& ctx); + void CreateDeliveryCacheStorageService(Kernel::HLERequestContext& ctx); + void CreateDeliveryCacheStorageServiceWithApplicationId(Kernel::HLERequestContext& ctx); protected: + FileSystem::FileSystemController& fsc; + std::shared_ptr<Module> module; + std::unique_ptr<Backend> backend; + + private: + Core::System& system; }; }; /// Registers all BCAT services with the specified service manager. -void InstallInterfaces(SM::ServiceManager& service_manager); +void InstallInterfaces(Core::System& system); + +} // namespace BCAT -} // namespace Service::BCAT +} // namespace Service diff --git a/src/core/hle/service/es/es.cpp b/src/core/hle/service/es/es.cpp index af70d174d..f77ddd739 100644 --- a/src/core/hle/service/es/es.cpp +++ b/src/core/hle/service/es/es.cpp @@ -128,7 +128,7 @@ private: void CountCommonTicket(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_ETicket, "called"); - const auto count = keys.GetCommonTickets().size(); + const u32 count = static_cast<u32>(keys.GetCommonTickets().size()); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(RESULT_SUCCESS); @@ -138,7 +138,7 @@ private: void CountPersonalizedTicket(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_ETicket, "called"); - const auto count = keys.GetPersonalizedTickets().size(); + const u32 count = static_cast<u32>(keys.GetPersonalizedTickets().size()); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(RESULT_SUCCESS); @@ -150,7 +150,7 @@ private: if (keys.GetCommonTickets().empty()) out_entries = 0; else - out_entries = ctx.GetWriteBufferSize() / sizeof(u128); + out_entries = static_cast<u32>(ctx.GetWriteBufferSize() / sizeof(u128)); LOG_DEBUG(Service_ETicket, "called, entries={:016X}", out_entries); @@ -160,7 +160,7 @@ private: std::transform(tickets.begin(), tickets.end(), std::back_inserter(ids), [](const auto& pair) { return pair.first; }); - out_entries = std::min<u32>(ids.size(), out_entries); + out_entries = static_cast<u32>(std::min<std::size_t>(ids.size(), out_entries)); ctx.WriteBuffer(ids.data(), out_entries * sizeof(u128)); IPC::ResponseBuilder rb{ctx, 3}; @@ -173,7 +173,7 @@ private: if (keys.GetPersonalizedTickets().empty()) out_entries = 0; else - out_entries = ctx.GetWriteBufferSize() / sizeof(u128); + out_entries = static_cast<u32>(ctx.GetWriteBufferSize() / sizeof(u128)); LOG_DEBUG(Service_ETicket, "called, entries={:016X}", out_entries); @@ -183,7 +183,7 @@ private: std::transform(tickets.begin(), tickets.end(), std::back_inserter(ids), [](const auto& pair) { return pair.first; }); - out_entries = std::min<u32>(ids.size(), out_entries); + out_entries = static_cast<u32>(std::min<std::size_t>(ids.size(), out_entries)); ctx.WriteBuffer(ids.data(), out_entries * sizeof(u128)); IPC::ResponseBuilder rb{ctx, 3}; diff --git a/src/core/hle/service/fatal/fatal.cpp b/src/core/hle/service/fatal/fatal.cpp index b2ebf6240..2546d7595 100644 --- a/src/core/hle/service/fatal/fatal.cpp +++ b/src/core/hle/service/fatal/fatal.cpp @@ -66,7 +66,7 @@ enum class FatalType : u32 { static void GenerateErrorReport(Core::System& system, ResultCode error_code, const FatalInfo& info) { - const auto title_id = Core::CurrentProcess()->GetTitleID(); + const auto title_id = system.CurrentProcess()->GetTitleID(); std::string crash_report = fmt::format( "Yuzu {}-{} crash report\n" "Title ID: {:016x}\n" diff --git a/src/core/hle/service/filesystem/filesystem.cpp b/src/core/hle/service/filesystem/filesystem.cpp index 14cd0e322..11e5c56b7 100644 --- a/src/core/hle/service/filesystem/filesystem.cpp +++ b/src/core/hle/service/filesystem/filesystem.cpp @@ -241,7 +241,7 @@ ResultVal<FileSys::EntryType> VfsDirectoryServiceWrapper::GetEntryType( return FileSys::ERROR_PATH_NOT_FOUND; } -FileSystemController::FileSystemController() = default; +FileSystemController::FileSystemController(Core::System& system_) : system{system_} {} FileSystemController::~FileSystemController() = default; @@ -290,7 +290,7 @@ ResultVal<FileSys::VirtualFile> FileSystemController::OpenRomFSCurrentProcess() return ResultCode(-1); } - return romfs_factory->OpenCurrentProcess(); + return romfs_factory->OpenCurrentProcess(system.CurrentProcess()->GetTitleID()); } ResultVal<FileSys::VirtualFile> FileSystemController::OpenRomFS( @@ -447,10 +447,10 @@ FileSys::SaveDataSize FileSystemController::ReadSaveDataSize(FileSys::SaveDataTy FileSys::SaveDataSize new_size{SUFFICIENT_SAVE_DATA_SIZE, SUFFICIENT_SAVE_DATA_SIZE}; FileSys::NACP nacp; - const auto res = Core::System::GetInstance().GetAppLoader().ReadControlData(nacp); + const auto res = system.GetAppLoader().ReadControlData(nacp); if (res != Loader::ResultStatus::Success) { - FileSys::PatchManager pm{Core::CurrentProcess()->GetTitleID()}; + FileSys::PatchManager pm{system.CurrentProcess()->GetTitleID()}; auto [nacp_unique, discard] = pm.GetControlMetadata(); if (nacp_unique != nullptr) { @@ -674,6 +674,15 @@ FileSys::VirtualDir FileSystemController::GetModificationDumpRoot(u64 title_id) return bis_factory->GetModificationDumpRoot(title_id); } +FileSys::VirtualDir FileSystemController::GetBCATDirectory(u64 title_id) const { + LOG_TRACE(Service_FS, "Opening BCAT root for tid={:016X}", title_id); + + if (bis_factory == nullptr) + return nullptr; + + return bis_factory->GetBCATDirectory(title_id); +} + void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite) { if (overwrite) { bis_factory = nullptr; @@ -693,10 +702,10 @@ void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool ove if (bis_factory == nullptr) { bis_factory = std::make_unique<FileSys::BISFactory>(nand_directory, load_directory, dump_directory); - Core::System::GetInstance().RegisterContentProvider( - FileSys::ContentProviderUnionSlot::SysNAND, bis_factory->GetSystemNANDContents()); - Core::System::GetInstance().RegisterContentProvider( - FileSys::ContentProviderUnionSlot::UserNAND, bis_factory->GetUserNANDContents()); + system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::SysNAND, + bis_factory->GetSystemNANDContents()); + system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::UserNAND, + bis_factory->GetUserNANDContents()); } if (save_data_factory == nullptr) { @@ -705,8 +714,8 @@ void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool ove if (sdmc_factory == nullptr) { sdmc_factory = std::make_unique<FileSys::SDMCFactory>(std::move(sd_directory)); - Core::System::GetInstance().RegisterContentProvider(FileSys::ContentProviderUnionSlot::SDMC, - sdmc_factory->GetSDMCContents()); + system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::SDMC, + sdmc_factory->GetSDMCContents()); } } diff --git a/src/core/hle/service/filesystem/filesystem.h b/src/core/hle/service/filesystem/filesystem.h index 3e0c03ec0..1b0a6a949 100644 --- a/src/core/hle/service/filesystem/filesystem.h +++ b/src/core/hle/service/filesystem/filesystem.h @@ -10,6 +10,10 @@ #include "core/file_sys/vfs.h" #include "core/hle/result.h" +namespace Core { +class System; +} + namespace FileSys { class BISFactory; class RegisteredCache; @@ -52,7 +56,7 @@ enum class ImageDirectoryId : u32 { class FileSystemController { public: - FileSystemController(); + explicit FileSystemController(Core::System& system_); ~FileSystemController(); ResultCode RegisterRomFS(std::unique_ptr<FileSys::RomFSFactory>&& factory); @@ -110,6 +114,8 @@ public: FileSys::VirtualDir GetModificationLoadRoot(u64 title_id) const; FileSys::VirtualDir GetModificationDumpRoot(u64 title_id) const; + FileSys::VirtualDir GetBCATDirectory(u64 title_id) const; + // Creates the SaveData, SDMC, and BIS Factories. Should be called once and before any function // above is called. void CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite = true); @@ -123,6 +129,8 @@ private: std::unique_ptr<FileSys::XCI> gamecard; std::unique_ptr<FileSys::RegisteredCache> gamecard_registered; std::unique_ptr<FileSys::PlaceholderCache> gamecard_placeholder; + + Core::System& system; }; void InstallInterfaces(Core::System& system); diff --git a/src/core/hle/service/filesystem/fsp_srv.cpp b/src/core/hle/service/filesystem/fsp_srv.cpp index eb982ad49..cbd5466c1 100644 --- a/src/core/hle/service/filesystem/fsp_srv.cpp +++ b/src/core/hle/service/filesystem/fsp_srv.cpp @@ -803,7 +803,7 @@ void FSP_SRV::CreateSaveDataFileSystem(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; auto save_struct = rp.PopRaw<FileSys::SaveDataDescriptor>(); - auto save_create_struct = rp.PopRaw<std::array<u8, 0x40>>(); + [[maybe_unused]] auto save_create_struct = rp.PopRaw<std::array<u8, 0x40>>(); u128 uid = rp.PopRaw<u128>(); LOG_DEBUG(Service_FS, "called save_struct = {}, uid = {:016X}{:016X}", save_struct.DebugInfo(), diff --git a/src/core/hle/service/friend/friend.cpp b/src/core/hle/service/friend/friend.cpp index 42b4ee861..75dd9043b 100644 --- a/src/core/hle/service/friend/friend.cpp +++ b/src/core/hle/service/friend/friend.cpp @@ -237,7 +237,6 @@ private: }; Common::UUID uuid; - bool is_event_created = false; Kernel::EventPair notification_event; std::queue<SizedNotificationInfo> notifications; States states{}; diff --git a/src/core/hle/service/hid/controllers/debug_pad.cpp b/src/core/hle/service/hid/controllers/debug_pad.cpp index 8e8263f5b..1f2131ec8 100644 --- a/src/core/hle/service/hid/controllers/debug_pad.cpp +++ b/src/core/hle/service/hid/controllers/debug_pad.cpp @@ -11,11 +11,10 @@ namespace Service::HID { constexpr s32 HID_JOYSTICK_MAX = 0x7fff; -constexpr s32 HID_JOYSTICK_MIN = -0x7fff; +[[maybe_unused]] constexpr s32 HID_JOYSTICK_MIN = -0x7fff; enum class JoystickId : std::size_t { Joystick_Left, Joystick_Right }; -Controller_DebugPad::Controller_DebugPad(Core::System& system) - : ControllerBase(system), system(system) {} +Controller_DebugPad::Controller_DebugPad(Core::System& system) : ControllerBase(system) {} Controller_DebugPad::~Controller_DebugPad() = default; void Controller_DebugPad::OnInit() {} diff --git a/src/core/hle/service/hid/controllers/debug_pad.h b/src/core/hle/service/hid/controllers/debug_pad.h index 6c4de817e..555b29d76 100644 --- a/src/core/hle/service/hid/controllers/debug_pad.h +++ b/src/core/hle/service/hid/controllers/debug_pad.h @@ -89,6 +89,5 @@ private: buttons; std::array<std::unique_ptr<Input::AnalogDevice>, Settings::NativeAnalog::NUM_STICKS_HID> analogs; - Core::System& system; }; } // namespace Service::HID diff --git a/src/core/hle/service/hid/controllers/gesture.cpp b/src/core/hle/service/hid/controllers/gesture.cpp index 80da0a0d3..6e990dd00 100644 --- a/src/core/hle/service/hid/controllers/gesture.cpp +++ b/src/core/hle/service/hid/controllers/gesture.cpp @@ -10,8 +10,7 @@ namespace Service::HID { constexpr std::size_t SHARED_MEMORY_OFFSET = 0x3BA00; -Controller_Gesture::Controller_Gesture(Core::System& system) - : ControllerBase(system), system(system) {} +Controller_Gesture::Controller_Gesture(Core::System& system) : ControllerBase(system) {} Controller_Gesture::~Controller_Gesture() = default; void Controller_Gesture::OnInit() {} diff --git a/src/core/hle/service/hid/controllers/gesture.h b/src/core/hle/service/hid/controllers/gesture.h index 396897527..f650b8338 100644 --- a/src/core/hle/service/hid/controllers/gesture.h +++ b/src/core/hle/service/hid/controllers/gesture.h @@ -59,6 +59,5 @@ private: std::array<GestureState, 17> gesture_states; }; SharedMemory shared_memory{}; - Core::System& system; }; } // namespace Service::HID diff --git a/src/core/hle/service/hid/controllers/keyboard.cpp b/src/core/hle/service/hid/controllers/keyboard.cpp index e587b2e15..358cb9329 100644 --- a/src/core/hle/service/hid/controllers/keyboard.cpp +++ b/src/core/hle/service/hid/controllers/keyboard.cpp @@ -12,8 +12,7 @@ namespace Service::HID { constexpr std::size_t SHARED_MEMORY_OFFSET = 0x3800; constexpr u8 KEYS_PER_BYTE = 8; -Controller_Keyboard::Controller_Keyboard(Core::System& system) - : ControllerBase(system), system(system) {} +Controller_Keyboard::Controller_Keyboard(Core::System& system) : ControllerBase(system) {} Controller_Keyboard::~Controller_Keyboard() = default; void Controller_Keyboard::OnInit() {} diff --git a/src/core/hle/service/hid/controllers/keyboard.h b/src/core/hle/service/hid/controllers/keyboard.h index ef586f7eb..f3eef5936 100644 --- a/src/core/hle/service/hid/controllers/keyboard.h +++ b/src/core/hle/service/hid/controllers/keyboard.h @@ -53,6 +53,5 @@ private: keyboard_keys; std::array<std::unique_ptr<Input::ButtonDevice>, Settings::NativeKeyboard::NumKeyboardMods> keyboard_mods; - Core::System& system; }; } // namespace Service::HID diff --git a/src/core/hle/service/hid/controllers/mouse.cpp b/src/core/hle/service/hid/controllers/mouse.cpp index 88f2ca4c1..93d88ea50 100644 --- a/src/core/hle/service/hid/controllers/mouse.cpp +++ b/src/core/hle/service/hid/controllers/mouse.cpp @@ -11,7 +11,7 @@ namespace Service::HID { constexpr std::size_t SHARED_MEMORY_OFFSET = 0x3400; -Controller_Mouse::Controller_Mouse(Core::System& system) : ControllerBase(system), system(system) {} +Controller_Mouse::Controller_Mouse(Core::System& system) : ControllerBase(system) {} Controller_Mouse::~Controller_Mouse() = default; void Controller_Mouse::OnInit() {} diff --git a/src/core/hle/service/hid/controllers/mouse.h b/src/core/hle/service/hid/controllers/mouse.h index df2da6ae3..357ab7107 100644 --- a/src/core/hle/service/hid/controllers/mouse.h +++ b/src/core/hle/service/hid/controllers/mouse.h @@ -53,6 +53,5 @@ private: std::unique_ptr<Input::MouseDevice> mouse_device; std::array<std::unique_ptr<Input::ButtonDevice>, Settings::NativeMouseButton::NumMouseButtons> mouse_button_devices; - Core::System& system; }; } // namespace Service::HID diff --git a/src/core/hle/service/hid/controllers/npad.cpp b/src/core/hle/service/hid/controllers/npad.cpp index 44b668fbf..a2b25a796 100644 --- a/src/core/hle/service/hid/controllers/npad.cpp +++ b/src/core/hle/service/hid/controllers/npad.cpp @@ -20,7 +20,7 @@ namespace Service::HID { constexpr s32 HID_JOYSTICK_MAX = 0x7fff; -constexpr s32 HID_JOYSTICK_MIN = -0x7fff; +[[maybe_unused]] constexpr s32 HID_JOYSTICK_MIN = -0x7fff; constexpr std::size_t NPAD_OFFSET = 0x9A00; constexpr u32 BATTERY_FULL = 2; constexpr u32 MAX_NPAD_ID = 7; @@ -105,6 +105,8 @@ void Controller_NPad::InitNewlyAddedControler(std::size_t controller_idx) { controller.joy_styles.raw = 0; // Zero out controller.device_type.raw = 0; switch (controller_type) { + case NPadControllerType::None: + UNREACHABLE(); case NPadControllerType::Handheld: controller.joy_styles.handheld.Assign(1); controller.device_type.handheld.Assign(1); @@ -165,13 +167,14 @@ void Controller_NPad::InitNewlyAddedControler(std::size_t controller_idx) { controller.battery_level[0] = BATTERY_FULL; controller.battery_level[1] = BATTERY_FULL; controller.battery_level[2] = BATTERY_FULL; + styleset_changed_events[controller_idx].writable->Signal(); } void Controller_NPad::OnInit() { auto& kernel = system.Kernel(); for (std::size_t i = 0; i < styleset_changed_events.size(); i++) { styleset_changed_events[i] = Kernel::WritableEvent::CreateEventPair( - kernel, Kernel::ResetType::Automatic, fmt::format("npad:NpadStyleSetChanged_{}", i)); + kernel, Kernel::ResetType::Manual, fmt::format("npad:NpadStyleSetChanged_{}", i)); } if (!IsControllerActivated()) { @@ -238,7 +241,7 @@ void Controller_NPad::OnRelease() {} void Controller_NPad::RequestPadStateUpdate(u32 npad_id) { const auto controller_idx = NPadIdToIndex(npad_id); - const auto controller_type = connected_controllers[controller_idx].type; + [[maybe_unused]] const auto controller_type = connected_controllers[controller_idx].type; if (!connected_controllers[controller_idx].is_connected) { return; } @@ -345,6 +348,8 @@ void Controller_NPad::OnUpdate(const Core::Timing::CoreTiming& core_timing, u8* libnx_entry.connection_status.raw = 0; switch (controller_type) { + case NPadControllerType::None: + UNREACHABLE(); case NPadControllerType::Handheld: handheld_entry.connection_status.raw = 0; handheld_entry.connection_status.IsWired.Assign(1); @@ -433,7 +438,6 @@ void Controller_NPad::SetSupportedNPadIdTypes(u8* data, std::size_t length) { supported_npad_id_types.clear(); supported_npad_id_types.resize(length / sizeof(u32)); std::memcpy(supported_npad_id_types.data(), data, length); - bool had_controller_update = false; for (std::size_t i = 0; i < connected_controllers.size(); i++) { auto& controller = connected_controllers[i]; if (!controller.is_connected) { @@ -452,10 +456,6 @@ void Controller_NPad::SetSupportedNPadIdTypes(u8* data, std::size_t length) { controller.type = requested_controller; InitNewlyAddedControler(i); } - had_controller_update = true; - } - if (had_controller_update) { - styleset_changed_events[i].writable->Signal(); } } } @@ -481,7 +481,6 @@ void Controller_NPad::SetNpadMode(u32 npad_id, NPadAssignments assignment_mode) const std::size_t npad_index = NPadIdToIndex(npad_id); ASSERT(npad_index < shared_memory_entries.size()); if (shared_memory_entries[npad_index].pad_assignment != assignment_mode) { - styleset_changed_events[npad_index].writable->Signal(); shared_memory_entries[npad_index].pad_assignment = assignment_mode; } } @@ -507,7 +506,6 @@ Kernel::SharedPtr<Kernel::ReadableEvent> Controller_NPad::GetStyleSetChangedEven // TODO(ogniK): Figure out the best time to signal this event. This event seems that it should // be signalled at least once, and signaled after a new controller is connected? const auto& styleset_event = styleset_changed_events[NPadIdToIndex(npad_id)]; - styleset_event.writable->Signal(); return styleset_event.readable; } diff --git a/src/core/hle/service/hid/controllers/stubbed.cpp b/src/core/hle/service/hid/controllers/stubbed.cpp index 9b829341e..9e527d176 100644 --- a/src/core/hle/service/hid/controllers/stubbed.cpp +++ b/src/core/hle/service/hid/controllers/stubbed.cpp @@ -9,8 +9,7 @@ namespace Service::HID { -Controller_Stubbed::Controller_Stubbed(Core::System& system) - : ControllerBase(system), system(system) {} +Controller_Stubbed::Controller_Stubbed(Core::System& system) : ControllerBase(system) {} Controller_Stubbed::~Controller_Stubbed() = default; void Controller_Stubbed::OnInit() {} diff --git a/src/core/hle/service/hid/controllers/stubbed.h b/src/core/hle/service/hid/controllers/stubbed.h index 37d7d8538..4fa83ac85 100644 --- a/src/core/hle/service/hid/controllers/stubbed.h +++ b/src/core/hle/service/hid/controllers/stubbed.h @@ -30,6 +30,5 @@ public: private: bool smart_update{}; std::size_t common_offset{}; - Core::System& system; }; } // namespace Service::HID diff --git a/src/core/hle/service/hid/controllers/touchscreen.cpp b/src/core/hle/service/hid/controllers/touchscreen.cpp index 25912fd69..1c6e55566 100644 --- a/src/core/hle/service/hid/controllers/touchscreen.cpp +++ b/src/core/hle/service/hid/controllers/touchscreen.cpp @@ -13,8 +13,7 @@ namespace Service::HID { constexpr std::size_t SHARED_MEMORY_OFFSET = 0x400; -Controller_Touchscreen::Controller_Touchscreen(Core::System& system) - : ControllerBase(system), system(system) {} +Controller_Touchscreen::Controller_Touchscreen(Core::System& system) : ControllerBase(system) {} Controller_Touchscreen::~Controller_Touchscreen() = default; void Controller_Touchscreen::OnInit() {} diff --git a/src/core/hle/service/hid/controllers/touchscreen.h b/src/core/hle/service/hid/controllers/touchscreen.h index 3429c84db..a1d97269e 100644 --- a/src/core/hle/service/hid/controllers/touchscreen.h +++ b/src/core/hle/service/hid/controllers/touchscreen.h @@ -69,6 +69,5 @@ private: TouchScreenSharedMemory shared_memory{}; std::unique_ptr<Input::TouchDevice> touch_device; s64_le last_touch{}; - Core::System& system; }; } // namespace Service::HID diff --git a/src/core/hle/service/hid/controllers/xpad.cpp b/src/core/hle/service/hid/controllers/xpad.cpp index 1bce044b4..27511b27b 100644 --- a/src/core/hle/service/hid/controllers/xpad.cpp +++ b/src/core/hle/service/hid/controllers/xpad.cpp @@ -10,7 +10,7 @@ namespace Service::HID { constexpr std::size_t SHARED_MEMORY_OFFSET = 0x3C00; -Controller_XPad::Controller_XPad(Core::System& system) : ControllerBase(system), system(system) {} +Controller_XPad::Controller_XPad(Core::System& system) : ControllerBase(system) {} Controller_XPad::~Controller_XPad() = default; void Controller_XPad::OnInit() {} diff --git a/src/core/hle/service/hid/controllers/xpad.h b/src/core/hle/service/hid/controllers/xpad.h index c445ebec0..ad229787c 100644 --- a/src/core/hle/service/hid/controllers/xpad.h +++ b/src/core/hle/service/hid/controllers/xpad.h @@ -56,6 +56,5 @@ private: }; static_assert(sizeof(SharedMemory) == 0x1000, "SharedMemory is an invalid size"); SharedMemory shared_memory{}; - Core::System& system; }; } // namespace Service::HID diff --git a/src/core/hle/service/hid/hid.cpp b/src/core/hle/service/hid/hid.cpp index 8d76ba746..ba1da4181 100644 --- a/src/core/hle/service/hid/hid.cpp +++ b/src/core/hle/service/hid/hid.cpp @@ -38,8 +38,10 @@ namespace Service::HID { // Updating period for each HID device. // TODO(ogniK): Find actual polling rate of hid constexpr s64 pad_update_ticks = static_cast<s64>(Core::Timing::BASE_CLOCK_RATE / 66); -constexpr s64 accelerometer_update_ticks = static_cast<s64>(Core::Timing::BASE_CLOCK_RATE / 100); -constexpr s64 gyroscope_update_ticks = static_cast<s64>(Core::Timing::BASE_CLOCK_RATE / 100); +[[maybe_unused]] constexpr s64 accelerometer_update_ticks = + static_cast<s64>(Core::Timing::BASE_CLOCK_RATE / 100); +[[maybe_unused]] constexpr s64 gyroscope_update_ticks = + static_cast<s64>(Core::Timing::BASE_CLOCK_RATE / 100); constexpr std::size_t SHARED_MEMORY_SIZE = 0x40000; IAppletResource::IAppletResource(Core::System& system) @@ -193,7 +195,7 @@ Hid::Hid(Core::System& system) : ServiceFramework("hid"), system(system) { {101, &Hid::GetSupportedNpadStyleSet, "GetSupportedNpadStyleSet"}, {102, &Hid::SetSupportedNpadIdType, "SetSupportedNpadIdType"}, {103, &Hid::ActivateNpad, "ActivateNpad"}, - {104, nullptr, "DeactivateNpad"}, + {104, &Hid::DeactivateNpad, "DeactivateNpad"}, {106, &Hid::AcquireNpadStyleSetUpdateEventHandle, "AcquireNpadStyleSetUpdateEventHandle"}, {107, &Hid::DisconnectNpad, "DisconnectNpad"}, {108, &Hid::GetPlayerLedPattern, "GetPlayerLedPattern"}, @@ -468,6 +470,17 @@ void Hid::ActivateNpad(Kernel::HLERequestContext& ctx) { applet_resource->ActivateController(HidController::NPad); } +void Hid::DeactivateNpad(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto applet_resource_user_id{rp.Pop<u64>()}; + + LOG_DEBUG(Service_HID, "called, applet_resource_user_id={}", applet_resource_user_id); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(RESULT_SUCCESS); + applet_resource->DeactivateController(HidController::NPad); +} + void Hid::AcquireNpadStyleSetUpdateEventHandle(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; const auto npad_id{rp.Pop<u32>()}; diff --git a/src/core/hle/service/hid/hid.h b/src/core/hle/service/hid/hid.h index 35b663679..01852e019 100644 --- a/src/core/hle/service/hid/hid.h +++ b/src/core/hle/service/hid/hid.h @@ -99,6 +99,7 @@ private: void GetSupportedNpadStyleSet(Kernel::HLERequestContext& ctx); void SetSupportedNpadIdType(Kernel::HLERequestContext& ctx); void ActivateNpad(Kernel::HLERequestContext& ctx); + void DeactivateNpad(Kernel::HLERequestContext& ctx); void AcquireNpadStyleSetUpdateEventHandle(Kernel::HLERequestContext& ctx); void DisconnectNpad(Kernel::HLERequestContext& ctx); void GetPlayerLedPattern(Kernel::HLERequestContext& ctx); diff --git a/src/core/hle/service/ldr/ldr.cpp b/src/core/hle/service/ldr/ldr.cpp index 3164ca26e..499376bfc 100644 --- a/src/core/hle/service/ldr/ldr.cpp +++ b/src/core/hle/service/ldr/ldr.cpp @@ -163,7 +163,7 @@ public: return; } - if (Core::CurrentProcess()->GetTitleID() != header.title_id) { + if (system.CurrentProcess()->GetTitleID() != header.title_id) { LOG_ERROR(Service_LDR, "Attempting to load NRR with title ID other than current process. (actual " "{:016X})!", @@ -327,7 +327,7 @@ public: } // Load NRO as new executable module - auto* process = Core::CurrentProcess(); + auto* process = system.CurrentProcess(); auto& vm_manager = process->VMManager(); auto map_address = vm_manager.FindFreeRegion(nro_size + bss_size); @@ -411,7 +411,7 @@ public: return; } - auto& vm_manager = Core::CurrentProcess()->VMManager(); + auto& vm_manager = system.CurrentProcess()->VMManager(); const auto& nro_info = iter->second; // Unmap the mirrored memory diff --git a/src/core/hle/service/lm/lm.cpp b/src/core/hle/service/lm/lm.cpp index 2a61593e2..435f2d286 100644 --- a/src/core/hle/service/lm/lm.cpp +++ b/src/core/hle/service/lm/lm.cpp @@ -6,8 +6,10 @@ #include <string> #include "common/logging/log.h" +#include "common/scope_exit.h" #include "core/hle/ipc_helpers.h" #include "core/hle/service/lm/lm.h" +#include "core/hle/service/lm/manager.h" #include "core/hle/service/service.h" #include "core/memory.h" @@ -15,65 +17,16 @@ namespace Service::LM { class ILogger final : public ServiceFramework<ILogger> { public: - ILogger() : ServiceFramework("ILogger") { + ILogger(Manager& manager) : ServiceFramework("ILogger"), manager(manager) { static const FunctionInfo functions[] = { - {0x00000000, &ILogger::Initialize, "Initialize"}, - {0x00000001, &ILogger::SetDestination, "SetDestination"}, + {0, &ILogger::Log, "Log"}, + {1, &ILogger::SetDestination, "SetDestination"}, }; RegisterHandlers(functions); } private: - struct MessageHeader { - enum Flags : u32_le { - IsHead = 1, - IsTail = 2, - }; - enum Severity : u32_le { - Trace, - Info, - Warning, - Error, - Critical, - }; - - u64_le pid; - u64_le threadContext; - union { - BitField<0, 16, Flags> flags; - BitField<16, 8, Severity> severity; - BitField<24, 8, u32> verbosity; - }; - u32_le payload_size; - - bool IsHeadLog() const { - return flags & Flags::IsHead; - } - bool IsTailLog() const { - return flags & Flags::IsTail; - } - }; - static_assert(sizeof(MessageHeader) == 0x18, "MessageHeader is incorrect size"); - - /// Log field type - enum class Field : u8 { - Skip = 1, - Message = 2, - Line = 3, - Filename = 4, - Function = 5, - Module = 6, - Thread = 7, - }; - - /** - * ILogger::Initialize service function - * Inputs: - * 0: 0x00000000 - * Outputs: - * 0: ResultCode - */ - void Initialize(Kernel::HLERequestContext& ctx) { + void Log(Kernel::HLERequestContext& ctx) { // This function only succeeds - Get that out of the way IPC::ResponseBuilder rb{ctx, 2}; rb.Push(RESULT_SUCCESS); @@ -85,140 +38,70 @@ private: Memory::ReadBlock(addr, &header, sizeof(MessageHeader)); addr += sizeof(MessageHeader); - if (header.IsHeadLog()) { - log_stream.str(""); - log_stream.clear(); - } - - // Parse out log metadata - u32 line{}; - std::string module; - std::string message; - std::string filename; - std::string function; - std::string thread; + FieldMap fields; while (addr < end_addr) { - const Field field{static_cast<Field>(Memory::Read8(addr++))}; - const std::size_t length{Memory::Read8(addr++)}; + const auto field = static_cast<Field>(Memory::Read8(addr++)); + const auto length = Memory::Read8(addr++); if (static_cast<Field>(Memory::Read8(addr)) == Field::Skip) { ++addr; } - switch (field) { - case Field::Skip: - break; - case Field::Message: - message = Memory::ReadCString(addr, length); - break; - case Field::Line: - line = Memory::Read32(addr); - break; - case Field::Filename: - filename = Memory::ReadCString(addr, length); - break; - case Field::Function: - function = Memory::ReadCString(addr, length); - break; - case Field::Module: - module = Memory::ReadCString(addr, length); - break; - case Field::Thread: - thread = Memory::ReadCString(addr, length); - break; - } + SCOPE_EXIT({ addr += length; }); - addr += length; - } + if (field == Field::Skip) { + continue; + } - // Empty log - nothing to do here - if (log_stream.str().empty() && message.empty()) { - return; + std::vector<u8> data(length); + Memory::ReadBlock(addr, data.data(), length); + fields.emplace(field, std::move(data)); } - // Format a nicely printable string out of the log metadata - if (!filename.empty()) { - log_stream << filename << ':'; - } - if (!module.empty()) { - log_stream << module << ':'; - } - if (!function.empty()) { - log_stream << function << ':'; - } - if (line) { - log_stream << std::to_string(line) << ':'; - } - if (!thread.empty()) { - log_stream << thread << ':'; - } - if (log_stream.str().length() > 0 && log_stream.str().back() == ':') { - log_stream << ' '; - } - log_stream << message; - - if (header.IsTailLog()) { - switch (header.severity) { - case MessageHeader::Severity::Trace: - LOG_DEBUG(Debug_Emulated, "{}", log_stream.str()); - break; - case MessageHeader::Severity::Info: - LOG_INFO(Debug_Emulated, "{}", log_stream.str()); - break; - case MessageHeader::Severity::Warning: - LOG_WARNING(Debug_Emulated, "{}", log_stream.str()); - break; - case MessageHeader::Severity::Error: - LOG_ERROR(Debug_Emulated, "{}", log_stream.str()); - break; - case MessageHeader::Severity::Critical: - LOG_CRITICAL(Debug_Emulated, "{}", log_stream.str()); - break; - } - } + manager.Log({header, std::move(fields)}); } - // This service function is intended to be used as a way to - // redirect logging output to different destinations, however, - // given we always want to see the logging output, it's sufficient - // to do nothing and return success here. void SetDestination(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_LM, "called"); + IPC::RequestParser rp{ctx}; + const auto destination = rp.PopEnum<DestinationFlag>(); + + LOG_DEBUG(Service_LM, "called, destination={:08X}", static_cast<u32>(destination)); + + manager.SetDestination(destination); IPC::ResponseBuilder rb{ctx, 2}; rb.Push(RESULT_SUCCESS); } - std::ostringstream log_stream; + Manager& manager; }; class LM final : public ServiceFramework<LM> { public: - explicit LM() : ServiceFramework{"lm"} { + explicit LM(Manager& manager) : ServiceFramework{"lm"}, manager(manager) { + // clang-format off static const FunctionInfo functions[] = { - {0x00000000, &LM::OpenLogger, "OpenLogger"}, + {0, &LM::OpenLogger, "OpenLogger"}, }; + // clang-format on + RegisterHandlers(functions); } - /** - * LM::OpenLogger service function - * Inputs: - * 0: 0x00000000 - * Outputs: - * 0: ResultCode - */ +private: void OpenLogger(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_LM, "called"); IPC::ResponseBuilder rb{ctx, 2, 0, 1}; rb.Push(RESULT_SUCCESS); - rb.PushIpcInterface<ILogger>(); + rb.PushIpcInterface<ILogger>(manager); } + + Manager& manager; }; -void InstallInterfaces(SM::ServiceManager& service_manager) { - std::make_shared<LM>()->InstallAsService(service_manager); +void InstallInterfaces(Core::System& system) { + std::make_shared<LM>(system.GetLogManager())->InstallAsService(system.ServiceManager()); } } // namespace Service::LM diff --git a/src/core/hle/service/lm/lm.h b/src/core/hle/service/lm/lm.h index 7806ae27b..d40410b5c 100644 --- a/src/core/hle/service/lm/lm.h +++ b/src/core/hle/service/lm/lm.h @@ -4,13 +4,13 @@ #pragma once -namespace Service::SM { -class ServiceManager; +namespace Core { +class System; } namespace Service::LM { /// Registers all LM services with the specified service manager. -void InstallInterfaces(SM::ServiceManager& service_manager); +void InstallInterfaces(Core::System& system); } // namespace Service::LM diff --git a/src/core/hle/service/lm/manager.cpp b/src/core/hle/service/lm/manager.cpp new file mode 100644 index 000000000..b67081b86 --- /dev/null +++ b/src/core/hle/service/lm/manager.cpp @@ -0,0 +1,133 @@ +// Copyright 2019 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/assert.h" +#include "common/logging/log.h" +#include "common/string_util.h" +#include "core/hle/service/lm/manager.h" +#include "core/reporter.h" + +namespace Service::LM { + +std::ostream& operator<<(std::ostream& os, DestinationFlag dest) { + std::vector<std::string> array; + const auto check_single_flag = [dest, &array](DestinationFlag check, std::string name) { + if ((static_cast<u32>(check) & static_cast<u32>(dest)) != 0) { + array.emplace_back(std::move(name)); + } + }; + + check_single_flag(DestinationFlag::Default, "Default"); + check_single_flag(DestinationFlag::UART, "UART"); + check_single_flag(DestinationFlag::UARTSleeping, "UART (Sleeping)"); + + os << "["; + for (const auto& entry : array) { + os << entry << ", "; + } + return os << "]"; +} + +std::ostream& operator<<(std::ostream& os, MessageHeader::Severity severity) { + switch (severity) { + case MessageHeader::Severity::Trace: + return os << "Trace"; + case MessageHeader::Severity::Info: + return os << "Info"; + case MessageHeader::Severity::Warning: + return os << "Warning"; + case MessageHeader::Severity::Error: + return os << "Error"; + case MessageHeader::Severity::Critical: + return os << "Critical"; + default: + return os << fmt::format("{:08X}", static_cast<u32>(severity)); + } +} + +std::ostream& operator<<(std::ostream& os, Field field) { + switch (field) { + case Field::Skip: + return os << "Skip"; + case Field::Message: + return os << "Message"; + case Field::Line: + return os << "Line"; + case Field::Filename: + return os << "Filename"; + case Field::Function: + return os << "Function"; + case Field::Module: + return os << "Module"; + case Field::Thread: + return os << "Thread"; + default: + return os << fmt::format("{:08X}", static_cast<u32>(field)); + } +} + +std::string FormatField(Field type, const std::vector<u8>& data) { + switch (type) { + case Field::Skip: + return ""; + case Field::Line: + if (data.size() >= sizeof(u32)) { + u32 line; + std::memcpy(&line, data.data(), sizeof(u32)); + return fmt::format("{}", line); + } + return "[ERROR DECODING LINE NUMBER]"; + case Field::Message: + case Field::Filename: + case Field::Function: + case Field::Module: + case Field::Thread: + return Common::StringFromFixedZeroTerminatedBuffer( + reinterpret_cast<const char*>(data.data()), data.size()); + default: + UNIMPLEMENTED(); + } +} + +Manager::Manager(Core::Reporter& reporter) : reporter(reporter) {} + +Manager::~Manager() = default; + +void Manager::SetEnabled(bool enabled) { + this->enabled = enabled; +} + +void Manager::SetDestination(DestinationFlag destination) { + this->destination = destination; +} + +void Manager::Log(LogMessage message) { + if (message.header.IsHeadLog()) { + InitializeLog(); + } + + current_log.emplace_back(std::move(message)); + + if (current_log.back().header.IsTailLog()) { + FinalizeLog(); + } +} + +void Manager::Flush() { + FinalizeLog(); +} + +void Manager::InitializeLog() { + current_log.clear(); + + LOG_INFO(Service_LM, "Initialized new log session"); +} + +void Manager::FinalizeLog() { + reporter.SaveLogReport(static_cast<u32>(destination), std::move(current_log)); + + LOG_INFO(Service_LM, "Finalized current log session"); +} + +} // namespace Service::LM diff --git a/src/core/hle/service/lm/manager.h b/src/core/hle/service/lm/manager.h new file mode 100644 index 000000000..544e636ba --- /dev/null +++ b/src/core/hle/service/lm/manager.h @@ -0,0 +1,106 @@ +// Copyright 2019 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <map> +#include <ostream> +#include <vector> +#include "common/bit_field.h" +#include "common/common_types.h" +#include "common/swap.h" + +namespace Core { +class Reporter; +} + +namespace Service::LM { + +enum class DestinationFlag : u32 { + Default = 1, + UART = 2, + UARTSleeping = 4, + + All = 0xFFFF, +}; + +struct MessageHeader { + enum Flags : u32_le { + IsHead = 1, + IsTail = 2, + }; + enum Severity : u32_le { + Trace, + Info, + Warning, + Error, + Critical, + }; + + u64_le pid; + u64_le thread_context; + union { + BitField<0, 16, Flags> flags; + BitField<16, 8, Severity> severity; + BitField<24, 8, u32> verbosity; + }; + u32_le payload_size; + + bool IsHeadLog() const { + return flags & IsHead; + } + bool IsTailLog() const { + return flags & IsTail; + } +}; +static_assert(sizeof(MessageHeader) == 0x18, "MessageHeader is incorrect size"); + +enum class Field : u8 { + Skip = 1, + Message = 2, + Line = 3, + Filename = 4, + Function = 5, + Module = 6, + Thread = 7, +}; + +std::ostream& operator<<(std::ostream& os, DestinationFlag dest); +std::ostream& operator<<(std::ostream& os, MessageHeader::Severity severity); +std::ostream& operator<<(std::ostream& os, Field field); + +using FieldMap = std::map<Field, std::vector<u8>>; + +struct LogMessage { + MessageHeader header; + FieldMap fields; +}; + +std::string FormatField(Field type, const std::vector<u8>& data); + +class Manager { +public: + explicit Manager(Core::Reporter& reporter); + ~Manager(); + + void SetEnabled(bool enabled); + void SetDestination(DestinationFlag destination); + + void Log(LogMessage message); + + void Flush(); + +private: + void InitializeLog(); + void FinalizeLog(); + + bool enabled = true; + DestinationFlag destination = DestinationFlag::All; + + std::vector<LogMessage> current_log; + + Core::Reporter& reporter; +}; + +} // namespace Service::LM diff --git a/src/core/hle/service/nfp/nfp.cpp b/src/core/hle/service/nfp/nfp.cpp index a42c22d44..aa886cd3e 100644 --- a/src/core/hle/service/nfp/nfp.cpp +++ b/src/core/hle/service/nfp/nfp.cpp @@ -18,8 +18,8 @@ namespace Service::NFP { namespace ErrCodes { -constexpr ResultCode ERR_TAG_FAILED(ErrorModule::NFP, - -1); // TODO(ogniK): Find the actual error code +[[maybe_unused]] constexpr ResultCode ERR_TAG_FAILED(ErrorModule::NFP, + -1); // TODO(ogniK): Find the actual error code constexpr ResultCode ERR_NO_APPLICATION_AREA(ErrorModule::NFP, 152); } // namespace ErrCodes @@ -35,7 +35,7 @@ Module::Interface::~Interface() = default; class IUser final : public ServiceFramework<IUser> { public: IUser(Module::Interface& nfp_interface, Core::System& system) - : ServiceFramework("NFP::IUser"), nfp_interface(nfp_interface), system(system) { + : ServiceFramework("NFP::IUser"), nfp_interface(nfp_interface) { static const FunctionInfo functions[] = { {0, &IUser::Initialize, "Initialize"}, {1, &IUser::Finalize, "Finalize"}, @@ -183,6 +183,8 @@ private: case DeviceState::TagRemoved: device_state = DeviceState::Initialized; break; + default: + break; } IPC::ResponseBuilder rb{ctx, 2}; rb.Push(RESULT_SUCCESS); @@ -324,7 +326,6 @@ private: Kernel::EventPair deactivate_event; Kernel::EventPair availability_change_event; const Module::Interface& nfp_interface; - Core::System& system; }; void Module::Interface::CreateUserInterface(Kernel::HLERequestContext& ctx) { diff --git a/src/core/hle/service/nifm/nifm.cpp b/src/core/hle/service/nifm/nifm.cpp index 24d1813a7..756a2af57 100644 --- a/src/core/hle/service/nifm/nifm.cpp +++ b/src/core/hle/service/nifm/nifm.cpp @@ -12,6 +12,13 @@ namespace Service::NIFM { +enum class RequestState : u32 { + NotSubmitted = 1, + Error = 1, ///< The duplicate 1 is intentional; it means both not submitted and error on HW. + Pending = 2, + Connected = 3, +}; + class IScanRequest final : public ServiceFramework<IScanRequest> { public: explicit IScanRequest() : ServiceFramework("IScanRequest") { @@ -81,7 +88,7 @@ private: IPC::ResponseBuilder rb{ctx, 3}; rb.Push(RESULT_SUCCESS); - rb.Push<u32>(0); + rb.PushEnum(RequestState::Connected); } void GetResult(Kernel::HLERequestContext& ctx) { @@ -189,14 +196,14 @@ private: IPC::ResponseBuilder rb{ctx, 3}; rb.Push(RESULT_SUCCESS); - rb.Push<u8>(0); + rb.Push<u8>(1); } void IsAnyInternetRequestAccepted(Kernel::HLERequestContext& ctx) { LOG_WARNING(Service_NIFM, "(STUBBED) called"); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(RESULT_SUCCESS); - rb.Push<u8>(0); + rb.Push<u8>(1); } Core::System& system; }; diff --git a/src/core/hle/service/ns/pl_u.cpp b/src/core/hle/service/ns/pl_u.cpp index 7dcdb4a07..f64535237 100644 --- a/src/core/hle/service/ns/pl_u.cpp +++ b/src/core/hle/service/ns/pl_u.cpp @@ -324,14 +324,14 @@ void PL_U::GetSharedMemoryAddressOffset(Kernel::HLERequestContext& ctx) { void PL_U::GetSharedMemoryNativeHandle(Kernel::HLERequestContext& ctx) { // Map backing memory for the font data LOG_DEBUG(Service_NS, "called"); - Core::CurrentProcess()->VMManager().MapMemoryBlock(SHARED_FONT_MEM_VADDR, impl->shared_font, 0, - SHARED_FONT_MEM_SIZE, - Kernel::MemoryState::Shared); + system.CurrentProcess()->VMManager().MapMemoryBlock(SHARED_FONT_MEM_VADDR, impl->shared_font, 0, + SHARED_FONT_MEM_SIZE, + Kernel::MemoryState::Shared); // Create shared font memory object auto& kernel = system.Kernel(); impl->shared_font_mem = Kernel::SharedMemory::Create( - kernel, Core::CurrentProcess(), SHARED_FONT_MEM_SIZE, Kernel::MemoryPermission::ReadWrite, + kernel, system.CurrentProcess(), SHARED_FONT_MEM_SIZE, Kernel::MemoryPermission::ReadWrite, Kernel::MemoryPermission::Read, SHARED_FONT_MEM_VADDR, Kernel::MemoryRegion::BASE, "PL_U:shared_font_mem"); diff --git a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp index 6bc053f27..07c88465e 100644 --- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp +++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp @@ -45,6 +45,8 @@ u32 nvhost_as_gpu::ioctl(Ioctl command, const std::vector<u8>& input, const std: return GetVARegions(input, output); case IoctlCommand::IocUnmapBufferCommand: return UnmapBuffer(input, output); + default: + break; } if (static_cast<IoctlCommand>(command.cmd.Value()) == IoctlCommand::IocRemapCommand) diff --git a/src/core/hle/service/nvdrv/devices/nvhost_ctrl.cpp b/src/core/hle/service/nvdrv/devices/nvhost_ctrl.cpp index ff6b1abae..eb88fee1b 100644 --- a/src/core/hle/service/nvdrv/devices/nvhost_ctrl.cpp +++ b/src/core/hle/service/nvdrv/devices/nvhost_ctrl.cpp @@ -38,9 +38,10 @@ u32 nvhost_ctrl::ioctl(Ioctl command, const std::vector<u8>& input, const std::v return IocCtrlEventUnregister(input, output); case IoctlCommand::IocCtrlEventSignalCommand: return IocCtrlEventSignal(input, output); + default: + UNIMPLEMENTED_MSG("Unimplemented ioctl"); + return 0; } - UNIMPLEMENTED_MSG("Unimplemented ioctl"); - return 0; } u32 nvhost_ctrl::NvOsGetConfigU32(const std::vector<u8>& input, std::vector<u8>& output) { diff --git a/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp index 389ace76f..cc2192e5c 100644 --- a/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp +++ b/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp @@ -40,9 +40,10 @@ u32 nvhost_ctrl_gpu::ioctl(Ioctl command, const std::vector<u8>& input, return FlushL2(input, output); case IoctlCommand::IocGetGpuTime: return GetGpuTime(input, output); + default: + UNIMPLEMENTED_MSG("Unimplemented ioctl"); + return 0; } - UNIMPLEMENTED_MSG("Unimplemented ioctl"); - return 0; } u32 nvhost_ctrl_gpu::GetCharacteristics(const std::vector<u8>& input, std::vector<u8>& output, diff --git a/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp index 2b8d1bef6..9de0ace22 100644 --- a/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp +++ b/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp @@ -44,6 +44,8 @@ u32 nvhost_gpu::ioctl(Ioctl command, const std::vector<u8>& input, const std::ve return GetWaitbase(input, output); case IoctlCommand::IocChannelSetTimeoutCommand: return ChannelSetTimeout(input, output); + default: + break; } if (command.group == NVGPU_IOCTL_MAGIC) { diff --git a/src/core/hle/service/service.cpp b/src/core/hle/service/service.cpp index 831a427de..7c5302017 100644 --- a/src/core/hle/service/service.cpp +++ b/src/core/hle/service/service.cpp @@ -208,7 +208,7 @@ void Init(std::shared_ptr<SM::ServiceManager>& sm, Core::System& system) { AOC::InstallInterfaces(*sm, system); APM::InstallInterfaces(system); Audio::InstallInterfaces(*sm, system); - BCAT::InstallInterfaces(*sm); + BCAT::InstallInterfaces(system); BPC::InstallInterfaces(*sm); BtDrv::InstallInterfaces(*sm, system); BTM::InstallInterfaces(*sm, system); @@ -226,7 +226,7 @@ void Init(std::shared_ptr<SM::ServiceManager>& sm, Core::System& system) { LBL::InstallInterfaces(*sm); LDN::InstallInterfaces(*sm); LDR::InstallInterfaces(*sm, system); - LM::InstallInterfaces(*sm); + LM::InstallInterfaces(system); Migration::InstallInterfaces(*sm); Mii::InstallInterfaces(*sm); MM::InstallInterfaces(*sm); diff --git a/src/core/loader/nso.cpp b/src/core/loader/nso.cpp index e75c700ad..f629892ae 100644 --- a/src/core/loader/nso.cpp +++ b/src/core/loader/nso.cpp @@ -150,6 +150,7 @@ std::optional<VAddr> AppLoader_NSO::LoadModule(Kernel::Process& process, // Apply cheats if they exist and the program has a valid title ID if (pm) { auto& system = Core::System::GetInstance(); + system.SetCurrentProcessBuildID(nso_header.build_id); const auto cheats = pm->CreateCheatList(system, nso_header.build_id); if (!cheats.empty()) { system.RegisterCheatList(cheats, nso_header.build_id, load_base, image_size); diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 9e030789d..fa49f3dd0 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -146,7 +146,7 @@ static u8* GetPointerFromVMA(const Kernel::Process& process, VAddr vaddr) { * using a VMA from the current process. */ static u8* GetPointerFromVMA(VAddr vaddr) { - return GetPointerFromVMA(*Core::CurrentProcess(), vaddr); + return GetPointerFromVMA(*Core::System::GetInstance().CurrentProcess(), vaddr); } template <typename T> @@ -226,7 +226,7 @@ bool IsValidVirtualAddress(const Kernel::Process& process, const VAddr vaddr) { } bool IsValidVirtualAddress(const VAddr vaddr) { - return IsValidVirtualAddress(*Core::CurrentProcess(), vaddr); + return IsValidVirtualAddress(*Core::System::GetInstance().CurrentProcess(), vaddr); } bool IsKernelVirtualAddress(const VAddr vaddr) { @@ -387,7 +387,7 @@ void ReadBlock(const Kernel::Process& process, const VAddr src_addr, void* dest_ } void ReadBlock(const VAddr src_addr, void* dest_buffer, const std::size_t size) { - ReadBlock(*Core::CurrentProcess(), src_addr, dest_buffer, size); + ReadBlock(*Core::System::GetInstance().CurrentProcess(), src_addr, dest_buffer, size); } void Write8(const VAddr addr, const u8 data) { @@ -450,7 +450,7 @@ void WriteBlock(const Kernel::Process& process, const VAddr dest_addr, const voi } void WriteBlock(const VAddr dest_addr, const void* src_buffer, const std::size_t size) { - WriteBlock(*Core::CurrentProcess(), dest_addr, src_buffer, size); + WriteBlock(*Core::System::GetInstance().CurrentProcess(), dest_addr, src_buffer, size); } void ZeroBlock(const Kernel::Process& process, const VAddr dest_addr, const std::size_t size) { @@ -539,7 +539,7 @@ void CopyBlock(const Kernel::Process& process, VAddr dest_addr, VAddr src_addr, } void CopyBlock(VAddr dest_addr, VAddr src_addr, std::size_t size) { - CopyBlock(*Core::CurrentProcess(), dest_addr, src_addr, size); + CopyBlock(*Core::System::GetInstance().CurrentProcess(), dest_addr, src_addr, size); } } // namespace Memory diff --git a/src/core/reporter.cpp b/src/core/reporter.cpp index 9c657929e..6f4af77fd 100644 --- a/src/core/reporter.cpp +++ b/src/core/reporter.cpp @@ -7,6 +7,7 @@ #include <fmt/chrono.h> #include <fmt/format.h> +#include <fmt/ostream.h> #include <json.hpp> #include "common/file_util.h" @@ -17,6 +18,7 @@ #include "core/hle/kernel/hle_ipc.h" #include "core/hle/kernel/process.h" #include "core/hle/result.h" +#include "core/hle/service/lm/manager.h" #include "core/reporter.h" #include "core/settings.h" @@ -354,6 +356,55 @@ void Reporter::SaveErrorReport(u64 title_id, ResultCode result, SaveToFile(std::move(out), GetPath("error_report", title_id, timestamp)); } +void Reporter::SaveLogReport(u32 destination, std::vector<Service::LM::LogMessage> messages) const { + if (!IsReportingEnabled()) { + return; + } + + const auto timestamp = GetTimestamp(); + json out; + + out["yuzu_version"] = GetYuzuVersionData(); + out["report_common"] = + GetReportCommonData(system.CurrentProcess()->GetTitleID(), RESULT_SUCCESS, timestamp); + + out["log_destination"] = + fmt::format("{}", static_cast<Service::LM::DestinationFlag>(destination)); + + auto json_messages = json::array(); + std::transform(messages.begin(), messages.end(), std::back_inserter(json_messages), + [](const Service::LM::LogMessage& message) { + json out; + out["is_head"] = fmt::format("{}", message.header.IsHeadLog()); + out["is_tail"] = fmt::format("{}", message.header.IsTailLog()); + out["pid"] = fmt::format("{:016X}", message.header.pid); + out["thread_context"] = + fmt::format("{:016X}", message.header.thread_context); + out["payload_size"] = fmt::format("{:016X}", message.header.payload_size); + out["flags"] = fmt::format("{:04X}", message.header.flags.Value()); + out["severity"] = fmt::format("{}", message.header.severity.Value()); + out["verbosity"] = fmt::format("{:02X}", message.header.verbosity); + + auto fields = json::array(); + std::transform(message.fields.begin(), message.fields.end(), + std::back_inserter(fields), [](const auto& kv) { + json out; + out["type"] = fmt::format("{}", kv.first); + out["data"] = + Service::LM::FormatField(kv.first, kv.second); + return out; + }); + + out["fields"] = std::move(fields); + return out; + }); + + out["log_messages"] = std::move(json_messages); + + SaveToFile(std::move(out), + GetPath("log_report", system.CurrentProcess()->GetTitleID(), timestamp)); +} + void Reporter::SaveFilesystemAccessReport(Service::FileSystem::LogMode log_mode, std::string log_message) const { if (!IsReportingEnabled()) diff --git a/src/core/reporter.h b/src/core/reporter.h index f08aa11fb..380941b1b 100644 --- a/src/core/reporter.h +++ b/src/core/reporter.h @@ -20,6 +20,10 @@ namespace Service::FileSystem { enum class LogMode : u32; } +namespace Service::LM { +struct LogMessage; +} // namespace Service::LM + namespace Core { class System; @@ -29,18 +33,22 @@ public: explicit Reporter(System& system); ~Reporter(); + // Used by fatal services void SaveCrashReport(u64 title_id, ResultCode result, u64 set_flags, u64 entry_point, u64 sp, u64 pc, u64 pstate, u64 afsr0, u64 afsr1, u64 esr, u64 far, const std::array<u64, 31>& registers, const std::array<u64, 32>& backtrace, u32 backtrace_size, const std::string& arch, u32 unk10) const; + // Used by syscall svcBreak void SaveSvcBreakReport(u32 type, bool signal_debugger, u64 info1, u64 info2, std::optional<std::vector<u8>> resolved_buffer = {}) const; + // Used by HLE service handler void SaveUnimplementedFunctionReport(Kernel::HLERequestContext& ctx, u32 command_id, const std::string& name, const std::string& service_name) const; + // Used by stub applet implementation void SaveUnimplementedAppletReport(u32 applet_id, u32 common_args_version, u32 library_version, u32 theme_color, bool startup_sound, u64 system_tick, std::vector<std::vector<u8>> normal_channel, @@ -55,6 +63,7 @@ public: void SavePlayReport(PlayReportType type, u64 title_id, std::vector<std::vector<u8>> data, std::optional<u64> process_id = {}, std::optional<u128> user_id = {}) const; + // Used by error applet void SaveErrorReport(u64 title_id, ResultCode result, std::optional<std::string> custom_text_main = {}, std::optional<std::string> custom_text_detail = {}) const; @@ -62,6 +71,11 @@ public: void SaveFilesystemAccessReport(Service::FileSystem::LogMode log_mode, std::string log_message) const; + // Used by lm services + void SaveLogReport(u32 destination, std::vector<Service::LM::LogMessage> messages) const; + + // Can be used anywhere to generate a backtrace and general info report at any point during + // execution. Not intended to be used for anything other than debugging or testing. void SaveUserReport() const; private: diff --git a/src/core/settings.cpp b/src/core/settings.cpp index 7de3fd1e5..d1fc94060 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -103,6 +103,8 @@ void LogSettings() { LogSetting("Debugging_UseGdbstub", Settings::values.use_gdbstub); LogSetting("Debugging_GdbstubPort", Settings::values.gdbstub_port); LogSetting("Debugging_ProgramArgs", Settings::values.program_args); + LogSetting("Services_BCATBackend", Settings::values.bcat_backend); + LogSetting("Services_BCATBoxcatLocal", Settings::values.bcat_boxcat_local); } } // namespace Settings diff --git a/src/core/settings.h b/src/core/settings.h index 47bddfb30..9c98a9287 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -448,6 +448,10 @@ struct Values { bool reporting_services; bool quest_flag; + // BCAT + std::string bcat_backend; + bool bcat_boxcat_local; + // WebService bool enable_telemetry; std::string web_api_url; diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index e2f85c5f1..eaa694ff8 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -105,9 +105,15 @@ add_library(video_core STATIC shader/decode/warp.cpp shader/decode/xmad.cpp shader/decode/other.cpp + shader/ast.cpp + shader/ast.h shader/control_flow.cpp shader/control_flow.h + shader/compiler_settings.cpp + shader/compiler_settings.h shader/decode.cpp + shader/expr.cpp + shader/expr.h shader/node_helper.cpp shader/node_helper.h shader/node.h diff --git a/src/video_core/renderer_opengl/gl_rasterizer.cpp b/src/video_core/renderer_opengl/gl_rasterizer.cpp index 6a17bed72..a85f730a8 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer.cpp +++ b/src/video_core/renderer_opengl/gl_rasterizer.cpp @@ -1340,7 +1340,9 @@ void RasterizerOpenGL::SyncPolygonOffset() { state.polygon_offset.fill_enable = regs.polygon_offset_fill_enable != 0; state.polygon_offset.line_enable = regs.polygon_offset_line_enable != 0; state.polygon_offset.point_enable = regs.polygon_offset_point_enable != 0; - state.polygon_offset.units = regs.polygon_offset_units; + + // Hardware divides polygon offset units by two + state.polygon_offset.units = regs.polygon_offset_units / 2.0f; state.polygon_offset.factor = regs.polygon_offset_factor; state.polygon_offset.clamp = regs.polygon_offset_clamp; diff --git a/src/video_core/renderer_opengl/gl_shader_decompiler.cpp b/src/video_core/renderer_opengl/gl_shader_decompiler.cpp index 8fa9e6534..6a610a3bc 100644 --- a/src/video_core/renderer_opengl/gl_shader_decompiler.cpp +++ b/src/video_core/renderer_opengl/gl_shader_decompiler.cpp @@ -19,6 +19,7 @@ #include "video_core/renderer_opengl/gl_device.h" #include "video_core/renderer_opengl/gl_rasterizer.h" #include "video_core/renderer_opengl/gl_shader_decompiler.h" +#include "video_core/shader/ast.h" #include "video_core/shader/node.h" #include "video_core/shader/shader_ir.h" @@ -334,39 +335,24 @@ constexpr bool IsVertexShader(ProgramType stage) { return stage == ProgramType::VertexA || stage == ProgramType::VertexB; } +class ASTDecompiler; +class ExprDecompiler; + class GLSLDecompiler final { public: explicit GLSLDecompiler(const Device& device, const ShaderIR& ir, ProgramType stage, std::string suffix) : device{device}, ir{ir}, stage{stage}, suffix{suffix}, header{ir.GetHeader()} {} - void Decompile() { - DeclareVertex(); - DeclareGeometry(); - DeclareRegisters(); - DeclarePredicates(); - DeclareLocalMemory(); - DeclareSharedMemory(); - DeclareInternalFlags(); - DeclareInputAttributes(); - DeclareOutputAttributes(); - DeclareConstantBuffers(); - DeclareGlobalMemory(); - DeclareSamplers(); - DeclarePhysicalAttributeReader(); - DeclareImages(); - - code.AddLine("void execute_{}() {{", suffix); - ++code.scope; - + void DecompileBranchMode() { // VM's program counter const auto first_address = ir.GetBasicBlocks().begin()->first; code.AddLine("uint jmp_to = {}U;", first_address); // TODO(Subv): Figure out the actual depth of the flow stack, for now it seems // unlikely that shaders will use 20 nested SSYs and PBKs. + constexpr u32 FLOW_STACK_SIZE = 20; if (!ir.IsFlowStackDisabled()) { - constexpr u32 FLOW_STACK_SIZE = 20; for (const auto stack : std::array{MetaStackClass::Ssy, MetaStackClass::Pbk}) { code.AddLine("uint {}[{}];", FlowStackName(stack), FLOW_STACK_SIZE); code.AddLine("uint {} = 0U;", FlowStackTopName(stack)); @@ -392,10 +378,37 @@ public: code.AddLine("default: return;"); code.AddLine("}}"); - for (std::size_t i = 0; i < 2; ++i) { - --code.scope; - code.AddLine("}}"); + --code.scope; + code.AddLine("}}"); + } + + void DecompileAST(); + + void Decompile() { + DeclareVertex(); + DeclareGeometry(); + DeclareRegisters(); + DeclarePredicates(); + DeclareLocalMemory(); + DeclareInternalFlags(); + DeclareInputAttributes(); + DeclareOutputAttributes(); + DeclareConstantBuffers(); + DeclareGlobalMemory(); + DeclareSamplers(); + DeclarePhysicalAttributeReader(); + + code.AddLine("void execute_{}() {{", suffix); + ++code.scope; + + if (ir.IsDecompiled()) { + DecompileAST(); + } else { + DecompileBranchMode(); } + + --code.scope; + code.AddLine("}}"); } std::string GetResult() { @@ -424,6 +437,9 @@ public: } private: + friend class ASTDecompiler; + friend class ExprDecompiler; + void DeclareVertex() { if (!IsVertexShader(stage)) return; @@ -1821,10 +1837,9 @@ private: return {}; } - Expression Exit(Operation operation) { + void PreExit() { if (stage != ProgramType::Fragment) { - code.AddLine("return;"); - return {}; + return; } const auto& used_registers = ir.GetRegisters(); const auto SafeGetRegister = [&](u32 reg) -> Expression { @@ -1856,7 +1871,10 @@ private: // already contains one past the last color register. code.AddLine("gl_FragDepth = {};", SafeGetRegister(current_reg + 1).AsFloat()); } + } + Expression Exit(Operation operation) { + PreExit(); code.AddLine("return;"); return {}; } @@ -2253,6 +2271,208 @@ private: ShaderWriter code; }; +static constexpr std::string_view flow_var = "flow_var_"; + +std::string GetFlowVariable(u32 i) { + return fmt::format("{}{}", flow_var, i); +} + +class ExprDecompiler { +public: + explicit ExprDecompiler(GLSLDecompiler& decomp) : decomp{decomp} {} + + void operator()(VideoCommon::Shader::ExprAnd& expr) { + inner += "( "; + std::visit(*this, *expr.operand1); + inner += " && "; + std::visit(*this, *expr.operand2); + inner += ')'; + } + + void operator()(VideoCommon::Shader::ExprOr& expr) { + inner += "( "; + std::visit(*this, *expr.operand1); + inner += " || "; + std::visit(*this, *expr.operand2); + inner += ')'; + } + + void operator()(VideoCommon::Shader::ExprNot& expr) { + inner += '!'; + std::visit(*this, *expr.operand1); + } + + void operator()(VideoCommon::Shader::ExprPredicate& expr) { + const auto pred = static_cast<Tegra::Shader::Pred>(expr.predicate); + inner += decomp.GetPredicate(pred); + } + + void operator()(VideoCommon::Shader::ExprCondCode& expr) { + const Node cc = decomp.ir.GetConditionCode(expr.cc); + std::string target; + + if (const auto pred = std::get_if<PredicateNode>(&*cc)) { + const auto index = pred->GetIndex(); + switch (index) { + case Tegra::Shader::Pred::NeverExecute: + target = "false"; + case Tegra::Shader::Pred::UnusedIndex: + target = "true"; + default: + target = decomp.GetPredicate(index); + } + } else if (const auto flag = std::get_if<InternalFlagNode>(&*cc)) { + target = decomp.GetInternalFlag(flag->GetFlag()); + } else { + UNREACHABLE(); + } + inner += target; + } + + void operator()(VideoCommon::Shader::ExprVar& expr) { + inner += GetFlowVariable(expr.var_index); + } + + void operator()(VideoCommon::Shader::ExprBoolean& expr) { + inner += expr.value ? "true" : "false"; + } + + std::string& GetResult() { + return inner; + } + +private: + std::string inner; + GLSLDecompiler& decomp; +}; + +class ASTDecompiler { +public: + explicit ASTDecompiler(GLSLDecompiler& decomp) : decomp{decomp} {} + + void operator()(VideoCommon::Shader::ASTProgram& ast) { + ASTNode current = ast.nodes.GetFirst(); + while (current) { + Visit(current); + current = current->GetNext(); + } + } + + void operator()(VideoCommon::Shader::ASTIfThen& ast) { + ExprDecompiler expr_parser{decomp}; + std::visit(expr_parser, *ast.condition); + decomp.code.AddLine("if ({}) {{", expr_parser.GetResult()); + decomp.code.scope++; + ASTNode current = ast.nodes.GetFirst(); + while (current) { + Visit(current); + current = current->GetNext(); + } + decomp.code.scope--; + decomp.code.AddLine("}}"); + } + + void operator()(VideoCommon::Shader::ASTIfElse& ast) { + decomp.code.AddLine("else {{"); + decomp.code.scope++; + ASTNode current = ast.nodes.GetFirst(); + while (current) { + Visit(current); + current = current->GetNext(); + } + decomp.code.scope--; + decomp.code.AddLine("}}"); + } + + void operator()(VideoCommon::Shader::ASTBlockEncoded& ast) { + UNREACHABLE(); + } + + void operator()(VideoCommon::Shader::ASTBlockDecoded& ast) { + decomp.VisitBlock(ast.nodes); + } + + void operator()(VideoCommon::Shader::ASTVarSet& ast) { + ExprDecompiler expr_parser{decomp}; + std::visit(expr_parser, *ast.condition); + decomp.code.AddLine("{} = {};", GetFlowVariable(ast.index), expr_parser.GetResult()); + } + + void operator()(VideoCommon::Shader::ASTLabel& ast) { + decomp.code.AddLine("// Label_{}:", ast.index); + } + + void operator()(VideoCommon::Shader::ASTGoto& ast) { + UNREACHABLE(); + } + + void operator()(VideoCommon::Shader::ASTDoWhile& ast) { + ExprDecompiler expr_parser{decomp}; + std::visit(expr_parser, *ast.condition); + decomp.code.AddLine("do {{"); + decomp.code.scope++; + ASTNode current = ast.nodes.GetFirst(); + while (current) { + Visit(current); + current = current->GetNext(); + } + decomp.code.scope--; + decomp.code.AddLine("}} while({});", expr_parser.GetResult()); + } + + void operator()(VideoCommon::Shader::ASTReturn& ast) { + const bool is_true = VideoCommon::Shader::ExprIsTrue(ast.condition); + if (!is_true) { + ExprDecompiler expr_parser{decomp}; + std::visit(expr_parser, *ast.condition); + decomp.code.AddLine("if ({}) {{", expr_parser.GetResult()); + decomp.code.scope++; + } + if (ast.kills) { + decomp.code.AddLine("discard;"); + } else { + decomp.PreExit(); + decomp.code.AddLine("return;"); + } + if (!is_true) { + decomp.code.scope--; + decomp.code.AddLine("}}"); + } + } + + void operator()(VideoCommon::Shader::ASTBreak& ast) { + const bool is_true = VideoCommon::Shader::ExprIsTrue(ast.condition); + if (!is_true) { + ExprDecompiler expr_parser{decomp}; + std::visit(expr_parser, *ast.condition); + decomp.code.AddLine("if ({}) {{", expr_parser.GetResult()); + decomp.code.scope++; + } + decomp.code.AddLine("break;"); + if (!is_true) { + decomp.code.scope--; + decomp.code.AddLine("}}"); + } + } + + void Visit(VideoCommon::Shader::ASTNode& node) { + std::visit(*this, *node->GetInnerData()); + } + +private: + GLSLDecompiler& decomp; +}; + +void GLSLDecompiler::DecompileAST() { + const u32 num_flow_variables = ir.GetASTNumVariables(); + for (u32 i = 0; i < num_flow_variables; i++) { + code.AddLine("bool {} = false;", GetFlowVariable(i)); + } + ASTDecompiler decompiler{*this}; + VideoCommon::Shader::ASTNode program = ir.GetASTProgram(); + decompiler.Visit(program); +} + } // Anonymous namespace std::string GetCommonDeclarations() { diff --git a/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp b/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp index 6a7012b54..74cc33476 100644 --- a/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp +++ b/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp @@ -112,14 +112,15 @@ std::optional<std::pair<std::vector<ShaderDiskCacheRaw>, std::vector<ShaderDiskC ShaderDiskCacheOpenGL::LoadTransferable() { // Skip games without title id const bool has_title_id = system.CurrentProcess()->GetTitleID() != 0; - if (!Settings::values.use_disk_shader_cache || !has_title_id) + if (!Settings::values.use_disk_shader_cache || !has_title_id) { return {}; - tried_to_load = true; + } FileUtil::IOFile file(GetTransferablePath(), "rb"); if (!file.IsOpen()) { LOG_INFO(Render_OpenGL, "No transferable shader cache found for game with title id={}", GetTitleID()); + is_usable = true; return {}; } @@ -135,6 +136,7 @@ ShaderDiskCacheOpenGL::LoadTransferable() { LOG_INFO(Render_OpenGL, "Transferable shader cache is old - removing"); file.Close(); InvalidateTransferable(); + is_usable = true; return {}; } if (version > NativeVersion) { @@ -180,13 +182,15 @@ ShaderDiskCacheOpenGL::LoadTransferable() { } } - return {{raws, usages}}; + is_usable = true; + return {{std::move(raws), std::move(usages)}}; } std::pair<std::unordered_map<u64, ShaderDiskCacheDecompiled>, ShaderDumpsMap> ShaderDiskCacheOpenGL::LoadPrecompiled() { - if (!IsUsable()) + if (!is_usable) { return {}; + } FileUtil::IOFile file(GetPrecompiledPath(), "rb"); if (!file.IsOpen()) { @@ -479,8 +483,9 @@ void ShaderDiskCacheOpenGL::InvalidatePrecompiled() { } void ShaderDiskCacheOpenGL::SaveRaw(const ShaderDiskCacheRaw& entry) { - if (!IsUsable()) + if (!is_usable) { return; + } const u64 id = entry.GetUniqueIdentifier(); if (transferable.find(id) != transferable.end()) { @@ -501,8 +506,9 @@ void ShaderDiskCacheOpenGL::SaveRaw(const ShaderDiskCacheRaw& entry) { } void ShaderDiskCacheOpenGL::SaveUsage(const ShaderDiskCacheUsage& usage) { - if (!IsUsable()) + if (!is_usable) { return; + } const auto it = transferable.find(usage.unique_identifier); ASSERT_MSG(it != transferable.end(), "Saving shader usage without storing raw previously"); @@ -528,8 +534,9 @@ void ShaderDiskCacheOpenGL::SaveUsage(const ShaderDiskCacheUsage& usage) { void ShaderDiskCacheOpenGL::SaveDecompiled(u64 unique_identifier, const std::string& code, const GLShader::ShaderEntries& entries) { - if (!IsUsable()) + if (!is_usable) { return; + } if (precompiled_cache_virtual_file.GetSize() == 0) { SavePrecompiledHeaderToVirtualPrecompiledCache(); @@ -543,8 +550,9 @@ void ShaderDiskCacheOpenGL::SaveDecompiled(u64 unique_identifier, const std::str } void ShaderDiskCacheOpenGL::SaveDump(const ShaderDiskCacheUsage& usage, GLuint program) { - if (!IsUsable()) + if (!is_usable) { return; + } GLint binary_length{}; glGetProgramiv(program, GL_PROGRAM_BINARY_LENGTH, &binary_length); @@ -565,10 +573,6 @@ void ShaderDiskCacheOpenGL::SaveDump(const ShaderDiskCacheUsage& usage, GLuint p } } -bool ShaderDiskCacheOpenGL::IsUsable() const { - return tried_to_load && Settings::values.use_disk_shader_cache; -} - FileUtil::IOFile ShaderDiskCacheOpenGL::AppendTransferableFile() const { if (!EnsureDirectories()) return {}; diff --git a/src/video_core/renderer_opengl/gl_shader_disk_cache.h b/src/video_core/renderer_opengl/gl_shader_disk_cache.h index cc8bbd61e..9595bd71b 100644 --- a/src/video_core/renderer_opengl/gl_shader_disk_cache.h +++ b/src/video_core/renderer_opengl/gl_shader_disk_cache.h @@ -224,9 +224,6 @@ private: bool SaveDecompiledFile(u64 unique_identifier, const std::string& code, const GLShader::ShaderEntries& entries); - /// Returns if the cache can be used - bool IsUsable() const; - /// Opens current game's transferable file and write it's header if it doesn't exist FileUtil::IOFile AppendTransferableFile() const; @@ -297,7 +294,7 @@ private: std::unordered_map<u64, std::unordered_set<ShaderDiskCacheUsage>> transferable; // The cache has been loaded at boot - bool tried_to_load{}; + bool is_usable{}; }; } // namespace OpenGL diff --git a/src/video_core/renderer_opengl/gl_shader_gen.cpp b/src/video_core/renderer_opengl/gl_shader_gen.cpp index 3a8d9e1da..b5a43e79e 100644 --- a/src/video_core/renderer_opengl/gl_shader_gen.cpp +++ b/src/video_core/renderer_opengl/gl_shader_gen.cpp @@ -11,12 +11,16 @@ namespace OpenGL::GLShader { using Tegra::Engines::Maxwell3D; +using VideoCommon::Shader::CompileDepth; +using VideoCommon::Shader::CompilerSettings; using VideoCommon::Shader::ProgramCode; using VideoCommon::Shader::ShaderIR; static constexpr u32 PROGRAM_OFFSET = 10; static constexpr u32 COMPUTE_OFFSET = 0; +static constexpr CompilerSettings settings{CompileDepth::NoFlowStack, true}; + ProgramResult GenerateVertexShader(const Device& device, const ShaderSetup& setup) { const std::string id = fmt::format("{:016x}", setup.program.unique_identifier); @@ -31,13 +35,14 @@ layout (std140, binding = EMULATION_UBO_BINDING) uniform vs_config { )"; - const ShaderIR program_ir(setup.program.code, PROGRAM_OFFSET, setup.program.size_a); + const ShaderIR program_ir(setup.program.code, PROGRAM_OFFSET, setup.program.size_a, settings); const auto stage = setup.IsDualProgram() ? ProgramType::VertexA : ProgramType::VertexB; ProgramResult program = Decompile(device, program_ir, stage, "vertex"); out += program.first; if (setup.IsDualProgram()) { - const ShaderIR program_ir_b(setup.program.code_b, PROGRAM_OFFSET, setup.program.size_b); + const ShaderIR program_ir_b(setup.program.code_b, PROGRAM_OFFSET, setup.program.size_b, + settings); ProgramResult program_b = Decompile(device, program_ir_b, ProgramType::VertexB, "vertex_b"); out += program_b.first; } @@ -80,7 +85,7 @@ layout (std140, binding = EMULATION_UBO_BINDING) uniform gs_config { )"; - const ShaderIR program_ir(setup.program.code, PROGRAM_OFFSET, setup.program.size_a); + const ShaderIR program_ir(setup.program.code, PROGRAM_OFFSET, setup.program.size_a, settings); ProgramResult program = Decompile(device, program_ir, ProgramType::Geometry, "geometry"); out += program.first; @@ -114,7 +119,8 @@ layout (std140, binding = EMULATION_UBO_BINDING) uniform fs_config { }; )"; - const ShaderIR program_ir(setup.program.code, PROGRAM_OFFSET, setup.program.size_a); + + const ShaderIR program_ir(setup.program.code, PROGRAM_OFFSET, setup.program.size_a, settings); ProgramResult program = Decompile(device, program_ir, ProgramType::Fragment, "fragment"); out += program.first; @@ -133,7 +139,7 @@ ProgramResult GenerateComputeShader(const Device& device, const ShaderSetup& set std::string out = "// Shader Unique Id: CS" + id + "\n\n"; out += GetCommonDeclarations(); - const ShaderIR program_ir(setup.program.code, COMPUTE_OFFSET, setup.program.size_a); + const ShaderIR program_ir(setup.program.code, COMPUTE_OFFSET, setup.program.size_a, settings); ProgramResult program = Decompile(device, program_ir, ProgramType::Compute, "compute"); out += program.first; diff --git a/src/video_core/renderer_vulkan/vk_shader_decompiler.cpp b/src/video_core/renderer_vulkan/vk_shader_decompiler.cpp index 77fc58f25..8bcd04221 100644 --- a/src/video_core/renderer_vulkan/vk_shader_decompiler.cpp +++ b/src/video_core/renderer_vulkan/vk_shader_decompiler.cpp @@ -88,6 +88,9 @@ bool IsPrecise(Operation operand) { } // namespace +class ASTDecompiler; +class ExprDecompiler; + class SPIRVDecompiler : public Sirit::Module { public: explicit SPIRVDecompiler(const VKDevice& device, const ShaderIR& ir, ShaderStage stage) @@ -97,27 +100,7 @@ public: AddExtension("SPV_KHR_variable_pointers"); } - void Decompile() { - AllocateBindings(); - AllocateLabels(); - - DeclareVertex(); - DeclareGeometry(); - DeclareFragment(); - DeclareRegisters(); - DeclarePredicates(); - DeclareLocalMemory(); - DeclareInternalFlags(); - DeclareInputAttributes(); - DeclareOutputAttributes(); - DeclareConstantBuffers(); - DeclareGlobalBuffers(); - DeclareSamplers(); - - execute_function = - Emit(OpFunction(t_void, spv::FunctionControlMask::Inline, TypeFunction(t_void))); - Emit(OpLabel()); - + void DecompileBranchMode() { const u32 first_address = ir.GetBasicBlocks().begin()->first; const Id loop_label = OpLabel("loop"); const Id merge_label = OpLabel("merge"); @@ -174,6 +157,43 @@ public: Emit(continue_label); Emit(OpBranch(loop_label)); Emit(merge_label); + } + + void DecompileAST(); + + void Decompile() { + const bool is_fully_decompiled = ir.IsDecompiled(); + AllocateBindings(); + if (!is_fully_decompiled) { + AllocateLabels(); + } + + DeclareVertex(); + DeclareGeometry(); + DeclareFragment(); + DeclareRegisters(); + DeclarePredicates(); + if (is_fully_decompiled) { + DeclareFlowVariables(); + } + DeclareLocalMemory(); + DeclareInternalFlags(); + DeclareInputAttributes(); + DeclareOutputAttributes(); + DeclareConstantBuffers(); + DeclareGlobalBuffers(); + DeclareSamplers(); + + execute_function = + Emit(OpFunction(t_void, spv::FunctionControlMask::Inline, TypeFunction(t_void))); + Emit(OpLabel()); + + if (is_fully_decompiled) { + DecompileAST(); + } else { + DecompileBranchMode(); + } + Emit(OpReturn()); Emit(OpFunctionEnd()); } @@ -206,6 +226,9 @@ public: } private: + friend class ASTDecompiler; + friend class ExprDecompiler; + static constexpr auto INTERNAL_FLAGS_COUNT = static_cast<std::size_t>(InternalFlag::Amount); void AllocateBindings() { @@ -294,6 +317,14 @@ private: } } + void DeclareFlowVariables() { + for (u32 i = 0; i < ir.GetASTNumVariables(); i++) { + const Id id = OpVariable(t_prv_bool, spv::StorageClass::Private, v_false); + Name(id, fmt::format("flow_var_{}", static_cast<u32>(i))); + flow_variables.emplace(i, AddGlobalVariable(id)); + } + } + void DeclareLocalMemory() { if (const u64 local_memory_size = header.GetLocalMemorySize(); local_memory_size > 0) { const auto element_count = static_cast<u32>(Common::AlignUp(local_memory_size, 4) / 4); @@ -615,9 +646,15 @@ private: Emit(OpBranchConditional(condition, true_label, skip_label)); Emit(true_label); + ++conditional_nest_count; VisitBasicBlock(conditional->GetCode()); + --conditional_nest_count; - Emit(OpBranch(skip_label)); + if (inside_branch == 0) { + Emit(OpBranch(skip_label)); + } else { + inside_branch--; + } Emit(skip_label); return {}; @@ -980,7 +1017,11 @@ private: UNIMPLEMENTED_IF(!target); Emit(OpStore(jmp_to, Constant(t_uint, target->GetValue()))); - BranchingOp([&]() { Emit(OpBranch(continue_label)); }); + Emit(OpBranch(continue_label)); + inside_branch = conditional_nest_count; + if (conditional_nest_count == 0) { + Emit(OpLabel()); + } return {}; } @@ -988,7 +1029,11 @@ private: const Id op_a = VisitOperand<Type::Uint>(operation, 0); Emit(OpStore(jmp_to, op_a)); - BranchingOp([&]() { Emit(OpBranch(continue_label)); }); + Emit(OpBranch(continue_label)); + inside_branch = conditional_nest_count; + if (conditional_nest_count == 0) { + Emit(OpLabel()); + } return {}; } @@ -1015,11 +1060,15 @@ private: Emit(OpStore(flow_stack_top, previous)); Emit(OpStore(jmp_to, target)); - BranchingOp([&]() { Emit(OpBranch(continue_label)); }); + Emit(OpBranch(continue_label)); + inside_branch = conditional_nest_count; + if (conditional_nest_count == 0) { + Emit(OpLabel()); + } return {}; } - Id Exit(Operation operation) { + Id PreExit() { switch (stage) { case ShaderStage::Vertex: { // TODO(Rodrigo): We should use VK_EXT_depth_range_unrestricted instead, but it doesn't @@ -1067,12 +1116,35 @@ private: } } - BranchingOp([&]() { Emit(OpReturn()); }); + return {}; + } + + Id Exit(Operation operation) { + PreExit(); + inside_branch = conditional_nest_count; + if (conditional_nest_count > 0) { + Emit(OpReturn()); + } else { + const Id dummy = OpLabel(); + Emit(OpBranch(dummy)); + Emit(dummy); + Emit(OpReturn()); + Emit(OpLabel()); + } return {}; } Id Discard(Operation operation) { - BranchingOp([&]() { Emit(OpKill()); }); + inside_branch = conditional_nest_count; + if (conditional_nest_count > 0) { + Emit(OpKill()); + } else { + const Id dummy = OpLabel(); + Emit(OpBranch(dummy)); + Emit(dummy); + Emit(OpKill()); + Emit(OpLabel()); + } return {}; } @@ -1267,17 +1339,6 @@ private: return {}; } - void BranchingOp(std::function<void()> call) { - const Id true_label = OpLabel(); - const Id skip_label = OpLabel(); - Emit(OpSelectionMerge(skip_label, spv::SelectionControlMask::Flatten)); - Emit(OpBranchConditional(v_true, true_label, skip_label, 1, 0)); - Emit(true_label); - call(); - - Emit(skip_label); - } - std::tuple<Id, Id> CreateFlowStack() { // TODO(Rodrigo): Figure out the actual depth of the flow stack, for now it seems unlikely // that shaders will use 20 nested SSYs and PBKs. @@ -1483,6 +1544,8 @@ private: const ShaderIR& ir; const ShaderStage stage; const Tegra::Shader::Header header; + u64 conditional_nest_count{}; + u64 inside_branch{}; const Id t_void = Name(TypeVoid(), "void"); @@ -1545,6 +1608,7 @@ private: Id per_vertex{}; std::map<u32, Id> registers; std::map<Tegra::Shader::Pred, Id> predicates; + std::map<u32, Id> flow_variables; Id local_memory{}; std::array<Id, INTERNAL_FLAGS_COUNT> internal_flags{}; std::map<Attribute::Index, Id> input_attributes; @@ -1580,6 +1644,223 @@ private: std::map<u32, Id> labels; }; +class ExprDecompiler { +public: + explicit ExprDecompiler(SPIRVDecompiler& decomp) : decomp{decomp} {} + + Id operator()(VideoCommon::Shader::ExprAnd& expr) { + const Id type_def = decomp.GetTypeDefinition(Type::Bool); + const Id op1 = Visit(expr.operand1); + const Id op2 = Visit(expr.operand2); + return decomp.Emit(decomp.OpLogicalAnd(type_def, op1, op2)); + } + + Id operator()(VideoCommon::Shader::ExprOr& expr) { + const Id type_def = decomp.GetTypeDefinition(Type::Bool); + const Id op1 = Visit(expr.operand1); + const Id op2 = Visit(expr.operand2); + return decomp.Emit(decomp.OpLogicalOr(type_def, op1, op2)); + } + + Id operator()(VideoCommon::Shader::ExprNot& expr) { + const Id type_def = decomp.GetTypeDefinition(Type::Bool); + const Id op1 = Visit(expr.operand1); + return decomp.Emit(decomp.OpLogicalNot(type_def, op1)); + } + + Id operator()(VideoCommon::Shader::ExprPredicate& expr) { + const auto pred = static_cast<Tegra::Shader::Pred>(expr.predicate); + return decomp.Emit(decomp.OpLoad(decomp.t_bool, decomp.predicates.at(pred))); + } + + Id operator()(VideoCommon::Shader::ExprCondCode& expr) { + const Node cc = decomp.ir.GetConditionCode(expr.cc); + Id target; + + if (const auto pred = std::get_if<PredicateNode>(&*cc)) { + const auto index = pred->GetIndex(); + switch (index) { + case Tegra::Shader::Pred::NeverExecute: + target = decomp.v_false; + case Tegra::Shader::Pred::UnusedIndex: + target = decomp.v_true; + default: + target = decomp.predicates.at(index); + } + } else if (const auto flag = std::get_if<InternalFlagNode>(&*cc)) { + target = decomp.internal_flags.at(static_cast<u32>(flag->GetFlag())); + } + return decomp.Emit(decomp.OpLoad(decomp.t_bool, target)); + } + + Id operator()(VideoCommon::Shader::ExprVar& expr) { + return decomp.Emit(decomp.OpLoad(decomp.t_bool, decomp.flow_variables.at(expr.var_index))); + } + + Id operator()(VideoCommon::Shader::ExprBoolean& expr) { + return expr.value ? decomp.v_true : decomp.v_false; + } + + Id Visit(VideoCommon::Shader::Expr& node) { + return std::visit(*this, *node); + } + +private: + SPIRVDecompiler& decomp; +}; + +class ASTDecompiler { +public: + explicit ASTDecompiler(SPIRVDecompiler& decomp) : decomp{decomp} {} + + void operator()(VideoCommon::Shader::ASTProgram& ast) { + ASTNode current = ast.nodes.GetFirst(); + while (current) { + Visit(current); + current = current->GetNext(); + } + } + + void operator()(VideoCommon::Shader::ASTIfThen& ast) { + ExprDecompiler expr_parser{decomp}; + const Id condition = expr_parser.Visit(ast.condition); + const Id then_label = decomp.OpLabel(); + const Id endif_label = decomp.OpLabel(); + decomp.Emit(decomp.OpSelectionMerge(endif_label, spv::SelectionControlMask::MaskNone)); + decomp.Emit(decomp.OpBranchConditional(condition, then_label, endif_label)); + decomp.Emit(then_label); + ASTNode current = ast.nodes.GetFirst(); + while (current) { + Visit(current); + current = current->GetNext(); + } + decomp.Emit(decomp.OpBranch(endif_label)); + decomp.Emit(endif_label); + } + + void operator()(VideoCommon::Shader::ASTIfElse& ast) { + UNREACHABLE(); + } + + void operator()(VideoCommon::Shader::ASTBlockEncoded& ast) { + UNREACHABLE(); + } + + void operator()(VideoCommon::Shader::ASTBlockDecoded& ast) { + decomp.VisitBasicBlock(ast.nodes); + } + + void operator()(VideoCommon::Shader::ASTVarSet& ast) { + ExprDecompiler expr_parser{decomp}; + const Id condition = expr_parser.Visit(ast.condition); + decomp.Emit(decomp.OpStore(decomp.flow_variables.at(ast.index), condition)); + } + + void operator()(VideoCommon::Shader::ASTLabel& ast) { + // Do nothing + } + + void operator()(VideoCommon::Shader::ASTGoto& ast) { + UNREACHABLE(); + } + + void operator()(VideoCommon::Shader::ASTDoWhile& ast) { + const Id loop_label = decomp.OpLabel(); + const Id endloop_label = decomp.OpLabel(); + const Id loop_start_block = decomp.OpLabel(); + const Id loop_end_block = decomp.OpLabel(); + current_loop_exit = endloop_label; + decomp.Emit(decomp.OpBranch(loop_label)); + decomp.Emit(loop_label); + decomp.Emit( + decomp.OpLoopMerge(endloop_label, loop_end_block, spv::LoopControlMask::MaskNone)); + decomp.Emit(decomp.OpBranch(loop_start_block)); + decomp.Emit(loop_start_block); + ASTNode current = ast.nodes.GetFirst(); + while (current) { + Visit(current); + current = current->GetNext(); + } + ExprDecompiler expr_parser{decomp}; + const Id condition = expr_parser.Visit(ast.condition); + decomp.Emit(decomp.OpBranchConditional(condition, loop_label, endloop_label)); + decomp.Emit(endloop_label); + } + + void operator()(VideoCommon::Shader::ASTReturn& ast) { + if (!VideoCommon::Shader::ExprIsTrue(ast.condition)) { + ExprDecompiler expr_parser{decomp}; + const Id condition = expr_parser.Visit(ast.condition); + const Id then_label = decomp.OpLabel(); + const Id endif_label = decomp.OpLabel(); + decomp.Emit(decomp.OpSelectionMerge(endif_label, spv::SelectionControlMask::MaskNone)); + decomp.Emit(decomp.OpBranchConditional(condition, then_label, endif_label)); + decomp.Emit(then_label); + if (ast.kills) { + decomp.Emit(decomp.OpKill()); + } else { + decomp.PreExit(); + decomp.Emit(decomp.OpReturn()); + } + decomp.Emit(endif_label); + } else { + const Id next_block = decomp.OpLabel(); + decomp.Emit(decomp.OpBranch(next_block)); + decomp.Emit(next_block); + if (ast.kills) { + decomp.Emit(decomp.OpKill()); + } else { + decomp.PreExit(); + decomp.Emit(decomp.OpReturn()); + } + decomp.Emit(decomp.OpLabel()); + } + } + + void operator()(VideoCommon::Shader::ASTBreak& ast) { + if (!VideoCommon::Shader::ExprIsTrue(ast.condition)) { + ExprDecompiler expr_parser{decomp}; + const Id condition = expr_parser.Visit(ast.condition); + const Id then_label = decomp.OpLabel(); + const Id endif_label = decomp.OpLabel(); + decomp.Emit(decomp.OpSelectionMerge(endif_label, spv::SelectionControlMask::MaskNone)); + decomp.Emit(decomp.OpBranchConditional(condition, then_label, endif_label)); + decomp.Emit(then_label); + decomp.Emit(decomp.OpBranch(current_loop_exit)); + decomp.Emit(endif_label); + } else { + const Id next_block = decomp.OpLabel(); + decomp.Emit(decomp.OpBranch(next_block)); + decomp.Emit(next_block); + decomp.Emit(decomp.OpBranch(current_loop_exit)); + decomp.Emit(decomp.OpLabel()); + } + } + + void Visit(VideoCommon::Shader::ASTNode& node) { + std::visit(*this, *node->GetInnerData()); + } + +private: + SPIRVDecompiler& decomp; + Id current_loop_exit{}; +}; + +void SPIRVDecompiler::DecompileAST() { + const u32 num_flow_variables = ir.GetASTNumVariables(); + for (u32 i = 0; i < num_flow_variables; i++) { + const Id id = OpVariable(t_prv_bool, spv::StorageClass::Private, v_false); + Name(id, fmt::format("flow_var_{}", i)); + flow_variables.emplace(i, AddGlobalVariable(id)); + } + ASTDecompiler decompiler{*this}; + VideoCommon::Shader::ASTNode program = ir.GetASTProgram(); + decompiler.Visit(program); + const Id next_block = OpLabel(); + Emit(OpBranch(next_block)); + Emit(next_block); +} + DecompilerResult Decompile(const VKDevice& device, const VideoCommon::Shader::ShaderIR& ir, Maxwell::ShaderStage stage) { auto decompiler = std::make_unique<SPIRVDecompiler>(device, ir, stage); diff --git a/src/video_core/shader/ast.cpp b/src/video_core/shader/ast.cpp new file mode 100644 index 000000000..436d45f4b --- /dev/null +++ b/src/video_core/shader/ast.cpp @@ -0,0 +1,738 @@ +// Copyright 2019 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <string> + +#include "common/assert.h" +#include "common/common_types.h" +#include "video_core/shader/ast.h" +#include "video_core/shader/expr.h" + +namespace VideoCommon::Shader { + +ASTZipper::ASTZipper() = default; + +void ASTZipper::Init(const ASTNode new_first, const ASTNode parent) { + ASSERT(new_first->manager == nullptr); + first = new_first; + last = new_first; + + ASTNode current = first; + while (current) { + current->manager = this; + current->parent = parent; + last = current; + current = current->next; + } +} + +void ASTZipper::PushBack(const ASTNode new_node) { + ASSERT(new_node->manager == nullptr); + new_node->previous = last; + if (last) { + last->next = new_node; + } + new_node->next.reset(); + last = new_node; + if (!first) { + first = new_node; + } + new_node->manager = this; +} + +void ASTZipper::PushFront(const ASTNode new_node) { + ASSERT(new_node->manager == nullptr); + new_node->previous.reset(); + new_node->next = first; + if (first) { + first->previous = new_node; + } + if (last == first) { + last = new_node; + } + first = new_node; + new_node->manager = this; +} + +void ASTZipper::InsertAfter(const ASTNode new_node, const ASTNode at_node) { + ASSERT(new_node->manager == nullptr); + if (!at_node) { + PushFront(new_node); + return; + } + const ASTNode next = at_node->next; + if (next) { + next->previous = new_node; + } + new_node->previous = at_node; + if (at_node == last) { + last = new_node; + } + new_node->next = next; + at_node->next = new_node; + new_node->manager = this; +} + +void ASTZipper::InsertBefore(const ASTNode new_node, const ASTNode at_node) { + ASSERT(new_node->manager == nullptr); + if (!at_node) { + PushBack(new_node); + return; + } + const ASTNode previous = at_node->previous; + if (previous) { + previous->next = new_node; + } + new_node->next = at_node; + if (at_node == first) { + first = new_node; + } + new_node->previous = previous; + at_node->previous = new_node; + new_node->manager = this; +} + +void ASTZipper::DetachTail(ASTNode node) { + ASSERT(node->manager == this); + if (node == first) { + first.reset(); + last.reset(); + return; + } + + last = node->previous; + last->next.reset(); + node->previous.reset(); + + ASTNode current = std::move(node); + while (current) { + current->manager = nullptr; + current->parent.reset(); + current = current->next; + } +} + +void ASTZipper::DetachSegment(const ASTNode start, const ASTNode end) { + ASSERT(start->manager == this && end->manager == this); + if (start == end) { + DetachSingle(start); + return; + } + const ASTNode prev = start->previous; + const ASTNode post = end->next; + if (!prev) { + first = post; + } else { + prev->next = post; + } + if (!post) { + last = prev; + } else { + post->previous = prev; + } + start->previous.reset(); + end->next.reset(); + ASTNode current = start; + bool found = false; + while (current) { + current->manager = nullptr; + current->parent.reset(); + found |= current == end; + current = current->next; + } + ASSERT(found); +} + +void ASTZipper::DetachSingle(const ASTNode node) { + ASSERT(node->manager == this); + const ASTNode prev = node->previous; + const ASTNode post = node->next; + node->previous.reset(); + node->next.reset(); + if (!prev) { + first = post; + } else { + prev->next = post; + } + if (!post) { + last = prev; + } else { + post->previous = prev; + } + + node->manager = nullptr; + node->parent.reset(); +} + +void ASTZipper::Remove(const ASTNode node) { + ASSERT(node->manager == this); + const ASTNode next = node->next; + const ASTNode previous = node->previous; + if (previous) { + previous->next = next; + } + if (next) { + next->previous = previous; + } + node->parent.reset(); + node->manager = nullptr; + if (node == last) { + last = previous; + } + if (node == first) { + first = next; + } +} + +class ExprPrinter final { +public: + void operator()(const ExprAnd& expr) { + inner += "( "; + std::visit(*this, *expr.operand1); + inner += " && "; + std::visit(*this, *expr.operand2); + inner += ')'; + } + + void operator()(const ExprOr& expr) { + inner += "( "; + std::visit(*this, *expr.operand1); + inner += " || "; + std::visit(*this, *expr.operand2); + inner += ')'; + } + + void operator()(const ExprNot& expr) { + inner += "!"; + std::visit(*this, *expr.operand1); + } + + void operator()(const ExprPredicate& expr) { + inner += "P" + std::to_string(expr.predicate); + } + + void operator()(const ExprCondCode& expr) { + u32 cc = static_cast<u32>(expr.cc); + inner += "CC" + std::to_string(cc); + } + + void operator()(const ExprVar& expr) { + inner += "V" + std::to_string(expr.var_index); + } + + void operator()(const ExprBoolean& expr) { + inner += expr.value ? "true" : "false"; + } + + const std::string& GetResult() const { + return inner; + } + + std::string inner{}; +}; + +class ASTPrinter { +public: + void operator()(const ASTProgram& ast) { + scope++; + inner += "program {\n"; + ASTNode current = ast.nodes.GetFirst(); + while (current) { + Visit(current); + current = current->GetNext(); + } + inner += "}\n"; + scope--; + } + + void operator()(const ASTIfThen& ast) { + ExprPrinter expr_parser{}; + std::visit(expr_parser, *ast.condition); + inner += Ident() + "if (" + expr_parser.GetResult() + ") {\n"; + scope++; + ASTNode current = ast.nodes.GetFirst(); + while (current) { + Visit(current); + current = current->GetNext(); + } + scope--; + inner += Ident() + "}\n"; + } + + void operator()(const ASTIfElse& ast) { + inner += Ident() + "else {\n"; + scope++; + ASTNode current = ast.nodes.GetFirst(); + while (current) { + Visit(current); + current = current->GetNext(); + } + scope--; + inner += Ident() + "}\n"; + } + + void operator()(const ASTBlockEncoded& ast) { + inner += Ident() + "Block(" + std::to_string(ast.start) + ", " + std::to_string(ast.end) + + ");\n"; + } + + void operator()(const ASTBlockDecoded& ast) { + inner += Ident() + "Block;\n"; + } + + void operator()(const ASTVarSet& ast) { + ExprPrinter expr_parser{}; + std::visit(expr_parser, *ast.condition); + inner += + Ident() + "V" + std::to_string(ast.index) + " := " + expr_parser.GetResult() + ";\n"; + } + + void operator()(const ASTLabel& ast) { + inner += "Label_" + std::to_string(ast.index) + ":\n"; + } + + void operator()(const ASTGoto& ast) { + ExprPrinter expr_parser{}; + std::visit(expr_parser, *ast.condition); + inner += Ident() + "(" + expr_parser.GetResult() + ") -> goto Label_" + + std::to_string(ast.label) + ";\n"; + } + + void operator()(const ASTDoWhile& ast) { + ExprPrinter expr_parser{}; + std::visit(expr_parser, *ast.condition); + inner += Ident() + "do {\n"; + scope++; + ASTNode current = ast.nodes.GetFirst(); + while (current) { + Visit(current); + current = current->GetNext(); + } + scope--; + inner += Ident() + "} while (" + expr_parser.GetResult() + ");\n"; + } + + void operator()(const ASTReturn& ast) { + ExprPrinter expr_parser{}; + std::visit(expr_parser, *ast.condition); + inner += Ident() + "(" + expr_parser.GetResult() + ") -> " + + (ast.kills ? "discard" : "exit") + ";\n"; + } + + void operator()(const ASTBreak& ast) { + ExprPrinter expr_parser{}; + std::visit(expr_parser, *ast.condition); + inner += Ident() + "(" + expr_parser.GetResult() + ") -> break;\n"; + } + + std::string& Ident() { + if (memo_scope == scope) { + return tabs_memo; + } + tabs_memo = tabs.substr(0, scope * 2); + memo_scope = scope; + return tabs_memo; + } + + void Visit(ASTNode& node) { + std::visit(*this, *node->GetInnerData()); + } + + const std::string& GetResult() const { + return inner; + } + +private: + std::string inner{}; + u32 scope{}; + + std::string tabs_memo{}; + u32 memo_scope{}; + + static constexpr std::string_view tabs{" "}; +}; + +std::string ASTManager::Print() { + ASTPrinter printer{}; + printer.Visit(main_node); + return printer.GetResult(); +} + +ASTManager::ASTManager(bool full_decompile, bool disable_else_derivation) + : full_decompile{full_decompile}, disable_else_derivation{disable_else_derivation} {}; + +ASTManager::~ASTManager() { + Clear(); +} + +void ASTManager::Init() { + main_node = ASTBase::Make<ASTProgram>(ASTNode{}); + program = std::get_if<ASTProgram>(main_node->GetInnerData()); + false_condition = MakeExpr<ExprBoolean>(false); +} + +void ASTManager::DeclareLabel(u32 address) { + const auto pair = labels_map.emplace(address, labels_count); + if (pair.second) { + labels_count++; + labels.resize(labels_count); + } +} + +void ASTManager::InsertLabel(u32 address) { + const u32 index = labels_map[address]; + const ASTNode label = ASTBase::Make<ASTLabel>(main_node, index); + labels[index] = label; + program->nodes.PushBack(label); +} + +void ASTManager::InsertGoto(Expr condition, u32 address) { + const u32 index = labels_map[address]; + const ASTNode goto_node = ASTBase::Make<ASTGoto>(main_node, std::move(condition), index); + gotos.push_back(goto_node); + program->nodes.PushBack(goto_node); +} + +void ASTManager::InsertBlock(u32 start_address, u32 end_address) { + ASTNode block = ASTBase::Make<ASTBlockEncoded>(main_node, start_address, end_address); + program->nodes.PushBack(std::move(block)); +} + +void ASTManager::InsertReturn(Expr condition, bool kills) { + ASTNode node = ASTBase::Make<ASTReturn>(main_node, std::move(condition), kills); + program->nodes.PushBack(std::move(node)); +} + +// The decompile algorithm is based on +// "Taming control flow: A structured approach to eliminating goto statements" +// by AM Erosa, LJ Hendren 1994. In general, the idea is to get gotos to be +// on the same structured level as the label which they jump to. This is done, +// through outward/inward movements and lifting. Once they are at the same +// level, you can enclose them in an "if" structure or a "do-while" structure. +void ASTManager::Decompile() { + auto it = gotos.begin(); + while (it != gotos.end()) { + const ASTNode goto_node = *it; + const auto label_index = goto_node->GetGotoLabel(); + if (!label_index) { + return; + } + const ASTNode label = labels[*label_index]; + if (!full_decompile) { + // We only decompile backward jumps + if (!IsBackwardsJump(goto_node, label)) { + it++; + continue; + } + } + if (IndirectlyRelated(goto_node, label)) { + while (!DirectlyRelated(goto_node, label)) { + MoveOutward(goto_node); + } + } + if (DirectlyRelated(goto_node, label)) { + u32 goto_level = goto_node->GetLevel(); + const u32 label_level = label->GetLevel(); + while (label_level < goto_level) { + MoveOutward(goto_node); + goto_level--; + } + // TODO(Blinkhawk): Implement Lifting and Inward Movements + } + if (label->GetParent() == goto_node->GetParent()) { + bool is_loop = false; + ASTNode current = goto_node->GetPrevious(); + while (current) { + if (current == label) { + is_loop = true; + break; + } + current = current->GetPrevious(); + } + + if (is_loop) { + EncloseDoWhile(goto_node, label); + } else { + EncloseIfThen(goto_node, label); + } + it = gotos.erase(it); + continue; + } + it++; + } + if (full_decompile) { + for (const ASTNode& label : labels) { + auto& manager = label->GetManager(); + manager.Remove(label); + } + labels.clear(); + } else { + auto label_it = labels.begin(); + while (label_it != labels.end()) { + bool can_remove = true; + ASTNode label = *label_it; + for (const ASTNode& goto_node : gotos) { + const auto label_index = goto_node->GetGotoLabel(); + if (!label_index) { + return; + } + ASTNode& glabel = labels[*label_index]; + if (glabel == label) { + can_remove = false; + break; + } + } + if (can_remove) { + label->MarkLabelUnused(); + } + } + } +} + +bool ASTManager::IsBackwardsJump(ASTNode goto_node, ASTNode label_node) const { + u32 goto_level = goto_node->GetLevel(); + u32 label_level = label_node->GetLevel(); + while (goto_level > label_level) { + goto_level--; + goto_node = goto_node->GetParent(); + } + while (label_level > goto_level) { + label_level--; + label_node = label_node->GetParent(); + } + while (goto_node->GetParent() != label_node->GetParent()) { + goto_node = goto_node->GetParent(); + label_node = label_node->GetParent(); + } + ASTNode current = goto_node->GetPrevious(); + while (current) { + if (current == label_node) { + return true; + } + current = current->GetPrevious(); + } + return false; +} + +bool ASTManager::IndirectlyRelated(const ASTNode& first, const ASTNode& second) const { + return !(first->GetParent() == second->GetParent() || DirectlyRelated(first, second)); +} + +bool ASTManager::DirectlyRelated(const ASTNode& first, const ASTNode& second) const { + if (first->GetParent() == second->GetParent()) { + return false; + } + const u32 first_level = first->GetLevel(); + const u32 second_level = second->GetLevel(); + u32 min_level; + u32 max_level; + ASTNode max; + ASTNode min; + if (first_level > second_level) { + min_level = second_level; + min = second; + max_level = first_level; + max = first; + } else { + min_level = first_level; + min = first; + max_level = second_level; + max = second; + } + + while (max_level > min_level) { + max_level--; + max = max->GetParent(); + } + + return min->GetParent() == max->GetParent(); +} + +void ASTManager::ShowCurrentState(std::string_view state) { + LOG_CRITICAL(HW_GPU, "\nState {}:\n\n{}\n", state, Print()); + SanityCheck(); +} + +void ASTManager::SanityCheck() { + for (auto& label : labels) { + if (!label->GetParent()) { + LOG_CRITICAL(HW_GPU, "Sanity Check Failed"); + } + } +} + +void ASTManager::EncloseDoWhile(ASTNode goto_node, ASTNode label) { + ASTZipper& zipper = goto_node->GetManager(); + const ASTNode loop_start = label->GetNext(); + if (loop_start == goto_node) { + zipper.Remove(goto_node); + return; + } + const ASTNode parent = label->GetParent(); + const Expr condition = goto_node->GetGotoCondition(); + zipper.DetachSegment(loop_start, goto_node); + const ASTNode do_while_node = ASTBase::Make<ASTDoWhile>(parent, condition); + ASTZipper* sub_zipper = do_while_node->GetSubNodes(); + sub_zipper->Init(loop_start, do_while_node); + zipper.InsertAfter(do_while_node, label); + sub_zipper->Remove(goto_node); +} + +void ASTManager::EncloseIfThen(ASTNode goto_node, ASTNode label) { + ASTZipper& zipper = goto_node->GetManager(); + const ASTNode if_end = label->GetPrevious(); + if (if_end == goto_node) { + zipper.Remove(goto_node); + return; + } + const ASTNode prev = goto_node->GetPrevious(); + const Expr condition = goto_node->GetGotoCondition(); + bool do_else = false; + if (!disable_else_derivation && prev->IsIfThen()) { + const Expr if_condition = prev->GetIfCondition(); + do_else = ExprAreEqual(if_condition, condition); + } + const ASTNode parent = label->GetParent(); + zipper.DetachSegment(goto_node, if_end); + ASTNode if_node; + if (do_else) { + if_node = ASTBase::Make<ASTIfElse>(parent); + } else { + Expr neg_condition = MakeExprNot(condition); + if_node = ASTBase::Make<ASTIfThen>(parent, neg_condition); + } + ASTZipper* sub_zipper = if_node->GetSubNodes(); + sub_zipper->Init(goto_node, if_node); + zipper.InsertAfter(if_node, prev); + sub_zipper->Remove(goto_node); +} + +void ASTManager::MoveOutward(ASTNode goto_node) { + ASTZipper& zipper = goto_node->GetManager(); + const ASTNode parent = goto_node->GetParent(); + ASTZipper& zipper2 = parent->GetManager(); + const ASTNode grandpa = parent->GetParent(); + const bool is_loop = parent->IsLoop(); + const bool is_else = parent->IsIfElse(); + const bool is_if = parent->IsIfThen(); + + const ASTNode prev = goto_node->GetPrevious(); + const ASTNode post = goto_node->GetNext(); + + const Expr condition = goto_node->GetGotoCondition(); + zipper.DetachSingle(goto_node); + if (is_loop) { + const u32 var_index = NewVariable(); + const Expr var_condition = MakeExpr<ExprVar>(var_index); + const ASTNode var_node = ASTBase::Make<ASTVarSet>(parent, var_index, condition); + const ASTNode var_node_init = ASTBase::Make<ASTVarSet>(parent, var_index, false_condition); + zipper2.InsertBefore(var_node_init, parent); + zipper.InsertAfter(var_node, prev); + goto_node->SetGotoCondition(var_condition); + const ASTNode break_node = ASTBase::Make<ASTBreak>(parent, var_condition); + zipper.InsertAfter(break_node, var_node); + } else if (is_if || is_else) { + const u32 var_index = NewVariable(); + const Expr var_condition = MakeExpr<ExprVar>(var_index); + const ASTNode var_node = ASTBase::Make<ASTVarSet>(parent, var_index, condition); + const ASTNode var_node_init = ASTBase::Make<ASTVarSet>(parent, var_index, false_condition); + if (is_if) { + zipper2.InsertBefore(var_node_init, parent); + } else { + zipper2.InsertBefore(var_node_init, parent->GetPrevious()); + } + zipper.InsertAfter(var_node, prev); + goto_node->SetGotoCondition(var_condition); + if (post) { + zipper.DetachTail(post); + const ASTNode if_node = ASTBase::Make<ASTIfThen>(parent, MakeExprNot(var_condition)); + ASTZipper* sub_zipper = if_node->GetSubNodes(); + sub_zipper->Init(post, if_node); + zipper.InsertAfter(if_node, var_node); + } + } else { + UNREACHABLE(); + } + const ASTNode next = parent->GetNext(); + if (is_if && next && next->IsIfElse()) { + zipper2.InsertAfter(goto_node, next); + goto_node->SetParent(grandpa); + return; + } + zipper2.InsertAfter(goto_node, parent); + goto_node->SetParent(grandpa); +} + +class ASTClearer { +public: + ASTClearer() = default; + + void operator()(const ASTProgram& ast) { + ASTNode current = ast.nodes.GetFirst(); + while (current) { + Visit(current); + current = current->GetNext(); + } + } + + void operator()(const ASTIfThen& ast) { + ASTNode current = ast.nodes.GetFirst(); + while (current) { + Visit(current); + current = current->GetNext(); + } + } + + void operator()(const ASTIfElse& ast) { + ASTNode current = ast.nodes.GetFirst(); + while (current) { + Visit(current); + current = current->GetNext(); + } + } + + void operator()([[maybe_unused]] const ASTBlockEncoded& ast) {} + + void operator()(ASTBlockDecoded& ast) { + ast.nodes.clear(); + } + + void operator()([[maybe_unused]] const ASTVarSet& ast) {} + + void operator()([[maybe_unused]] const ASTLabel& ast) {} + + void operator()([[maybe_unused]] const ASTGoto& ast) {} + + void operator()(const ASTDoWhile& ast) { + ASTNode current = ast.nodes.GetFirst(); + while (current) { + Visit(current); + current = current->GetNext(); + } + } + + void operator()([[maybe_unused]] const ASTReturn& ast) {} + + void operator()([[maybe_unused]] const ASTBreak& ast) {} + + void Visit(const ASTNode& node) { + std::visit(*this, *node->GetInnerData()); + node->Clear(); + } +}; + +void ASTManager::Clear() { + if (!main_node) { + return; + } + ASTClearer clearer{}; + clearer.Visit(main_node); + main_node.reset(); + program = nullptr; + labels_map.clear(); + labels.clear(); + gotos.clear(); +} + +} // namespace VideoCommon::Shader diff --git a/src/video_core/shader/ast.h b/src/video_core/shader/ast.h new file mode 100644 index 000000000..d7bf11821 --- /dev/null +++ b/src/video_core/shader/ast.h @@ -0,0 +1,400 @@ +// Copyright 2019 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <functional> +#include <list> +#include <memory> +#include <optional> +#include <string> +#include <unordered_map> +#include <vector> + +#include "video_core/shader/expr.h" +#include "video_core/shader/node.h" + +namespace VideoCommon::Shader { + +class ASTBase; +class ASTBlockDecoded; +class ASTBlockEncoded; +class ASTBreak; +class ASTDoWhile; +class ASTGoto; +class ASTIfElse; +class ASTIfThen; +class ASTLabel; +class ASTProgram; +class ASTReturn; +class ASTVarSet; + +using ASTData = std::variant<ASTProgram, ASTIfThen, ASTIfElse, ASTBlockEncoded, ASTBlockDecoded, + ASTVarSet, ASTGoto, ASTLabel, ASTDoWhile, ASTReturn, ASTBreak>; + +using ASTNode = std::shared_ptr<ASTBase>; + +enum class ASTZipperType : u32 { + Program, + IfThen, + IfElse, + Loop, +}; + +class ASTZipper final { +public: + explicit ASTZipper(); + + void Init(ASTNode first, ASTNode parent); + + ASTNode GetFirst() const { + return first; + } + + ASTNode GetLast() const { + return last; + } + + void PushBack(ASTNode new_node); + void PushFront(ASTNode new_node); + void InsertAfter(ASTNode new_node, ASTNode at_node); + void InsertBefore(ASTNode new_node, ASTNode at_node); + void DetachTail(ASTNode node); + void DetachSingle(ASTNode node); + void DetachSegment(ASTNode start, ASTNode end); + void Remove(ASTNode node); + + ASTNode first{}; + ASTNode last{}; +}; + +class ASTProgram { +public: + ASTZipper nodes{}; +}; + +class ASTIfThen { +public: + explicit ASTIfThen(Expr condition) : condition{std::move(condition)} {} + Expr condition; + ASTZipper nodes{}; +}; + +class ASTIfElse { +public: + ASTZipper nodes{}; +}; + +class ASTBlockEncoded { +public: + explicit ASTBlockEncoded(u32 start, u32 end) : start{start}, end{end} {} + u32 start; + u32 end; +}; + +class ASTBlockDecoded { +public: + explicit ASTBlockDecoded(NodeBlock&& new_nodes) : nodes(std::move(new_nodes)) {} + NodeBlock nodes; +}; + +class ASTVarSet { +public: + explicit ASTVarSet(u32 index, Expr condition) : index{index}, condition{std::move(condition)} {} + u32 index; + Expr condition; +}; + +class ASTLabel { +public: + explicit ASTLabel(u32 index) : index{index} {} + u32 index; + bool unused{}; +}; + +class ASTGoto { +public: + explicit ASTGoto(Expr condition, u32 label) : condition{std::move(condition)}, label{label} {} + Expr condition; + u32 label; +}; + +class ASTDoWhile { +public: + explicit ASTDoWhile(Expr condition) : condition{std::move(condition)} {} + Expr condition; + ASTZipper nodes{}; +}; + +class ASTReturn { +public: + explicit ASTReturn(Expr condition, bool kills) + : condition{std::move(condition)}, kills{kills} {} + Expr condition; + bool kills; +}; + +class ASTBreak { +public: + explicit ASTBreak(Expr condition) : condition{std::move(condition)} {} + Expr condition; +}; + +class ASTBase { +public: + explicit ASTBase(ASTNode parent, ASTData data) + : data{std::move(data)}, parent{std::move(parent)} {} + + template <class U, class... Args> + static ASTNode Make(ASTNode parent, Args&&... args) { + return std::make_shared<ASTBase>(std::move(parent), + ASTData(U(std::forward<Args>(args)...))); + } + + void SetParent(ASTNode new_parent) { + parent = std::move(new_parent); + } + + ASTNode& GetParent() { + return parent; + } + + const ASTNode& GetParent() const { + return parent; + } + + u32 GetLevel() const { + u32 level = 0; + auto next_parent = parent; + while (next_parent) { + next_parent = next_parent->GetParent(); + level++; + } + return level; + } + + ASTData* GetInnerData() { + return &data; + } + + const ASTData* GetInnerData() const { + return &data; + } + + ASTNode GetNext() const { + return next; + } + + ASTNode GetPrevious() const { + return previous; + } + + ASTZipper& GetManager() { + return *manager; + } + + const ASTZipper& GetManager() const { + return *manager; + } + + std::optional<u32> GetGotoLabel() const { + auto inner = std::get_if<ASTGoto>(&data); + if (inner) { + return {inner->label}; + } + return {}; + } + + Expr GetGotoCondition() const { + auto inner = std::get_if<ASTGoto>(&data); + if (inner) { + return inner->condition; + } + return nullptr; + } + + void MarkLabelUnused() { + auto inner = std::get_if<ASTLabel>(&data); + if (inner) { + inner->unused = true; + } + } + + bool IsLabelUnused() const { + auto inner = std::get_if<ASTLabel>(&data); + if (inner) { + return inner->unused; + } + return true; + } + + std::optional<u32> GetLabelIndex() const { + auto inner = std::get_if<ASTLabel>(&data); + if (inner) { + return {inner->index}; + } + return {}; + } + + Expr GetIfCondition() const { + auto inner = std::get_if<ASTIfThen>(&data); + if (inner) { + return inner->condition; + } + return nullptr; + } + + void SetGotoCondition(Expr new_condition) { + auto inner = std::get_if<ASTGoto>(&data); + if (inner) { + inner->condition = std::move(new_condition); + } + } + + bool IsIfThen() const { + return std::holds_alternative<ASTIfThen>(data); + } + + bool IsIfElse() const { + return std::holds_alternative<ASTIfElse>(data); + } + + bool IsBlockEncoded() const { + return std::holds_alternative<ASTBlockEncoded>(data); + } + + void TransformBlockEncoded(NodeBlock&& nodes) { + data = ASTBlockDecoded(std::move(nodes)); + } + + bool IsLoop() const { + return std::holds_alternative<ASTDoWhile>(data); + } + + ASTZipper* GetSubNodes() { + if (std::holds_alternative<ASTProgram>(data)) { + return &std::get_if<ASTProgram>(&data)->nodes; + } + if (std::holds_alternative<ASTIfThen>(data)) { + return &std::get_if<ASTIfThen>(&data)->nodes; + } + if (std::holds_alternative<ASTIfElse>(data)) { + return &std::get_if<ASTIfElse>(&data)->nodes; + } + if (std::holds_alternative<ASTDoWhile>(data)) { + return &std::get_if<ASTDoWhile>(&data)->nodes; + } + return nullptr; + } + + void Clear() { + next.reset(); + previous.reset(); + parent.reset(); + manager = nullptr; + } + +private: + friend class ASTZipper; + + ASTData data; + ASTNode parent{}; + ASTNode next{}; + ASTNode previous{}; + ASTZipper* manager{}; +}; + +class ASTManager final { +public: + ASTManager(bool full_decompile, bool disable_else_derivation); + ~ASTManager(); + + ASTManager(const ASTManager& o) = delete; + ASTManager& operator=(const ASTManager& other) = delete; + + ASTManager(ASTManager&& other) noexcept = default; + ASTManager& operator=(ASTManager&& other) noexcept = default; + + void Init(); + + void DeclareLabel(u32 address); + + void InsertLabel(u32 address); + + void InsertGoto(Expr condition, u32 address); + + void InsertBlock(u32 start_address, u32 end_address); + + void InsertReturn(Expr condition, bool kills); + + std::string Print(); + + void Decompile(); + + void ShowCurrentState(std::string_view state); + + void SanityCheck(); + + void Clear(); + + bool IsFullyDecompiled() const { + if (full_decompile) { + return gotos.empty(); + } + + for (ASTNode goto_node : gotos) { + auto label_index = goto_node->GetGotoLabel(); + if (!label_index) { + return false; + } + ASTNode glabel = labels[*label_index]; + if (IsBackwardsJump(goto_node, glabel)) { + return false; + } + } + return true; + } + + ASTNode GetProgram() const { + return main_node; + } + + u32 GetVariables() const { + return variables; + } + + const std::vector<ASTNode>& GetLabels() const { + return labels; + } + +private: + bool IsBackwardsJump(ASTNode goto_node, ASTNode label_node) const; + + bool IndirectlyRelated(const ASTNode& first, const ASTNode& second) const; + + bool DirectlyRelated(const ASTNode& first, const ASTNode& second) const; + + void EncloseDoWhile(ASTNode goto_node, ASTNode label); + + void EncloseIfThen(ASTNode goto_node, ASTNode label); + + void MoveOutward(ASTNode goto_node); + + u32 NewVariable() { + return variables++; + } + + bool full_decompile{}; + bool disable_else_derivation{}; + std::unordered_map<u32, u32> labels_map{}; + u32 labels_count{}; + std::vector<ASTNode> labels{}; + std::list<ASTNode> gotos{}; + u32 variables{}; + ASTProgram* program{}; + ASTNode main_node{}; + Expr false_condition{}; +}; + +} // namespace VideoCommon::Shader diff --git a/src/video_core/shader/compiler_settings.cpp b/src/video_core/shader/compiler_settings.cpp new file mode 100644 index 000000000..cddcbd4f0 --- /dev/null +++ b/src/video_core/shader/compiler_settings.cpp @@ -0,0 +1,26 @@ +// Copyright 2019 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "video_core/shader/compiler_settings.h" + +namespace VideoCommon::Shader { + +std::string CompileDepthAsString(const CompileDepth cd) { + switch (cd) { + case CompileDepth::BruteForce: + return "Brute Force Compile"; + case CompileDepth::FlowStack: + return "Simple Flow Stack Mode"; + case CompileDepth::NoFlowStack: + return "Remove Flow Stack"; + case CompileDepth::DecompileBackwards: + return "Decompile Backward Jumps"; + case CompileDepth::FullDecompile: + return "Full Decompilation"; + default: + return "Unknown Compiler Process"; + } +} + +} // namespace VideoCommon::Shader diff --git a/src/video_core/shader/compiler_settings.h b/src/video_core/shader/compiler_settings.h new file mode 100644 index 000000000..916018c01 --- /dev/null +++ b/src/video_core/shader/compiler_settings.h @@ -0,0 +1,26 @@ +// Copyright 2019 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "video_core/engines/shader_bytecode.h" + +namespace VideoCommon::Shader { + +enum class CompileDepth : u32 { + BruteForce = 0, + FlowStack = 1, + NoFlowStack = 2, + DecompileBackwards = 3, + FullDecompile = 4, +}; + +std::string CompileDepthAsString(CompileDepth cd); + +struct CompilerSettings { + CompileDepth depth{CompileDepth::NoFlowStack}; + bool disable_else_derivation{true}; +}; + +} // namespace VideoCommon::Shader diff --git a/src/video_core/shader/control_flow.cpp b/src/video_core/shader/control_flow.cpp index ec3a76690..268d1aed0 100644 --- a/src/video_core/shader/control_flow.cpp +++ b/src/video_core/shader/control_flow.cpp @@ -4,13 +4,14 @@ #include <list> #include <map> +#include <set> #include <stack> #include <unordered_map> -#include <unordered_set> #include <vector> #include "common/assert.h" #include "common/common_types.h" +#include "video_core/shader/ast.h" #include "video_core/shader/control_flow.h" #include "video_core/shader/shader_ir.h" @@ -64,12 +65,13 @@ struct CFGRebuildState { std::list<u32> inspect_queries{}; std::list<Query> queries{}; std::unordered_map<u32, u32> registered{}; - std::unordered_set<u32> labels{}; + std::set<u32> labels{}; std::map<u32, u32> ssy_labels{}; std::map<u32, u32> pbk_labels{}; std::unordered_map<u32, BlockStack> stacks{}; const ProgramCode& program_code; const std::size_t program_size; + ASTManager* manager; }; enum class BlockCollision : u32 { None, Found, Inside }; @@ -415,38 +417,133 @@ bool TryQuery(CFGRebuildState& state) { } } // Anonymous namespace -std::optional<ShaderCharacteristics> ScanFlow(const ProgramCode& program_code, - std::size_t program_size, u32 start_address) { - CFGRebuildState state{program_code, program_size, start_address}; +void InsertBranch(ASTManager& mm, const BlockBranchInfo& branch) { + const auto get_expr = ([&](const Condition& cond) -> Expr { + Expr result{}; + if (cond.cc != ConditionCode::T) { + result = MakeExpr<ExprCondCode>(cond.cc); + } + if (cond.predicate != Pred::UnusedIndex) { + u32 pred = static_cast<u32>(cond.predicate); + bool negate = false; + if (pred > 7) { + negate = true; + pred -= 8; + } + Expr extra = MakeExpr<ExprPredicate>(pred); + if (negate) { + extra = MakeExpr<ExprNot>(extra); + } + if (result) { + return MakeExpr<ExprAnd>(extra, result); + } + return extra; + } + if (result) { + return result; + } + return MakeExpr<ExprBoolean>(true); + }); + if (branch.address < 0) { + if (branch.kill) { + mm.InsertReturn(get_expr(branch.condition), true); + return; + } + mm.InsertReturn(get_expr(branch.condition), false); + return; + } + mm.InsertGoto(get_expr(branch.condition), branch.address); +} + +void DecompileShader(CFGRebuildState& state) { + state.manager->Init(); + for (auto label : state.labels) { + state.manager->DeclareLabel(label); + } + for (auto& block : state.block_info) { + if (state.labels.count(block.start) != 0) { + state.manager->InsertLabel(block.start); + } + u32 end = block.branch.ignore ? block.end + 1 : block.end; + state.manager->InsertBlock(block.start, end); + if (!block.branch.ignore) { + InsertBranch(*state.manager, block.branch); + } + } + state.manager->Decompile(); +} + +std::unique_ptr<ShaderCharacteristics> ScanFlow(const ProgramCode& program_code, u32 program_size, + u32 start_address, + const CompilerSettings& settings) { + auto result_out = std::make_unique<ShaderCharacteristics>(); + if (settings.depth == CompileDepth::BruteForce) { + result_out->settings.depth = CompileDepth::BruteForce; + return result_out; + } + CFGRebuildState state{program_code, program_size, start_address}; // Inspect Code and generate blocks state.labels.clear(); state.labels.emplace(start_address); state.inspect_queries.push_back(state.start); while (!state.inspect_queries.empty()) { if (!TryInspectAddress(state)) { - return {}; + result_out->settings.depth = CompileDepth::BruteForce; + return result_out; } } - // Decompile Stacks - state.queries.push_back(Query{state.start, {}, {}}); - bool decompiled = true; - while (!state.queries.empty()) { - if (!TryQuery(state)) { - decompiled = false; - break; + bool use_flow_stack = true; + + bool decompiled = false; + + if (settings.depth != CompileDepth::FlowStack) { + // Decompile Stacks + state.queries.push_back(Query{state.start, {}, {}}); + decompiled = true; + while (!state.queries.empty()) { + if (!TryQuery(state)) { + decompiled = false; + break; + } } } + use_flow_stack = !decompiled; + // Sort and organize results std::sort(state.block_info.begin(), state.block_info.end(), - [](const BlockInfo& a, const BlockInfo& b) { return a.start < b.start; }); - ShaderCharacteristics result_out{}; - result_out.decompilable = decompiled; - result_out.start = start_address; - result_out.end = start_address; - for (const auto& block : state.block_info) { + [](const BlockInfo& a, const BlockInfo& b) -> bool { return a.start < b.start; }); + if (decompiled && settings.depth != CompileDepth::NoFlowStack) { + ASTManager manager{settings.depth != CompileDepth::DecompileBackwards, + settings.disable_else_derivation}; + state.manager = &manager; + DecompileShader(state); + decompiled = state.manager->IsFullyDecompiled(); + if (!decompiled) { + if (settings.depth == CompileDepth::FullDecompile) { + LOG_CRITICAL(HW_GPU, "Failed to remove all the gotos!:"); + } else { + LOG_CRITICAL(HW_GPU, "Failed to remove all backward gotos!:"); + } + state.manager->ShowCurrentState("Of Shader"); + state.manager->Clear(); + } else { + auto characteristics = std::make_unique<ShaderCharacteristics>(); + characteristics->start = start_address; + characteristics->settings.depth = settings.depth; + characteristics->manager = std::move(manager); + characteristics->end = state.block_info.back().end + 1; + return characteristics; + } + } + + result_out->start = start_address; + result_out->settings.depth = + use_flow_stack ? CompileDepth::FlowStack : CompileDepth::NoFlowStack; + result_out->blocks.clear(); + for (auto& block : state.block_info) { ShaderBlock new_block{}; new_block.start = block.start; new_block.end = block.end; @@ -456,26 +553,26 @@ std::optional<ShaderCharacteristics> ScanFlow(const ProgramCode& program_code, new_block.branch.kills = block.branch.kill; new_block.branch.address = block.branch.address; } - result_out.end = std::max(result_out.end, block.end); - result_out.blocks.push_back(new_block); + result_out->end = std::max(result_out->end, block.end); + result_out->blocks.push_back(new_block); } - if (result_out.decompilable) { - result_out.labels = std::move(state.labels); - return {std::move(result_out)}; + if (!use_flow_stack) { + result_out->labels = std::move(state.labels); + return result_out; } - // If it's not decompilable, merge the unlabelled blocks together - auto back = result_out.blocks.begin(); + auto back = result_out->blocks.begin(); auto next = std::next(back); - while (next != result_out.blocks.end()) { + while (next != result_out->blocks.end()) { if (state.labels.count(next->start) == 0 && next->start == back->end + 1) { back->end = next->end; - next = result_out.blocks.erase(next); + next = result_out->blocks.erase(next); continue; } back = next; ++next; } - return {std::move(result_out)}; + + return result_out; } } // namespace VideoCommon::Shader diff --git a/src/video_core/shader/control_flow.h b/src/video_core/shader/control_flow.h index b0a5e4f8c..74e54a5c7 100644 --- a/src/video_core/shader/control_flow.h +++ b/src/video_core/shader/control_flow.h @@ -6,9 +6,11 @@ #include <list> #include <optional> -#include <unordered_set> +#include <set> #include "video_core/engines/shader_bytecode.h" +#include "video_core/shader/ast.h" +#include "video_core/shader/compiler_settings.h" #include "video_core/shader/shader_ir.h" namespace VideoCommon::Shader { @@ -67,13 +69,15 @@ struct ShaderBlock { struct ShaderCharacteristics { std::list<ShaderBlock> blocks{}; - bool decompilable{}; + std::set<u32> labels{}; u32 start{}; u32 end{}; - std::unordered_set<u32> labels{}; + ASTManager manager{true, true}; + CompilerSettings settings{}; }; -std::optional<ShaderCharacteristics> ScanFlow(const ProgramCode& program_code, - std::size_t program_size, u32 start_address); +std::unique_ptr<ShaderCharacteristics> ScanFlow(const ProgramCode& program_code, u32 program_size, + u32 start_address, + const CompilerSettings& settings); } // namespace VideoCommon::Shader diff --git a/src/video_core/shader/decode.cpp b/src/video_core/shader/decode.cpp index 47a9fd961..2626b1616 100644 --- a/src/video_core/shader/decode.cpp +++ b/src/video_core/shader/decode.cpp @@ -35,58 +35,138 @@ constexpr bool IsSchedInstruction(u32 offset, u32 main_offset) { } // namespace +class ASTDecoder { +public: + ASTDecoder(ShaderIR& ir) : ir(ir) {} + + void operator()(ASTProgram& ast) { + ASTNode current = ast.nodes.GetFirst(); + while (current) { + Visit(current); + current = current->GetNext(); + } + } + + void operator()(ASTIfThen& ast) { + ASTNode current = ast.nodes.GetFirst(); + while (current) { + Visit(current); + current = current->GetNext(); + } + } + + void operator()(ASTIfElse& ast) { + ASTNode current = ast.nodes.GetFirst(); + while (current) { + Visit(current); + current = current->GetNext(); + } + } + + void operator()(ASTBlockEncoded& ast) {} + + void operator()(ASTBlockDecoded& ast) {} + + void operator()(ASTVarSet& ast) {} + + void operator()(ASTLabel& ast) {} + + void operator()(ASTGoto& ast) {} + + void operator()(ASTDoWhile& ast) { + ASTNode current = ast.nodes.GetFirst(); + while (current) { + Visit(current); + current = current->GetNext(); + } + } + + void operator()(ASTReturn& ast) {} + + void operator()(ASTBreak& ast) {} + + void Visit(ASTNode& node) { + std::visit(*this, *node->GetInnerData()); + if (node->IsBlockEncoded()) { + auto block = std::get_if<ASTBlockEncoded>(node->GetInnerData()); + NodeBlock bb = ir.DecodeRange(block->start, block->end); + node->TransformBlockEncoded(std::move(bb)); + } + } + +private: + ShaderIR& ir; +}; + void ShaderIR::Decode() { std::memcpy(&header, program_code.data(), sizeof(Tegra::Shader::Header)); - disable_flow_stack = false; - const auto info = ScanFlow(program_code, program_size, main_offset); - if (info) { - const auto& shader_info = *info; - coverage_begin = shader_info.start; - coverage_end = shader_info.end; - if (shader_info.decompilable) { - disable_flow_stack = true; - const auto insert_block = [this](NodeBlock& nodes, u32 label) { - if (label == static_cast<u32>(exit_branch)) { - return; - } - basic_blocks.insert({label, nodes}); - }; - const auto& blocks = shader_info.blocks; - NodeBlock current_block; - u32 current_label = static_cast<u32>(exit_branch); - for (auto& block : blocks) { - if (shader_info.labels.count(block.start) != 0) { - insert_block(current_block, current_label); - current_block.clear(); - current_label = block.start; - } - if (!block.ignore_branch) { - DecodeRangeInner(current_block, block.start, block.end); - InsertControlFlow(current_block, block); - } else { - DecodeRangeInner(current_block, block.start, block.end + 1); - } - } - insert_block(current_block, current_label); - return; - } - LOG_WARNING(HW_GPU, "Flow Stack Removing Failed! Falling back to old method"); - // we can't decompile it, fallback to standard method + decompiled = false; + auto info = ScanFlow(program_code, program_size, main_offset, settings); + auto& shader_info = *info; + coverage_begin = shader_info.start; + coverage_end = shader_info.end; + switch (shader_info.settings.depth) { + case CompileDepth::FlowStack: { for (const auto& block : shader_info.blocks) { basic_blocks.insert({block.start, DecodeRange(block.start, block.end + 1)}); } - return; + break; } - LOG_WARNING(HW_GPU, "Flow Analysis Failed! Falling back to brute force compiling"); - - // Now we need to deal with an undecompilable shader. We need to brute force - // a shader that captures every position. - coverage_begin = main_offset; - const u32 shader_end = static_cast<u32>(program_size / sizeof(u64)); - coverage_end = shader_end; - for (u32 label = main_offset; label < shader_end; label++) { - basic_blocks.insert({label, DecodeRange(label, label + 1)}); + case CompileDepth::NoFlowStack: { + disable_flow_stack = true; + const auto insert_block = [this](NodeBlock& nodes, u32 label) { + if (label == static_cast<u32>(exit_branch)) { + return; + } + basic_blocks.insert({label, nodes}); + }; + const auto& blocks = shader_info.blocks; + NodeBlock current_block; + u32 current_label = static_cast<u32>(exit_branch); + for (auto& block : blocks) { + if (shader_info.labels.count(block.start) != 0) { + insert_block(current_block, current_label); + current_block.clear(); + current_label = block.start; + } + if (!block.ignore_branch) { + DecodeRangeInner(current_block, block.start, block.end); + InsertControlFlow(current_block, block); + } else { + DecodeRangeInner(current_block, block.start, block.end + 1); + } + } + insert_block(current_block, current_label); + break; + } + case CompileDepth::DecompileBackwards: + case CompileDepth::FullDecompile: { + program_manager = std::move(shader_info.manager); + disable_flow_stack = true; + decompiled = true; + ASTDecoder decoder{*this}; + ASTNode program = GetASTProgram(); + decoder.Visit(program); + break; + } + default: + LOG_CRITICAL(HW_GPU, "Unknown decompilation mode!"); + [[fallthrough]]; + case CompileDepth::BruteForce: { + coverage_begin = main_offset; + const u32 shader_end = static_cast<u32>(program_size / sizeof(u64)); + coverage_end = shader_end; + for (u32 label = main_offset; label < shader_end; label++) { + basic_blocks.insert({label, DecodeRange(label, label + 1)}); + } + break; + } + } + if (settings.depth != shader_info.settings.depth) { + LOG_WARNING( + HW_GPU, "Decompiling to this setting \"{}\" failed, downgrading to this setting \"{}\"", + CompileDepthAsString(settings.depth), CompileDepthAsString(shader_info.settings.depth)); } } diff --git a/src/video_core/shader/decode/half_set_predicate.cpp b/src/video_core/shader/decode/half_set_predicate.cpp index 840694527..fec8f2dbe 100644 --- a/src/video_core/shader/decode/half_set_predicate.cpp +++ b/src/video_core/shader/decode/half_set_predicate.cpp @@ -4,6 +4,7 @@ #include "common/assert.h" #include "common/common_types.h" +#include "common/logging/log.h" #include "video_core/engines/shader_bytecode.h" #include "video_core/shader/node_helper.h" #include "video_core/shader/shader_ir.h" @@ -18,7 +19,7 @@ u32 ShaderIR::DecodeHalfSetPredicate(NodeBlock& bb, u32 pc) { const Instruction instr = {program_code[pc]}; const auto opcode = OpCode::Decode(instr); - DEBUG_ASSERT(instr.hsetp2.ftz == 0); + LOG_DEBUG(HW_GPU, "ftz={}", static_cast<u32>(instr.hsetp2.ftz)); Node op_a = UnpackHalfFloat(GetRegister(instr.gpr8), instr.hsetp2.type_a); op_a = GetOperandAbsNegHalf(op_a, instr.hsetp2.abs_a, instr.hsetp2.negate_a); @@ -32,6 +33,8 @@ u32 ShaderIR::DecodeHalfSetPredicate(NodeBlock& bb, u32 pc) { h_and = instr.hsetp2.cbuf_and_imm.h_and; op_b = GetOperandAbsNegHalf(GetConstBuffer(instr.cbuf34.index, instr.cbuf34.GetOffset()), instr.hsetp2.cbuf.abs_b, instr.hsetp2.cbuf.negate_b); + // F32 is hardcoded in hardware + op_b = UnpackHalfFloat(std::move(op_b), Tegra::Shader::HalfType::F32); break; case OpCode::Id::HSETP2_IMM: cond = instr.hsetp2.cbuf_and_imm.cond; diff --git a/src/video_core/shader/expr.cpp b/src/video_core/shader/expr.cpp new file mode 100644 index 000000000..2647865d4 --- /dev/null +++ b/src/video_core/shader/expr.cpp @@ -0,0 +1,93 @@ +// Copyright 2019 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <memory> +#include <variant> + +#include "video_core/shader/expr.h" + +namespace VideoCommon::Shader { +namespace { +bool ExprIsBoolean(const Expr& expr) { + return std::holds_alternative<ExprBoolean>(*expr); +} + +bool ExprBooleanGet(const Expr& expr) { + return std::get_if<ExprBoolean>(expr.get())->value; +} +} // Anonymous namespace + +bool ExprAnd::operator==(const ExprAnd& b) const { + return (*operand1 == *b.operand1) && (*operand2 == *b.operand2); +} + +bool ExprAnd::operator!=(const ExprAnd& b) const { + return !operator==(b); +} + +bool ExprOr::operator==(const ExprOr& b) const { + return (*operand1 == *b.operand1) && (*operand2 == *b.operand2); +} + +bool ExprOr::operator!=(const ExprOr& b) const { + return !operator==(b); +} + +bool ExprNot::operator==(const ExprNot& b) const { + return *operand1 == *b.operand1; +} + +bool ExprNot::operator!=(const ExprNot& b) const { + return !operator==(b); +} + +Expr MakeExprNot(Expr first) { + if (std::holds_alternative<ExprNot>(*first)) { + return std::get_if<ExprNot>(first.get())->operand1; + } + return MakeExpr<ExprNot>(std::move(first)); +} + +Expr MakeExprAnd(Expr first, Expr second) { + if (ExprIsBoolean(first)) { + return ExprBooleanGet(first) ? second : first; + } + if (ExprIsBoolean(second)) { + return ExprBooleanGet(second) ? first : second; + } + return MakeExpr<ExprAnd>(std::move(first), std::move(second)); +} + +Expr MakeExprOr(Expr first, Expr second) { + if (ExprIsBoolean(first)) { + return ExprBooleanGet(first) ? first : second; + } + if (ExprIsBoolean(second)) { + return ExprBooleanGet(second) ? second : first; + } + return MakeExpr<ExprOr>(std::move(first), std::move(second)); +} + +bool ExprAreEqual(const Expr& first, const Expr& second) { + return (*first) == (*second); +} + +bool ExprAreOpposite(const Expr& first, const Expr& second) { + if (std::holds_alternative<ExprNot>(*first)) { + return ExprAreEqual(std::get_if<ExprNot>(first.get())->operand1, second); + } + if (std::holds_alternative<ExprNot>(*second)) { + return ExprAreEqual(std::get_if<ExprNot>(second.get())->operand1, first); + } + return false; +} + +bool ExprIsTrue(const Expr& first) { + if (ExprIsBoolean(first)) { + return ExprBooleanGet(first); + } + return false; +} + +} // namespace VideoCommon::Shader diff --git a/src/video_core/shader/expr.h b/src/video_core/shader/expr.h new file mode 100644 index 000000000..d3dcd00ec --- /dev/null +++ b/src/video_core/shader/expr.h @@ -0,0 +1,139 @@ +// Copyright 2019 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <memory> +#include <variant> + +#include "video_core/engines/shader_bytecode.h" + +namespace VideoCommon::Shader { + +using Tegra::Shader::ConditionCode; +using Tegra::Shader::Pred; + +class ExprAnd; +class ExprBoolean; +class ExprCondCode; +class ExprNot; +class ExprOr; +class ExprPredicate; +class ExprVar; + +using ExprData = + std::variant<ExprVar, ExprCondCode, ExprPredicate, ExprNot, ExprOr, ExprAnd, ExprBoolean>; +using Expr = std::shared_ptr<ExprData>; + +class ExprAnd final { +public: + explicit ExprAnd(Expr a, Expr b) : operand1{std::move(a)}, operand2{std::move(b)} {} + + bool operator==(const ExprAnd& b) const; + bool operator!=(const ExprAnd& b) const; + + Expr operand1; + Expr operand2; +}; + +class ExprOr final { +public: + explicit ExprOr(Expr a, Expr b) : operand1{std::move(a)}, operand2{std::move(b)} {} + + bool operator==(const ExprOr& b) const; + bool operator!=(const ExprOr& b) const; + + Expr operand1; + Expr operand2; +}; + +class ExprNot final { +public: + explicit ExprNot(Expr a) : operand1{std::move(a)} {} + + bool operator==(const ExprNot& b) const; + bool operator!=(const ExprNot& b) const; + + Expr operand1; +}; + +class ExprVar final { +public: + explicit ExprVar(u32 index) : var_index{index} {} + + bool operator==(const ExprVar& b) const { + return var_index == b.var_index; + } + + bool operator!=(const ExprVar& b) const { + return !operator==(b); + } + + u32 var_index; +}; + +class ExprPredicate final { +public: + explicit ExprPredicate(u32 predicate) : predicate{predicate} {} + + bool operator==(const ExprPredicate& b) const { + return predicate == b.predicate; + } + + bool operator!=(const ExprPredicate& b) const { + return !operator==(b); + } + + u32 predicate; +}; + +class ExprCondCode final { +public: + explicit ExprCondCode(ConditionCode cc) : cc{cc} {} + + bool operator==(const ExprCondCode& b) const { + return cc == b.cc; + } + + bool operator!=(const ExprCondCode& b) const { + return !operator==(b); + } + + ConditionCode cc; +}; + +class ExprBoolean final { +public: + explicit ExprBoolean(bool val) : value{val} {} + + bool operator==(const ExprBoolean& b) const { + return value == b.value; + } + + bool operator!=(const ExprBoolean& b) const { + return !operator==(b); + } + + bool value; +}; + +template <typename T, typename... Args> +Expr MakeExpr(Args&&... args) { + static_assert(std::is_convertible_v<T, ExprData>); + return std::make_shared<ExprData>(T(std::forward<Args>(args)...)); +} + +bool ExprAreEqual(const Expr& first, const Expr& second); + +bool ExprAreOpposite(const Expr& first, const Expr& second); + +Expr MakeExprNot(Expr first); + +Expr MakeExprAnd(Expr first, Expr second); + +Expr MakeExprOr(Expr first, Expr second); + +bool ExprIsTrue(const Expr& first); + +} // namespace VideoCommon::Shader diff --git a/src/video_core/shader/shader_ir.cpp b/src/video_core/shader/shader_ir.cpp index 2c357f310..c1f2b88c8 100644 --- a/src/video_core/shader/shader_ir.cpp +++ b/src/video_core/shader/shader_ir.cpp @@ -22,8 +22,10 @@ using Tegra::Shader::PredCondition; using Tegra::Shader::PredOperation; using Tegra::Shader::Register; -ShaderIR::ShaderIR(const ProgramCode& program_code, u32 main_offset, const std::size_t size) - : program_code{program_code}, main_offset{main_offset}, program_size{size} { +ShaderIR::ShaderIR(const ProgramCode& program_code, u32 main_offset, const std::size_t size, + CompilerSettings settings) + : program_code{program_code}, main_offset{main_offset}, program_size{size}, basic_blocks{}, + program_manager{true, true}, settings{settings} { Decode(); } @@ -137,7 +139,7 @@ Node ShaderIR::GetOutputAttribute(Attribute::Index index, u64 element, Node buff return MakeNode<AbufNode>(index, static_cast<u32>(element), std::move(buffer)); } -Node ShaderIR::GetInternalFlag(InternalFlag flag, bool negated) { +Node ShaderIR::GetInternalFlag(InternalFlag flag, bool negated) const { const Node node = MakeNode<InternalFlagNode>(flag); if (negated) { return Operation(OperationCode::LogicalNegate, node); @@ -367,13 +369,13 @@ OperationCode ShaderIR::GetPredicateCombiner(PredOperation operation) { return op->second; } -Node ShaderIR::GetConditionCode(Tegra::Shader::ConditionCode cc) { +Node ShaderIR::GetConditionCode(Tegra::Shader::ConditionCode cc) const { switch (cc) { case Tegra::Shader::ConditionCode::NEU: return GetInternalFlag(InternalFlag::Zero, true); default: UNIMPLEMENTED_MSG("Unimplemented condition code: {}", static_cast<u32>(cc)); - return GetPredicate(static_cast<u64>(Pred::NeverExecute)); + return MakeNode<PredicateNode>(Pred::NeverExecute, false); } } diff --git a/src/video_core/shader/shader_ir.h b/src/video_core/shader/shader_ir.h index 6f666ee30..105981d67 100644 --- a/src/video_core/shader/shader_ir.h +++ b/src/video_core/shader/shader_ir.h @@ -15,6 +15,8 @@ #include "video_core/engines/maxwell_3d.h" #include "video_core/engines/shader_bytecode.h" #include "video_core/engines/shader_header.h" +#include "video_core/shader/ast.h" +#include "video_core/shader/compiler_settings.h" #include "video_core/shader/node.h" namespace VideoCommon::Shader { @@ -64,7 +66,8 @@ struct GlobalMemoryUsage { class ShaderIR final { public: - explicit ShaderIR(const ProgramCode& program_code, u32 main_offset, std::size_t size); + explicit ShaderIR(const ProgramCode& program_code, u32 main_offset, std::size_t size, + CompilerSettings settings); ~ShaderIR(); const std::map<u32, NodeBlock>& GetBasicBlocks() const { @@ -144,11 +147,31 @@ public: return disable_flow_stack; } + bool IsDecompiled() const { + return decompiled; + } + + const ASTManager& GetASTManager() const { + return program_manager; + } + + ASTNode GetASTProgram() const { + return program_manager.GetProgram(); + } + + u32 GetASTNumVariables() const { + return program_manager.GetVariables(); + } + u32 ConvertAddressToNvidiaSpace(const u32 address) const { return (address - main_offset) * sizeof(Tegra::Shader::Instruction); } + /// Returns a condition code evaluated from internal flags + Node GetConditionCode(Tegra::Shader::ConditionCode cc) const; + private: + friend class ASTDecoder; void Decode(); NodeBlock DecodeRange(u32 begin, u32 end); @@ -213,7 +236,7 @@ private: /// Generates a node representing an output attribute. Keeps track of used attributes. Node GetOutputAttribute(Tegra::Shader::Attribute::Index index, u64 element, Node buffer); /// Generates a node representing an internal flag - Node GetInternalFlag(InternalFlag flag, bool negated = false); + Node GetInternalFlag(InternalFlag flag, bool negated = false) const; /// Generates a node representing a local memory address Node GetLocalMemory(Node address); /// Generates a node representing a shared memory address @@ -271,9 +294,6 @@ private: /// Returns a predicate combiner operation OperationCode GetPredicateCombiner(Tegra::Shader::PredOperation operation); - /// Returns a condition code evaluated from internal flags - Node GetConditionCode(Tegra::Shader::ConditionCode cc); - /// Accesses a texture sampler const Sampler& GetSampler(const Tegra::Shader::Sampler& sampler, Tegra::Shader::TextureType type, bool is_array, bool is_shadow); @@ -357,6 +377,7 @@ private: const ProgramCode& program_code; const u32 main_offset; const std::size_t program_size; + bool decompiled{}; bool disable_flow_stack{}; u32 coverage_begin{}; @@ -364,6 +385,8 @@ private: std::map<u32, NodeBlock> basic_blocks; NodeBlock global_code; + ASTManager program_manager; + CompilerSettings settings{}; std::set<u32> used_registers; std::set<Tegra::Shader::Pred> used_predicates; diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index 877c6635d..ca2da8f97 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -224,8 +224,13 @@ public: const Tegra::Engines::Fermi2D::Regs::Surface& dst_config, const Tegra::Engines::Fermi2D::Config& copy_config) { std::lock_guard lock{mutex}; - std::pair<TSurface, TView> dst_surface = GetFermiSurface(dst_config); - std::pair<TSurface, TView> src_surface = GetFermiSurface(src_config); + SurfaceParams src_params = SurfaceParams::CreateForFermiCopySurface(src_config); + SurfaceParams dst_params = SurfaceParams::CreateForFermiCopySurface(dst_config); + const GPUVAddr src_gpu_addr = src_config.Address(); + const GPUVAddr dst_gpu_addr = dst_config.Address(); + DeduceBestBlit(src_params, dst_params, src_gpu_addr, dst_gpu_addr); + std::pair<TSurface, TView> dst_surface = GetSurface(dst_gpu_addr, dst_params, true, false); + std::pair<TSurface, TView> src_surface = GetSurface(src_gpu_addr, src_params, true, false); ImageBlit(src_surface.second, dst_surface.second, copy_config); dst_surface.first->MarkAsModified(true, Tick()); } @@ -357,6 +362,29 @@ private: BufferCopy = 3, }; + enum class DeductionType : u32 { + DeductionComplete, + DeductionIncomplete, + DeductionFailed, + }; + + struct Deduction { + DeductionType type{DeductionType::DeductionFailed}; + TSurface surface{}; + + bool Failed() const { + return type == DeductionType::DeductionFailed; + } + + bool Incomplete() const { + return type == DeductionType::DeductionIncomplete; + } + + bool IsDepth() const { + return surface->GetSurfaceParams().IsPixelFormatZeta(); + } + }; + /** * `PickStrategy` takes care of selecting a proper strategy to deal with a texture recycle. * @param overlaps, the overlapping surfaces registered in the cache. @@ -691,6 +719,120 @@ private: MatchTopologyResult::FullMatch); } + /** + * `DeduceSurface` gets the starting address and parameters of a candidate surface and tries + * to find a matching surface within the cache that's similar to it. If there are many textures + * or the texture found if entirely incompatible, it will fail. If no texture is found, the + * blit will be unsuccessful. + * @param gpu_addr, the starting address of the candidate surface. + * @param params, the paremeters on the candidate surface. + **/ + Deduction DeduceSurface(const GPUVAddr gpu_addr, const SurfaceParams& params) { + const auto host_ptr{system.GPU().MemoryManager().GetPointer(gpu_addr)}; + const auto cache_addr{ToCacheAddr(host_ptr)}; + + if (!cache_addr) { + Deduction result{}; + result.type = DeductionType::DeductionFailed; + return result; + } + + if (const auto iter = l1_cache.find(cache_addr); iter != l1_cache.end()) { + TSurface& current_surface = iter->second; + const auto topological_result = current_surface->MatchesTopology(params); + if (topological_result != MatchTopologyResult::FullMatch) { + Deduction result{}; + result.type = DeductionType::DeductionFailed; + return result; + } + const auto struct_result = current_surface->MatchesStructure(params); + if (struct_result != MatchStructureResult::None && + current_surface->MatchTarget(params.target)) { + Deduction result{}; + result.type = DeductionType::DeductionComplete; + result.surface = current_surface; + return result; + } + } + + const std::size_t candidate_size = params.GetGuestSizeInBytes(); + auto overlaps{GetSurfacesInRegion(cache_addr, candidate_size)}; + + if (overlaps.empty()) { + Deduction result{}; + result.type = DeductionType::DeductionIncomplete; + return result; + } + + if (overlaps.size() > 1) { + Deduction result{}; + result.type = DeductionType::DeductionFailed; + return result; + } else { + Deduction result{}; + result.type = DeductionType::DeductionComplete; + result.surface = overlaps[0]; + return result; + } + } + + /** + * `DeduceBestBlit` gets the a source and destination starting address and parameters, + * and tries to deduce if they are supposed to be depth textures. If so, their + * parameters are modified and fixed into so. + * @param gpu_addr, the starting address of the candidate surface. + * @param params, the parameters on the candidate surface. + **/ + void DeduceBestBlit(SurfaceParams& src_params, SurfaceParams& dst_params, + const GPUVAddr src_gpu_addr, const GPUVAddr dst_gpu_addr) { + auto deduced_src = DeduceSurface(src_gpu_addr, src_params); + auto deduced_dst = DeduceSurface(src_gpu_addr, src_params); + if (deduced_src.Failed() || deduced_dst.Failed()) { + return; + } + + const bool incomplete_src = deduced_src.Incomplete(); + const bool incomplete_dst = deduced_dst.Incomplete(); + + if (incomplete_src && incomplete_dst) { + return; + } + + const bool any_incomplete = incomplete_src || incomplete_dst; + + if (!any_incomplete) { + if (!(deduced_src.IsDepth() && deduced_dst.IsDepth())) { + return; + } + } else { + if (incomplete_src && !(deduced_dst.IsDepth())) { + return; + } + + if (incomplete_dst && !(deduced_src.IsDepth())) { + return; + } + } + + const auto inherit_format = ([](SurfaceParams& to, TSurface from) { + const SurfaceParams& params = from->GetSurfaceParams(); + to.pixel_format = params.pixel_format; + to.component_type = params.component_type; + to.type = params.type; + }); + // Now we got the cases where one or both is Depth and the other is not known + if (!incomplete_src) { + inherit_format(src_params, deduced_src.surface); + } else { + inherit_format(src_params, deduced_dst.surface); + } + if (!incomplete_dst) { + inherit_format(dst_params, deduced_dst.surface); + } else { + inherit_format(dst_params, deduced_src.surface); + } + } + std::pair<TSurface, TView> InitializeSurface(GPUVAddr gpu_addr, const SurfaceParams& params, bool preserve_contents) { auto new_surface{GetUncachedSurface(gpu_addr, params)}; diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index dc6fa07fc..ff1c1d985 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -66,6 +66,9 @@ add_executable(yuzu configuration/configure_profile_manager.cpp configuration/configure_profile_manager.h configuration/configure_profile_manager.ui + configuration/configure_service.cpp + configuration/configure_service.h + configuration/configure_service.ui configuration/configure_system.cpp configuration/configure_system.h configuration/configure_system.ui @@ -186,6 +189,10 @@ if (YUZU_USE_QT_WEB_ENGINE) target_compile_definitions(yuzu PRIVATE -DYUZU_USE_QT_WEB_ENGINE) endif () +if (YUZU_ENABLE_BOXCAT) + target_compile_definitions(yuzu PRIVATE -DYUZU_ENABLE_BOXCAT) +endif () + if(UNIX AND NOT APPLE) install(TARGETS yuzu RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin") endif() diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index 92d9fb161..4cb27ddb2 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -525,6 +525,17 @@ void Config::ReadDebuggingValues() { qt_config->endGroup(); } +void Config::ReadServiceValues() { + qt_config->beginGroup(QStringLiteral("Services")); + Settings::values.bcat_backend = + ReadSetting(QStringLiteral("bcat_backend"), QStringLiteral("boxcat")) + .toString() + .toStdString(); + Settings::values.bcat_boxcat_local = + ReadSetting(QStringLiteral("bcat_boxcat_local"), false).toBool(); + qt_config->endGroup(); +} + void Config::ReadDisabledAddOnValues() { const auto size = qt_config->beginReadArray(QStringLiteral("DisabledAddOns")); @@ -769,6 +780,7 @@ void Config::ReadValues() { ReadMiscellaneousValues(); ReadDebuggingValues(); ReadWebServiceValues(); + ReadServiceValues(); ReadDisabledAddOnValues(); ReadUIValues(); } @@ -866,6 +878,7 @@ void Config::SaveValues() { SaveMiscellaneousValues(); SaveDebuggingValues(); SaveWebServiceValues(); + SaveServiceValues(); SaveDisabledAddOnValues(); SaveUIValues(); } @@ -963,6 +976,14 @@ void Config::SaveDebuggingValues() { qt_config->endGroup(); } +void Config::SaveServiceValues() { + qt_config->beginGroup(QStringLiteral("Services")); + WriteSetting(QStringLiteral("bcat_backend"), + QString::fromStdString(Settings::values.bcat_backend), QStringLiteral("null")); + WriteSetting(QStringLiteral("bcat_boxcat_local"), Settings::values.bcat_boxcat_local, false); + qt_config->endGroup(); +} + void Config::SaveDisabledAddOnValues() { qt_config->beginWriteArray(QStringLiteral("DisabledAddOns")); diff --git a/src/yuzu/configuration/config.h b/src/yuzu/configuration/config.h index 6b523ecdd..ba6888004 100644 --- a/src/yuzu/configuration/config.h +++ b/src/yuzu/configuration/config.h @@ -42,6 +42,7 @@ private: void ReadCoreValues(); void ReadDataStorageValues(); void ReadDebuggingValues(); + void ReadServiceValues(); void ReadDisabledAddOnValues(); void ReadMiscellaneousValues(); void ReadPathValues(); @@ -65,6 +66,7 @@ private: void SaveCoreValues(); void SaveDataStorageValues(); void SaveDebuggingValues(); + void SaveServiceValues(); void SaveDisabledAddOnValues(); void SaveMiscellaneousValues(); void SavePathValues(); diff --git a/src/yuzu/configuration/configure.ui b/src/yuzu/configuration/configure.ui index 49fadd0ef..372427ae2 100644 --- a/src/yuzu/configuration/configure.ui +++ b/src/yuzu/configuration/configure.ui @@ -98,6 +98,11 @@ <string>Web</string> </attribute> </widget> + <widget class="ConfigureService" name="serviceTab"> + <attribute name="title"> + <string>Services</string> + </attribute> + </widget> </widget> </item> </layout> @@ -178,6 +183,12 @@ <header>configuration/configure_hotkeys.h</header> <container>1</container> </customwidget> + <customwidget> + <class>ConfigureService</class> + <extends>QWidget</extends> + <header>configuration/configure_service.h</header> + <container>1</container> + </customwidget> </customwidgets> <resources/> <connections> diff --git a/src/yuzu/configuration/configure_dialog.cpp b/src/yuzu/configuration/configure_dialog.cpp index 7c875ae87..25b2e1b05 100644 --- a/src/yuzu/configuration/configure_dialog.cpp +++ b/src/yuzu/configuration/configure_dialog.cpp @@ -44,6 +44,7 @@ void ConfigureDialog::ApplyConfiguration() { ui->audioTab->ApplyConfiguration(); ui->debugTab->ApplyConfiguration(); ui->webTab->ApplyConfiguration(); + ui->serviceTab->ApplyConfiguration(); Settings::Apply(); Settings::LogSettings(); } @@ -74,7 +75,8 @@ Q_DECLARE_METATYPE(QList<QWidget*>); void ConfigureDialog::PopulateSelectionList() { const std::array<std::pair<QString, QList<QWidget*>>, 4> items{ {{tr("General"), {ui->generalTab, ui->webTab, ui->debugTab, ui->gameListTab}}, - {tr("System"), {ui->systemTab, ui->profileManagerTab, ui->filesystemTab, ui->audioTab}}, + {tr("System"), + {ui->systemTab, ui->profileManagerTab, ui->serviceTab, ui->filesystemTab, ui->audioTab}}, {tr("Graphics"), {ui->graphicsTab}}, {tr("Controls"), {ui->inputTab, ui->hotkeysTab}}}, }; @@ -108,6 +110,7 @@ void ConfigureDialog::UpdateVisibleTabs() { {ui->webTab, tr("Web")}, {ui->gameListTab, tr("Game List")}, {ui->filesystemTab, tr("Filesystem")}, + {ui->serviceTab, tr("Services")}, }; [[maybe_unused]] const QSignalBlocker blocker(ui->tabWidget); diff --git a/src/yuzu/configuration/configure_service.cpp b/src/yuzu/configuration/configure_service.cpp new file mode 100644 index 000000000..06566e981 --- /dev/null +++ b/src/yuzu/configuration/configure_service.cpp @@ -0,0 +1,138 @@ +// Copyright 2019 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <QGraphicsItem> +#include <QtConcurrent/QtConcurrent> +#include "core/hle/service/bcat/backend/boxcat.h" +#include "core/settings.h" +#include "ui_configure_service.h" +#include "yuzu/configuration/configure_service.h" + +namespace { +QString FormatEventStatusString(const Service::BCAT::EventStatus& status) { + QString out; + + if (status.header.has_value()) { + out += QStringLiteral("<i>%1</i><br>").arg(QString::fromStdString(*status.header)); + } + + if (status.events.size() == 1) { + out += QStringLiteral("%1<br>").arg(QString::fromStdString(status.events.front())); + } else { + for (const auto& event : status.events) { + out += QStringLiteral("- %1<br>").arg(QString::fromStdString(event)); + } + } + + if (status.footer.has_value()) { + out += QStringLiteral("<i>%1</i><br>").arg(QString::fromStdString(*status.footer)); + } + + return out; +} +} // Anonymous namespace + +ConfigureService::ConfigureService(QWidget* parent) + : QWidget(parent), ui(std::make_unique<Ui::ConfigureService>()) { + ui->setupUi(this); + + ui->bcat_source->addItem(QStringLiteral("None")); + ui->bcat_empty_label->setHidden(true); + ui->bcat_empty_header->setHidden(true); + +#ifdef YUZU_ENABLE_BOXCAT + ui->bcat_source->addItem(QStringLiteral("Boxcat"), QStringLiteral("boxcat")); +#endif + + connect(ui->bcat_source, QOverload<int>::of(&QComboBox::currentIndexChanged), this, + &ConfigureService::OnBCATImplChanged); + + this->SetConfiguration(); +} + +ConfigureService::~ConfigureService() = default; + +void ConfigureService::ApplyConfiguration() { + Settings::values.bcat_backend = ui->bcat_source->currentText().toLower().toStdString(); +} + +void ConfigureService::RetranslateUi() { + ui->retranslateUi(this); +} + +void ConfigureService::SetConfiguration() { + const int index = + ui->bcat_source->findData(QString::fromStdString(Settings::values.bcat_backend)); + ui->bcat_source->setCurrentIndex(index == -1 ? 0 : index); +} + +std::pair<QString, QString> ConfigureService::BCATDownloadEvents() { + std::optional<std::string> global; + std::map<std::string, Service::BCAT::EventStatus> map; + const auto res = Service::BCAT::Boxcat::GetStatus(global, map); + + switch (res) { + case Service::BCAT::Boxcat::StatusResult::Success: + break; + case Service::BCAT::Boxcat::StatusResult::Offline: + return {QString{}, + tr("The boxcat service is offline or you are not connected to the internet.")}; + case Service::BCAT::Boxcat::StatusResult::ParseError: + return {QString{}, + tr("There was an error while processing the boxcat event data. Contact the yuzu " + "developers.")}; + case Service::BCAT::Boxcat::StatusResult::BadClientVersion: + return {QString{}, + tr("The version of yuzu you are using is either too new or too old for the server. " + "Try updating to the latest official release of yuzu.")}; + } + + if (map.empty()) { + return {QStringLiteral("Current Boxcat Events"), + tr("There are currently no events on boxcat.")}; + } + + QString out; + + if (global.has_value()) { + out += QStringLiteral("%1<br>").arg(QString::fromStdString(*global)); + } + + for (const auto& [key, value] : map) { + out += QStringLiteral("%1<b>%2</b><br>%3") + .arg(out.isEmpty() ? QString{} : QStringLiteral("<br>")) + .arg(QString::fromStdString(key)) + .arg(FormatEventStatusString(value)); + } + return {QStringLiteral("Current Boxcat Events"), std::move(out)}; +} + +void ConfigureService::OnBCATImplChanged() { +#ifdef YUZU_ENABLE_BOXCAT + const auto boxcat = ui->bcat_source->currentText() == QStringLiteral("Boxcat"); + ui->bcat_empty_header->setHidden(!boxcat); + ui->bcat_empty_label->setHidden(!boxcat); + ui->bcat_empty_header->setText(QString{}); + ui->bcat_empty_label->setText(tr("Yuzu is retrieving the latest boxcat status...")); + + if (!boxcat) + return; + + const auto future = QtConcurrent::run([this] { return BCATDownloadEvents(); }); + + watcher.setFuture(future); + connect(&watcher, &QFutureWatcher<std::pair<QString, QString>>::finished, this, + [this] { OnUpdateBCATEmptyLabel(watcher.result()); }); +#endif +} + +void ConfigureService::OnUpdateBCATEmptyLabel(std::pair<QString, QString> string) { +#ifdef YUZU_ENABLE_BOXCAT + const auto boxcat = ui->bcat_source->currentText() == QStringLiteral("Boxcat"); + if (boxcat) { + ui->bcat_empty_header->setText(string.first); + ui->bcat_empty_label->setText(string.second); + } +#endif +} diff --git a/src/yuzu/configuration/configure_service.h b/src/yuzu/configuration/configure_service.h new file mode 100644 index 000000000..f5c1b703a --- /dev/null +++ b/src/yuzu/configuration/configure_service.h @@ -0,0 +1,34 @@ +// Copyright 2019 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <memory> +#include <QFutureWatcher> +#include <QWidget> + +namespace Ui { +class ConfigureService; +} + +class ConfigureService : public QWidget { + Q_OBJECT + +public: + explicit ConfigureService(QWidget* parent = nullptr); + ~ConfigureService() override; + + void ApplyConfiguration(); + void RetranslateUi(); + +private: + void SetConfiguration(); + + std::pair<QString, QString> BCATDownloadEvents(); + void OnBCATImplChanged(); + void OnUpdateBCATEmptyLabel(std::pair<QString, QString> string); + + std::unique_ptr<Ui::ConfigureService> ui; + QFutureWatcher<std::pair<QString, QString>> watcher{this}; +}; diff --git a/src/yuzu/configuration/configure_service.ui b/src/yuzu/configuration/configure_service.ui new file mode 100644 index 000000000..9668dd557 --- /dev/null +++ b/src/yuzu/configuration/configure_service.ui @@ -0,0 +1,124 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ConfigureService</class> + <widget class="QWidget" name="ConfigureService"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>433</width> + <height>561</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>BCAT</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="1" column="1" colspan="2"> + <widget class="QLabel" name="label_2"> + <property name="maximumSize"> + <size> + <width>260</width> + <height>16777215</height> + </size> + </property> + <property name="text"> + <string>BCAT is Nintendo's way of sending data to games to engage its community and unlock additional content.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>16777215</height> + </size> + </property> + <property name="text"> + <string>BCAT Backend</string> + </property> + </widget> + </item> + <item row="3" column="1" colspan="2"> + <widget class="QLabel" name="bcat_empty_label"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="maximumSize"> + <size> + <width>260</width> + <height>16777215</height> + </size> + </property> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="1" colspan="2"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string><html><head/><body><p><a href="https://yuzu-emu.org/help/feature/boxcat"><span style=" text-decoration: underline; color:#0000ff;">Learn more about BCAT, Boxcat, and Current Events</span></a></p></body></html></string> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="1" colspan="2"> + <widget class="QComboBox" name="bcat_source"/> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="bcat_empty_header"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index d5fab2f1f..a2b88c787 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -172,9 +172,7 @@ void GameList::onTextChanged(const QString& new_text) { const int folder_count = tree_view->model()->rowCount(); QString edit_filter_text = new_text.toLower(); QStandardItem* folder; - QStandardItem* child; int children_total = 0; - QModelIndex root_index = item_model->invisibleRootItem()->index(); // If the searchfield is empty every item is visible // Otherwise the filter gets applied @@ -272,6 +270,8 @@ void GameList::onUpdateThemedIcons() { .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), Qt::DecorationRole); break; + default: + break; } } } @@ -392,6 +392,8 @@ void GameList::ValidateEntry(const QModelIndex& item) { case GameListItemType::AddDir: emit AddDirectory(); break; + default: + break; } } @@ -462,6 +464,8 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { case GameListItemType::SysNandDir: AddPermDirPopup(context_menu, selected); break; + default: + break; } context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location)); } diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h index a8d888fee..1c2b37afd 100644 --- a/src/yuzu/game_list_p.h +++ b/src/yuzu/game_list_p.h @@ -247,7 +247,7 @@ public: Qt::DecorationRole); setData(QObject::tr("System Titles"), Qt::DisplayRole); break; - case GameListItemType::CustomDir: + case GameListItemType::CustomDir: { const QString icon_name = QFileInfo::exists(game_dir->path) ? QStringLiteral("folder") : QStringLiteral("bad_folder"); @@ -256,8 +256,11 @@ public: Qt::DecorationRole); setData(game_dir->path, Qt::DisplayRole); break; - }; - }; + } + default: + break; + } + } int type() const override { return static_cast<int>(dir_type); diff --git a/src/yuzu/game_list_worker.cpp b/src/yuzu/game_list_worker.cpp index fd21a9761..4c81ef12b 100644 --- a/src/yuzu/game_list_worker.cpp +++ b/src/yuzu/game_list_worker.cpp @@ -326,10 +326,10 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa } } else { std::vector<u8> icon; - const auto res1 = loader->ReadIcon(icon); + [[maybe_unused]] const auto res1 = loader->ReadIcon(icon); std::string name = " "; - const auto res3 = loader->ReadTitle(name); + [[maybe_unused]] const auto res3 = loader->ReadTitle(name); const FileSys::PatchManager patch{program_id}; @@ -354,20 +354,20 @@ void GameListWorker::run() { for (UISettings::GameDir& game_dir : game_dirs) { if (game_dir.path == QStringLiteral("SDMC")) { auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SdmcDir); - emit DirEntryReady({game_list_dir}); + emit DirEntryReady(game_list_dir); AddTitlesToGameList(game_list_dir); } else if (game_dir.path == QStringLiteral("UserNAND")) { auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::UserNandDir); - emit DirEntryReady({game_list_dir}); + emit DirEntryReady(game_list_dir); AddTitlesToGameList(game_list_dir); } else if (game_dir.path == QStringLiteral("SysNAND")) { auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SysNandDir); - emit DirEntryReady({game_list_dir}); + emit DirEntryReady(game_list_dir); AddTitlesToGameList(game_list_dir); } else { watch_list.append(game_dir.path); auto* const game_list_dir = new GameListDir(game_dir); - emit DirEntryReady({game_list_dir}); + emit DirEntryReady(game_list_dir); provider->ClearAllEntries(); ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path.toStdString(), 2, game_list_dir); diff --git a/src/yuzu/game_list_worker.h b/src/yuzu/game_list_worker.h index 6e52fca89..84e4e1b42 100644 --- a/src/yuzu/game_list_worker.h +++ b/src/yuzu/game_list_worker.h @@ -75,8 +75,9 @@ private: std::shared_ptr<FileSys::VfsFilesystem> vfs; FileSys::ManualContentProvider* provider; - QStringList watch_list; - const CompatibilityList& compatibility_list; QVector<UISettings::GameDir>& game_dirs; + const CompatibilityList& compatibility_list; + + QStringList watch_list; std::atomic_bool stop_processing; }; diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 2d82df739..451e4a4d6 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -1889,15 +1889,24 @@ void GMainWindow::OnCaptureScreenshot() { } void GMainWindow::UpdateWindowTitle(const QString& title_name) { - const QString full_name = QString::fromUtf8(Common::g_build_fullname); - const QString branch_name = QString::fromUtf8(Common::g_scm_branch); - const QString description = QString::fromUtf8(Common::g_scm_desc); + const auto full_name = std::string(Common::g_build_fullname); + const auto branch_name = std::string(Common::g_scm_branch); + const auto description = std::string(Common::g_scm_desc); + const auto build_id = std::string(Common::g_build_id); + + const auto date = + QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd")).toStdString(); if (title_name.isEmpty()) { - setWindowTitle(QStringLiteral("yuzu %1| %2-%3").arg(full_name, branch_name, description)); + const auto fmt = std::string(Common::g_title_bar_format_idle); + setWindowTitle(QString::fromStdString(fmt::format(fmt.empty() ? "yuzu {0}| {1}-{2}" : fmt, + full_name, branch_name, description, + std::string{}, date, build_id))); } else { - setWindowTitle(QStringLiteral("yuzu %1| %4 | %2-%3") - .arg(full_name, branch_name, description, title_name)); + const auto fmt = std::string(Common::g_title_bar_format_running); + setWindowTitle(QString::fromStdString( + fmt::format(fmt.empty() ? "yuzu {0}| {3} | {1}-{2}" : fmt, full_name, branch_name, + description, title_name.toStdString(), date, build_id))); } } diff --git a/src/yuzu/uisettings.cpp b/src/yuzu/uisettings.cpp index 7f7d247a3..43bad9678 100644 --- a/src/yuzu/uisettings.cpp +++ b/src/yuzu/uisettings.cpp @@ -9,6 +9,8 @@ namespace UISettings { const Themes themes{{ {"Default", "default"}, {"Dark", "qdarkstyle"}, + {"Colorful", "colorful"}, + {"Colorful Dark", "colorful_dark"}, }}; Values values = {}; diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h index c57290006..a8eaf5f8c 100644 --- a/src/yuzu/uisettings.h +++ b/src/yuzu/uisettings.h @@ -24,7 +24,7 @@ struct Shortcut { ContextualShortcut shortcut; }; -using Themes = std::array<std::pair<const char*, const char*>, 2>; +using Themes = std::array<std::pair<const char*, const char*>, 4>; extern const Themes themes; struct GameDir { diff --git a/src/yuzu_cmd/config.cpp b/src/yuzu_cmd/config.cpp index d82438502..1a812cb87 100644 --- a/src/yuzu_cmd/config.cpp +++ b/src/yuzu_cmd/config.cpp @@ -433,6 +433,11 @@ void Config::ReadValues() { sdl2_config->Get("WebService", "web_api_url", "https://api.yuzu-emu.org"); Settings::values.yuzu_username = sdl2_config->Get("WebService", "yuzu_username", ""); Settings::values.yuzu_token = sdl2_config->Get("WebService", "yuzu_token", ""); + + // Services + Settings::values.bcat_backend = sdl2_config->Get("Services", "bcat_backend", "boxcat"); + Settings::values.bcat_boxcat_local = + sdl2_config->GetBoolean("Services", "bcat_boxcat_local", false); } void Config::Reload() { diff --git a/src/yuzu_cmd/default_ini.h b/src/yuzu_cmd/default_ini.h index a6171c3ed..8d18a4a5a 100644 --- a/src/yuzu_cmd/default_ini.h +++ b/src/yuzu_cmd/default_ini.h @@ -251,6 +251,11 @@ web_api_url = https://api.yuzu-emu.org yuzu_username = yuzu_token = +[Services] +# The name of the backend to use for BCAT +# If this is set to 'boxcat' boxcat will be used, otherwise a null implementation will be used +bcat_backend = + [AddOns] # Used to disable add-ons # List of title IDs of games that will have add-ons disabled (separated by '|'): diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl2.cpp b/src/yuzu_cmd/emu_window/emu_window_sdl2.cpp index a6edc089a..b1c512db1 100644 --- a/src/yuzu_cmd/emu_window/emu_window_sdl2.cpp +++ b/src/yuzu_cmd/emu_window/emu_window_sdl2.cpp @@ -4,6 +4,9 @@ #include <SDL.h> #include "common/logging/log.h" +#include "common/scm_rev.h" +#include "core/core.h" +#include "core/perf_stats.h" #include "input_common/keyboard.h" #include "input_common/main.h" #include "input_common/motion_emu.h" @@ -170,6 +173,16 @@ void EmuWindow_SDL2::PollEvents() { break; } } + + const u32 current_time = SDL_GetTicks(); + if (current_time > last_time + 2000) { + const auto results = Core::System::GetInstance().GetAndResetPerfStats(); + const auto title = fmt::format( + "yuzu {} | {}-{} | FPS: {:.0f} ({:.0%})", Common::g_build_fullname, + Common::g_scm_branch, Common::g_scm_desc, results.game_fps, results.emulation_speed); + SDL_SetWindowTitle(render_window, title.c_str()); + last_time = current_time; + } } void EmuWindow_SDL2::OnMinimalClientAreaChangeRequest(std::pair<unsigned, unsigned> minimal_size) { diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl2.h b/src/yuzu_cmd/emu_window/emu_window_sdl2.h index d8051ebdf..eaa971f77 100644 --- a/src/yuzu_cmd/emu_window/emu_window_sdl2.h +++ b/src/yuzu_cmd/emu_window/emu_window_sdl2.h @@ -60,4 +60,7 @@ protected: /// Internal SDL2 render window SDL_Window* render_window; + + /// Keeps track of how often to update the title bar during gameplay + u32 last_time = 0; }; diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp index bac05b959..3ee088a91 100644 --- a/src/yuzu_cmd/yuzu.cpp +++ b/src/yuzu_cmd/yuzu.cpp @@ -186,8 +186,6 @@ int main(int argc, char** argv) { system.SetFilesystem(std::make_shared<FileSys::RealVfsFilesystem>()); system.GetFileSystemController().CreateFactories(*system.GetFilesystem()); - SCOPE_EXIT({ system.Shutdown(); }); - const Core::System::ResultStatus load_result{system.Load(*emu_window, filepath)}; switch (load_result) { @@ -227,6 +225,8 @@ int main(int argc, char** argv) { system.RunLoop(); } + system.Shutdown(); + detached_tasks.WaitForAllTasks(); return 0; } diff --git a/src/yuzu_tester/config.cpp b/src/yuzu_tester/config.cpp index 9a11dc6c3..84ab4d687 100644 --- a/src/yuzu_tester/config.cpp +++ b/src/yuzu_tester/config.cpp @@ -93,7 +93,6 @@ void Config::ReadValues() { // System Settings::values.use_docked_mode = sdl2_config->GetBoolean("System", "use_docked_mode", false); - const auto size = sdl2_config->GetInteger("System", "users_size", 0); Settings::values.current_user = std::clamp<int>( sdl2_config->GetInteger("System", "current_user", 0), 0, Service::Account::MAX_USERS - 1); |